Merge branch 'ipad-support-1' into voice-calls-2

This commit is contained in:
Ryan Zhao 2022-03-02 14:31:31 +11:00
commit 52407aec03
118 changed files with 4092 additions and 778 deletions

View File

@ -64,7 +64,6 @@
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; };
4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; };
4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BC20470A5B00CEE724 /* classic.aifc */; };
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2041E0D74AC003D14BE /* Platform.swift */; };
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */; };
451166C01FD86B98000739BA /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451166BF1FD86B98000739BA /* AccountManager.swift */; };
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; };
@ -150,6 +149,12 @@
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; };
7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; };
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; };
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; };
7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */; };
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; };
7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */; };
7B93D07327CF19C800811CB6 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */; };
7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */; };
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; };
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; };
7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; };
@ -568,8 +573,6 @@
C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
C38EF216255B6D3B007E1867 /* Theme.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF212255B6D3A007E1867 /* Theme.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF218255B6D3B007E1867 /* Theme.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF214255B6D3A007E1867 /* Theme.m */; };
C38EF228255B6D5D007E1867 /* AttachmentSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF223255B6D5D007E1867 /* AttachmentSharing.m */; };
C38EF22A255B6D5D007E1867 /* AttachmentSharing.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF225255B6D5D007E1867 /* AttachmentSharing.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF22B255B6D5D007E1867 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */; };
C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */; };
C38EF243255B6D67007E1867 /* UIViewController+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF236255B6D65007E1867 /* UIViewController+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -1059,7 +1062,6 @@
4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "classic-quiet.aifc"; sourceTree = "<group>"; };
4503F1BC20470A5B00CEE724 /* classic.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = classic.aifc; sourceTree = "<group>"; };
4509E7991DD653700025A59F /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = ThirdParty/WebRTC/Build/WebRTC.framework; sourceTree = "<group>"; };
450DF2041E0D74AC003D14BE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = UserNotificationsAdaptee.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppNotifications.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@ -1160,6 +1162,12 @@
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = "<group>"; };
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = "<group>"; };
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = "<group>"; };
7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = "<group>"; };
7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = "<group>"; };
@ -1599,9 +1607,7 @@
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SNProtoEnvelope+Conversion.swift"; sourceTree = "<group>"; };
C38EF212255B6D3A007E1867 /* Theme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Theme.h; path = SignalUtilitiesKit/Utilities/Theme.h; sourceTree = SOURCE_ROOT; };
C38EF214255B6D3A007E1867 /* Theme.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Theme.m; path = SignalUtilitiesKit/Utilities/Theme.m; sourceTree = SOURCE_ROOT; };
C38EF223255B6D5D007E1867 /* AttachmentSharing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AttachmentSharing.m; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.m"; sourceTree = SOURCE_ROOT; };
C38EF224255B6D5D007E1867 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SignalAttachment.swift; path = "SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift"; sourceTree = SOURCE_ROOT; };
C38EF225255B6D5D007E1867 /* AttachmentSharing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AttachmentSharing.h; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.h"; sourceTree = SOURCE_ROOT; };
C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewDelegate.swift; path = SignalUtilitiesKit/Utilities/ShareViewDelegate.swift; sourceTree = SOURCE_ROOT; };
C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSVideoPlayer.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift"; sourceTree = SOURCE_ROOT; };
C38EF236255B6D65007E1867 /* UIViewController+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+OWS.h"; path = "SignalUtilitiesKit/Utilities/UIViewController+OWS.h"; sourceTree = SOURCE_ROOT; };
@ -2071,6 +2077,7 @@
76EB03C118170B33006006FC /* Utilities */ = {
isa = PBXGroup;
children = (
7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */,
451166BF1FD86B98000739BA /* AccountManager.swift */,
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */,
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */,
@ -2088,7 +2095,6 @@
45B5360D206DD8BB00D61655 /* UIResponder+OWS.swift */,
4C586924224FAB83003FD070 /* AVAudioSession+OWS.h */,
4C586925224FAB83003FD070 /* AVAudioSession+OWS.m */,
450DF2041E0D74AC003D14BE /* Platform.swift */,
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */,
B8544E3223D50E4900299F14 /* SNAppearance.swift */,
@ -2121,6 +2127,22 @@
path = "Views & Modals";
sourceTree = "<group>";
};
7B93D06827CF173D00811CB6 /* Message Requests */ = {
isa = PBXGroup;
children = (
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */,
);
path = "Message Requests";
sourceTree = "<group>";
};
7B93D06B27CF175800811CB6 /* Views */ = {
isa = PBXGroup;
children = (
7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */,
);
path = Views;
sourceTree = "<group>";
};
7BA68907272A279900EFC32F /* Call Management */ = {
isa = PBXGroup;
children = (
@ -2557,6 +2579,8 @@
C300A5C72554B03900555489 /* Control Messages */ = {
isa = PBXGroup;
children = (
7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */,
7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */,
C3C2A7702553A41E00C340D1 /* ControlMessage.swift */,
B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */,
C300A5BC2554B00D00555489 /* ReadReceipt.swift */,
@ -2934,6 +2958,8 @@
C360968E25AD16E8008B62B2 /* Home */ = {
isa = PBXGroup;
children = (
7B93D06B27CF175800811CB6 /* Views */,
7B93D06827CF173D00811CB6 /* Message Requests */,
7BAF54CA27ACCEEC003D12F8 /* GlobalSearch */,
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
@ -3078,8 +3104,6 @@
children = (
C379DCEA2567334F0002D4EB /* Attachment Approval */,
C379DCE9256733390002D4EB /* Image Editing */,
C38EF225255B6D5D007E1867 /* AttachmentSharing.h */,
C38EF223255B6D5D007E1867 /* AttachmentSharing.m */,
C38EF358255B6DCC007E1867 /* MediaMessageView.swift */,
C38EF357255B6DCC007E1867 /* MessageApprovalViewController.swift */,
C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */,
@ -3149,6 +3173,7 @@
C379DCE82567330E0002D4EB /* Migrations */ = {
isa = PBXGroup;
children = (
7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */,
B8B32044258C117C0020074B /* ContactsMigration.swift */,
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
@ -3749,7 +3774,6 @@
C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */,
C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */,
C33FDC2C255A581F00E217F9 /* OWSFailedAttachmentDownloadsJob.h in Headers */,
C38EF22A255B6D5D007E1867 /* AttachmentSharing.h in Headers */,
C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */,
C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */,
C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */,
@ -4646,7 +4670,6 @@
C38EF3BF255B6DE7007E1867 /* ImageEditorView.swift in Sources */,
C38EF365255B6DCC007E1867 /* OWSTableViewController.m in Sources */,
C38EF36B255B6DCC007E1867 /* ScreenLockViewController.m in Sources */,
C38EF228255B6D5D007E1867 /* AttachmentSharing.m in Sources */,
C38EF40C255B6DF7007E1867 /* GradientView.swift in Sources */,
C38EF35C255B6DCC007E1867 /* SelectThreadViewController.m in Sources */,
C38EF30E255B6DBF007E1867 /* FullTextSearcher.swift in Sources */,
@ -4674,6 +4697,7 @@
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */,
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */,
7B93D07327CF19C800811CB6 /* MessageRequestsMigration.swift in Sources */,
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */,
@ -4840,6 +4864,7 @@
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */,
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */,
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */,
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */,
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
@ -4861,6 +4886,7 @@
B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */,
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */,
C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */,
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */,
B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */,
@ -4997,7 +5023,6 @@
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */,
B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */,
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */,
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
@ -5087,6 +5112,7 @@
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */,
7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */,
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */,
C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */,
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
@ -5105,6 +5131,8 @@
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */,
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */,
7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */,
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
@ -6473,6 +6501,7 @@
SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_AFTER_BUILD = YES;
WRAPPER_EXTENSION = app;
};
@ -6542,6 +6571,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Session/Meta/Signal-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_AFTER_BUILD = YES;
WRAPPER_EXTENSION = app;
};

View File

@ -7,7 +7,6 @@
#import "Session-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalUtilitiesKit/AttachmentSharing.h>
#import <SessionMessagingKit/Environment.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SignalUtilitiesKit/UIColor+OWS.h>

View File

@ -2,6 +2,7 @@ import UIKit
import CoreServices
import Photos
import PhotosUI
import PromiseKit
import SessionUtilitiesKit
import SignalUtilitiesKit
@ -10,6 +11,16 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
ConversationTitleViewDelegate {
func handleTitleViewTapped() {
// Don't take the user to settings for message requests
guard
let contactThread: TSContactThread = thread as? TSContactThread,
let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()),
contact.isApproved,
contact.didApproveMe
else {
return
}
openSettings()
}
@ -53,6 +64,13 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
UIView.animate(withDuration: 0.25, animations: {
self.blockedBanner.alpha = 0
}, completion: { _ in
if let contact: Contact = Storage.shared.getContact(with: publicKey) {
Storage.shared.write { transaction in
contact.isBlocked = false
Storage.shared.setContact(contact, using: transaction)
}
}
OWSBlockingManager.shared().removeBlockedPhoneNumber(publicKey)
})
}
@ -174,7 +192,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
} catch {
let alert = UIAlertController(title: "Session", message: "An error occurred.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
return present(alert, animated: true, completion: nil)
return presentAlert(alert)
}
let type = urlResourceValues.typeIdentifier ?? (kUTTypeData as String)
guard urlResourceValues.isDirectory != true else {
@ -234,9 +252,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()
@ -245,58 +266,123 @@ 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)
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
with: transaction,
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
)
.map { [weak self] _ in
self?.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()
})
}
self?.handleMessageSent()
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
promise.retainUntilComplete()
})
}
func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) {
guard !showBlockedModalIfNeeded() else { return }
for attachment in attachments {
if attachment.hasError {
return showErrorAlert(for: attachment, onDismiss: onComplete)
}
}
let thread = self.thread
let sentTimestamp: UInt64 = NSDate.millisecondTimestamp()
let message = VisibleMessage()
message.sentTimestamp = NSDate.millisecondTimestamp()
message.sentTimestamp = sentTimestamp
message.text = replaceMentions(in: text)
// 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 tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
Storage.write(with: { transaction in
tsMessage.save(with: transaction)
// The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
}, completion: { [weak self] in
Storage.write(with: { transaction in
MessageSender.send(message, with: attachments, in: thread, using: transaction)
}, completion: { [weak self] in
// At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
})
self?.handleMessageSent()
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
with: transaction,
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
)
.map { [weak self] _ in
Storage.write(
with: { transaction in
tsMessage.save(with: transaction)
// The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet
},
completion: { [weak self] in
Storage.write(with: { transaction in
MessageSender.send(message, with: attachments, in: thread, using: transaction)
}, completion: { [weak self] in
// At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
})
self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen
onComplete?()
}
)
}
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
// Attachment successfully sent - dismiss the screen
onComplete?()
promise.retainUntilComplete()
})
}
@ -304,6 +390,21 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
resetMentions()
self.snInputView.text = ""
self.snInputView.quoteDraftInfo = nil
// Update the input state if this is a contact thread
if let contactThread: TSContactThread = thread as? TSContactThread {
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
// If the contact doesn't exist yet then it's a message request without the first message sent
// so only allow text-based messages
self.snInputView.setEnabledMessageTypes(
(thread.isNoteToSelf() || contact?.didApproveMe == true || thread.isMessageRequest() ?
.all : .textOnly
),
message: nil
)
}
self.markAllAsRead()
if Environment.shared.preferences.soundInForeground() {
let soundID = OWSSounds.systemSoundID(for: .messageSent, quiet: true)
@ -503,6 +604,12 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
// Open the document if possible
guard let url = viewItem.attachmentStream?.originalMediaURL else { return }
let shareVC = UIActivityViewController(activityItems: [ url ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
navigationController!.present(shareVC, animated: true, completion: nil)
}
case .textOnlyMessage:
@ -567,7 +674,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
}))
}
}
present(sheet, animated: true, completion: nil)
presentAlert(sheet)
}
func handleViewItemDoubleTapped(_ viewItem: ConversationViewItem) {
@ -702,7 +809,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
OpenGroupAPIV2.ban(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
presentAlert(alert)
}
func banAndDeleteAllMessages(_ viewItem: ConversationViewItem) {
@ -716,7 +823,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
OpenGroupAPIV2.banAndDeleteAllMessages(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
presentAlert(alert)
}
func handleQuoteViewCancelButtonTapped() {
@ -1028,3 +1135,182 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate {
return self
}
}
// MARK: - Message Request Actions
extension ConversationVC {
@objc func handleBackPressed() {
// If this thread started as a message request but isn't one anymore then we want to skip the
// `MessageRequestsViewController` when going back
guard
threadStartedAsMessageRequest,
!thread.isMessageRequest(),
let viewControllers: [UIViewController] = navigationController?.viewControllers,
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
messageRequestsIndex > 0
else {
navigationController?.popViewController(animated: true)
return
}
navigationController?.popToViewController(viewControllers[messageRequestsIndex - 1], animated: true)
}
fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) }
// 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))
guard !contact.isApproved else { return Promise.value(()) }
return Promise.value(())
.then { [weak self] _ -> Promise<Void> in
guard !isNewThread else { return Promise.value(()) }
guard let strongSelf = self else { return Promise(error: MessageSender.Error.noThread) }
// 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)
let (promise, seal) = Promise<Void>.pending()
let messageRequestResponse: MessageRequestResponse = MessageRequestResponse(
isApproved: true
)
messageRequestResponse.sentTimestamp = timestamp
// Show a loading indicator
ModalActivityIndicatorViewController.present(fromViewController: strongSelf, canCancel: false) { _ in
seal.fulfill(())
}
return promise
.then { MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) }
.map { _ in
if self?.presentedViewController is ModalActivityIndicatorViewController {
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
}
}
}
.map { _ in
// 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)
// 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
let messageRequestViewWasVisible: Bool = (self?.messageRequestView.isHidden == false)
UIView.animate(withDuration: 0.3) {
self?.messageRequestView.isHidden = true
self?.scrollButtonMessageRequestsBottomConstraint?.isActive = false
self?.scrollButtonBottomConstraint?.isActive = true
// Update the table content inset and offset to account for the dissapearance of
// the messageRequestsView
if messageRequestViewWasVisible {
let messageRequestsOffset: CGFloat = ((self?.messageRequestView.bounds.height ?? 0) + 16)
let oldContentInset: UIEdgeInsets = (self?.messagesTableView.contentInset ?? UIEdgeInsets.zero)
self?.messagesTableView.contentInset = UIEdgeInsets(
top: 0,
leading: 0,
bottom: max(oldContentInset.bottom - messageRequestsOffset, 0),
trailing: 0
)
}
}
// Update UI
self?.updateNavBarButtons()
// Send a sync message with the details of the contact
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.forceSyncConfigurationNowIfNeeded(with: transaction).retainUntilComplete()
}
}
}
}
@objc func acceptMessageRequest() {
Storage.write { transaction in
let promise: Promise<Void> = self.approveMessageRequestIfNeeded(
for: self.thread,
with: transaction,
isNewThread: false,
timestamp: NSDate.millisecondTimestamp()
)
// Show an error indicating that approving the thread failed
promise.catch(on: DispatchQueue.main) { [weak self] _ in
let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
promise.retainUntilComplete()
}
}
@objc func deleteMessageRequest() {
guard let uniqueId: String = thread.uniqueId else { return }
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
// Delete the request
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
// Note: We set this to true so the current user will be able to send a
// message to the person who originally sent them the message request in
// the future if they unblock them
contact.didApproveMe = 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) {
// Stop observing the `BlockListDidChange` notification (we are about to pop the screen
// so showing the banner just looks buggy)
if let strongSelf = self {
NotificationCenter.default.removeObserver(strongSelf, name: NSNotification.Name(rawValue: kNSNotificationName_BlockListDidChange), object: nil)
}
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)
}
}
)
})
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
self.present(alertVC, animated: true, completion: nil)
}
}

View File

@ -1,3 +1,6 @@
import SessionUIKit
import SessionMessagingKit
import UIKit
// TODO:
// Slight paging glitch when scrolling up and loading more content
@ -7,10 +10,13 @@
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
let isUnsendRequestsEnabled = true // Set to true once unsend requests are done on all platforms
let thread: TSThread
let threadStartedAsMessageRequest: Bool
let focusedMessageID: String? // This is used for global search
var focusedMessageIndexPath: IndexPath?
var unreadViewItems: [ConversationViewItem] = []
var scrollButtonConstraint: NSLayoutConstraint?
var scrollButtonBottomConstraint: NSLayoutConstraint?
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
var messageRequestsViewBotomConstraint: NSLayoutConstraint?
// Search
var isShowingSearchUI = false
var lastSearchedText: String?
@ -93,7 +99,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,13 +110,21 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}()
lazy var messagesTableView: MessagesTableView = {
let result = MessagesTableView()
let result: MessagesTableView = MessagesTableView()
result.dataSource = self
result.delegate = self
result.contentInsetAdjustmentBehavior = .never
result.contentInset = UIEdgeInsets(
top: 0,
leading: 0,
bottom: Values.mediumSpacing,
trailing: 0
)
return result
}()
lazy var snInputView = InputView(delegate: self)
lazy var snInputView: InputView = InputView(delegate: self)
lazy var unreadCountView: UIView = {
let result = UIView()
@ -128,8 +145,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 +161,104 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
return result
}()
lazy var footerControlsStackView: UIStackView = {
let result: UIStackView = UIStackView()
result.translatesAutoresizingMaskIntoConstraints = false
result.axis = .vertical
result.alignment = .trailing
result.distribution = .equalSpacing
result.spacing = 10
result.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
result.isLayoutMarginsRelativeArrangement = true
return result
}()
lazy var scrollButton = ScrollToBottomButton(delegate: self)
lazy var messageRequestView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.isHidden = !thread.isMessageRequest()
result.setGradient(Gradients.defaultBackground)
return result
}()
private let messageRequestDescriptionLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.font = UIFont.systemFont(ofSize: 12)
result.text = NSLocalizedString("MESSAGE_REQUESTS_INFO", comment: "")
result.textColor = Colors.sessionMessageRequestsInfoText
result.textAlignment = .center
result.numberOfLines = 2
return result
}()
private let messageRequestAcceptButton: UIButton = {
let result: UIButton = UIButton()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
result.setTitle(NSLocalizedString("TXT_DELETE_ACCEPT", comment: ""), for: .normal)
result.setTitleColor(Colors.sessionHeading, for: .normal)
result.setBackgroundImage(
Colors.sessionHeading
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
.toImage(isDarkMode: isDarkMode),
for: .highlighted
)
result.layer.cornerRadius = (ConversationVC.messageRequestButtonHeight / 2)
result.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
}()
result.layer.borderWidth = 1
result.addTarget(self, action: #selector(acceptMessageRequest), for: .touchUpInside)
return result
}()
private let messageRequestDeleteButton: UIButton = {
let result: UIButton = UIButton()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
result.setTitle(NSLocalizedString("TXT_DELETE_TITLE", comment: ""), for: .normal)
result.setTitleColor(Colors.destructive, for: .normal)
result.setBackgroundImage(
Colors.destructive
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
.toImage(isDarkMode: isDarkMode),
for: .highlighted
)
result.layer.cornerRadius = (ConversationVC.messageRequestButtonHeight / 2)
result.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
}()
result.layer.borderWidth = 1
result.addTarget(self, action: #selector(deleteMessageRequest), for: .touchUpInside)
return result
}()
// 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).
@ -162,6 +275,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// MARK: Lifecycle
init(thread: TSThread, focusedMessageID: String? = nil) {
self.thread = thread
self.threadStartedAsMessageRequest = thread.isMessageRequest()
self.focusedMessageID = focusedMessageID
super.init(nibName: nil, bundle: nil)
var unreadCount: UInt = 0
@ -187,9 +301,49 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// Constraints
view.addSubview(messagesTableView)
messagesTableView.pin(to: view)
// Blocked banner
addOrRemoveBlockedBanner()
// Message requests view & scroll to bottom
view.addSubview(scrollButton)
scrollButton.pin(.right, to: .right, of: view, withInset: -16)
scrollButtonConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
view.addSubview(messageRequestView)
messageRequestView.addSubview(messageRequestDescriptionLabel)
messageRequestView.addSubview(messageRequestAcceptButton)
messageRequestView.addSubview(messageRequestDeleteButton)
scrollButton.pin(.right, to: .right, of: view, withInset: -20)
messageRequestView.pin(.left, to: .left, of: view)
messageRequestView.pin(.right, to: .right, of: view)
self.messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16)
self.scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint
self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16)
self.scrollButtonMessageRequestsBottomConstraint?.isActive = thread.isMessageRequest()
self.scrollButtonBottomConstraint?.isActive = !thread.isMessageRequest()
messageRequestDescriptionLabel.pin(.top, to: .top, of: messageRequestView, withInset: 10)
messageRequestDescriptionLabel.pin(.left, to: .left, of: messageRequestView, withInset: 40)
messageRequestDescriptionLabel.pin(.right, to: .right, of: messageRequestView, withInset: -40)
messageRequestAcceptButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20)
messageRequestAcceptButton.pin(.left, to: .left, of: messageRequestView, withInset: 20)
messageRequestAcceptButton.pin(.bottom, to: .bottom, of: messageRequestView)
messageRequestAcceptButton.set(.height, to: ConversationVC.messageRequestButtonHeight)
messageRequestAcceptButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20)
messageRequestAcceptButton.pin(.left, to: .left, of: messageRequestView, withInset: 20)
messageRequestAcceptButton.pin(.bottom, to: .bottom, of: messageRequestView)
messageRequestAcceptButton.set(.height, to: ConversationVC.messageRequestButtonHeight)
messageRequestDeleteButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20)
messageRequestDeleteButton.pin(.left, to: .right, of: messageRequestAcceptButton, withInset: 20)
messageRequestDeleteButton.pin(.right, to: .right, of: messageRequestView, withInset: -20)
messageRequestDeleteButton.pin(.bottom, to: .bottom, of: messageRequestView)
messageRequestDeleteButton.set(.width, to: .width, of: messageRequestAcceptButton)
messageRequestDeleteButton.set(.height, to: ConversationVC.messageRequestButtonHeight)
// Unread count view
view.addSubview(unreadCountView)
unreadCountView.addSubview(unreadCountLabel)
@ -200,8 +354,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)
@ -221,6 +374,21 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
if !draft.isEmpty {
snInputView.text = draft
}
// Update the input state if this is a contact thread
if let contactThread: TSContactThread = thread as? TSContactThread {
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
// If the contact doesn't exist yet then it's a message request without the first message sent
// so only allow text-based messages
self.snInputView.setEnabledMessageTypes(
(thread.isNoteToSelf() || contact?.didApproveMe == true || thread.isMessageRequest() ?
.all : .textOnly
),
message: nil
)
}
// Update member count if this is a V2 open group
if let v2OpenGroup = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
OpenGroupAPIV2.getMemberCount(for: v2OpenGroup.room, on: v2OpenGroup.server).retainUntilComplete()
@ -297,32 +465,46 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
// MARK: Updating
func updateNavBarButtons() {
navigationItem.hidesBackButton = isShowingSearchUI
if isShowingSearchUI {
navigationItem.leftBarButtonItem = nil
navigationItem.rightBarButtonItems = []
} else {
}
else {
navigationItem.leftBarButtonItem = UIViewController.createOWSBackButton(withTarget: self, selector: #selector(handleBackPressed))
var rightBarButtonItems: [UIBarButtonItem] = []
if thread is TSContactThread {
let size = Values.verySmallProfilePictureSize
let profilePictureView = ProfilePictureView()
profilePictureView.accessibilityLabel = "Settings button"
profilePictureView.size = size
profilePictureView.update(for: thread)
profilePictureView.set(.width, to: size)
profilePictureView.set(.height, to: size)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
let settingsButton = UIBarButtonItem(customView: profilePictureView)
settingsButton.accessibilityLabel = "Settings button"
settingsButton.isAccessibilityElement = true
rightBarButtonItems.append(settingsButton)
let shouldShowCallButton = SessionCall.isEnabled && !thread.isNoteToSelf() && SSKPreferences.areCallsEnabled
if shouldShowCallButton {
let callButton = UIBarButtonItem(image: UIImage(named: "Phone")!, style: .plain, target: self, action: #selector(startCall))
rightBarButtonItems.append(callButton)
if let contactThread: TSContactThread = thread as? TSContactThread {
// Don't show the settings button for message requests
if let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()), contact.isApproved, contact.didApproveMe {
let size = Values.verySmallProfilePictureSize
let profilePictureView = ProfilePictureView()
profilePictureView.accessibilityLabel = "Settings button"
profilePictureView.size = size
profilePictureView.update(for: thread)
profilePictureView.set(.width, to: size)
profilePictureView.set(.height, to: size)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
let settingsButton = UIBarButtonItem(customView: profilePictureView)
settingsButton.accessibilityLabel = "Settings button"
settingsButton.isAccessibilityElement = true
rightBarButtonItems.append(settingsButton)
let shouldShowCallButton = SessionCall.isEnabled && !thread.isNoteToSelf() && SSKPreferences.areCallsEnabled
if shouldShowCallButton {
let callButton = UIBarButtonItem(image: UIImage(named: "Phone")!, style: .plain, target: self, action: #selector(startCall))
rightBarButtonItems.append(callButton)
}
}
} else {
else {
// Note: Adding an empty button because without it the title alignment is busted (Note: The size was
// taken from the layout inspector for the back button in Xcode
rightBarButtonItems.append(UIBarButtonItem(customView: UIView(frame: CGRect(x: 0, y: 0, width: 37, height: 44))))
}
}
else {
let settingsButton = UIBarButtonItem(image: UIImage(named: "Gear"), style: .plain, target: self, action: #selector(openSettings))
settingsButton.accessibilityLabel = "Settings button"
settingsButton.isAccessibilityElement = true
@ -340,24 +522,96 @@ 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?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 16)
self?.messageRequestsViewBotomConstraint?.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?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 16)
self?.messageRequestsViewBotomConstraint?.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() {
@ -402,6 +656,20 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
self.scrollToBottom(isAnimated: false)
}
}
// Update the input state if this is a contact thread
if let contactThread: TSContactThread = thread as? TSContactThread {
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
// If the contact doesn't exist yet then it's a message request without the first message sent
// so only allow text-based messages
self.snInputView.setEnabledMessageTypes(
(thread.isNoteToSelf() || contact?.didApproveMe == true || thread.isMessageRequest() ?
.all : .textOnly
),
message: nil
)
}
}
func conversationViewModelWillLoadMoreItems() {
@ -468,7 +736,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
func markAllAsRead() {
guard let lastSortID = viewItems.last?.interaction.sortId else { return }
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: lastSortID, thread: thread)
OWSReadReceiptManager.shared().markAsReadLocally(
beforeSortId: lastSortID,
thread: thread,
trySendReadReceipt: !thread.isMessageRequest()
)
SSKEnvironment.shared.disappearingMessagesJob.cleanupMessagesWhichFailedToStartExpiringFromNow()
}
@ -568,29 +840,34 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
func showSearchUI() {
isShowingSearchUI = true
// Search bar
// FIXME: This code is duplicated with SearchBar
let searchBar = searchController.uiSearchController.searchBar
searchBar.searchBarStyle = .minimal
searchBar.barStyle = .black
searchBar.tintColor = Colors.accent
let searchIcon = UIImage(named: "searchbar_search")!.asTintedImage(color: Colors.searchBarPlaceholder)
searchBar.setImage(searchIcon, for: .search, state: UIControl.State.normal)
let clearIcon = UIImage(named: "searchbar_clear")!.asTintedImage(color: Colors.searchBarPlaceholder)
searchBar.setImage(clearIcon, for: .clear, state: UIControl.State.normal)
let searchTextField: UITextField
if #available(iOS 13, *) {
searchTextField = searchBar.searchTextField
searchBar.setUpSessionStyle()
let searchBarContainer = UIView()
searchBarContainer.layoutMargins = UIEdgeInsets.zero
searchBar.sizeToFit()
searchBar.layoutMargins = UIEdgeInsets.zero
searchBarContainer.set(.height, to: 44)
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
searchBarContainer.addSubview(searchBar)
navigationItem.titleView = searchBarContainer
// On iPad, the cancel button won't show
// See more https://developer.apple.com/documentation/uikit/uisearchbar/1624283-showscancelbutton?language=objc
if UIDevice.current.isIPad {
let ipadCancelButton = UIButton()
ipadCancelButton.setTitle("Cancel", for: .normal)
ipadCancelButton.addTarget(self, action: #selector(hideSearchUI(_ :)), for: .touchUpInside)
ipadCancelButton.setTitleColor(Colors.text, for: .normal)
searchBarContainer.addSubview(ipadCancelButton)
ipadCancelButton.pin(.trailing, to: .trailing, of: searchBarContainer)
ipadCancelButton.autoVCenterInSuperview()
searchBar.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .trailing)
searchBar.pin(.trailing, to: .leading, of: ipadCancelButton, withInset: -Values.smallSpacing)
} else {
searchTextField = searchBar.value(forKey: "_searchField") as! UITextField
searchBar.autoPinEdgesToSuperviewMargins()
}
searchTextField.backgroundColor = Colors.searchBarBackground
searchTextField.textColor = Colors.text
searchTextField.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [ .foregroundColor : Colors.searchBarPlaceholder ])
searchTextField.keyboardAppearance = isLightMode ? .default : .dark
searchBar.setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: .search)
searchBar.searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
searchBar.setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: .clear)
navigationItem.titleView = searchBar
// Nav bar buttons
updateNavBarButtons()
// Hack so that the ResultsBar stays on the screen when dismissing the search field
@ -624,7 +901,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
navBar.stubbedNextResponder = self
}
func hideSearchUI() {
@objc func hideSearchUI(_ sender: Any? = nil) {
isShowingSearchUI = false
navigationItem.titleView = titleView
updateNavBarButtons()

View File

@ -131,7 +131,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
- (void)copyMediaAction;
- (void)copyTextAction;
- (void)shareMediaAction;
- (void)saveMediaAction;
- (void)deleteLocallyAction;
- (void)deleteRemotelyAction;

View File

@ -800,42 +800,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
}
- (void)shareMediaAction
{
if (self.attachmentPointer != nil) {
OWSFailDebug(@"Can't share not-yet-downloaded attachment");
return;
}
switch (self.messageCellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextOnlyMessage:
case OWSMessageCellType_Audio:
case OWSMessageCellType_GenericAttachment:
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
break;
case OWSMessageCellType_MediaMessage: {
// TODO: We need a "canShareMediaAction" method.
OWSAssertDebug(self.mediaAlbumItems);
NSMutableArray<TSAttachmentStream *> *attachmentStreams = [NSMutableArray new];
for (ConversationMediaAlbumItem *mediaAlbumItem in self.mediaAlbumItems) {
if (mediaAlbumItem.attachmentStream && mediaAlbumItem.attachmentStream.isValidVisualMedia) {
[attachmentStreams addObject:mediaAlbumItem.attachmentStream];
}
}
if (attachmentStreams.count < 1) {
OWSFailDebug(@"Can't share media album; no valid items.");
return;
}
[AttachmentSharing showShareUIForAttachments:attachmentStreams completion:nil];
break;
}
case OWSMessageCellType_OversizeTextDownloading:
OWSFailDebug(@"Can't share not-yet-downloaded attachment");
return;
}
}
- (BOOL)canCopyMedia
{
if (self.attachmentPointer != nil) {

View File

@ -3,6 +3,16 @@ final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate {
private weak var delegate: ExpandingAttachmentsButtonDelegate?
private var isExpanded = false { didSet { expandOrCollapse() } }
override var isUserInteractionEnabled: Bool {
didSet {
gifButton.isUserInteractionEnabled = isUserInteractionEnabled
documentButton.isUserInteractionEnabled = isUserInteractionEnabled
libraryButton.isUserInteractionEnabled = isUserInteractionEnabled
cameraButton.isUserInteractionEnabled = isUserInteractionEnabled
mainButton.isUserInteractionEnabled = isUserInteractionEnabled
}
}
// MARK: Constraints
private lazy var gifButtonContainerBottomConstraint = gifButtonContainer.pin(.bottom, to: .bottom, of: self)
private lazy var documentButtonContainerBottomConstraint = documentButtonContainer.pin(.bottom, to: .bottom, of: self)
@ -134,7 +144,8 @@ final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate {
}
// MARK: Delegate
protocol ExpandingAttachmentsButtonDelegate : class {
protocol ExpandingAttachmentsButtonDelegate: AnyObject {
func handleGIFButtonTapped()
func handleDocumentButtonTapped()

View File

@ -1,5 +1,13 @@
import UIKit
import SessionUIKit
final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, QuoteViewDelegate, LinkPreviewViewDelegate, MentionSelectionViewDelegate {
enum MessageTypes {
case all
case textOnly
case none
}
private weak var delegate: InputViewDelegate?
var quoteDraftInfo: (model: OWSQuotedReplyModel, isOutgoing: Bool)? { didSet { handleQuoteDraftChanged() } }
var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
@ -16,10 +24,18 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
set { inputTextView.text = newValue }
}
var enabledMessageTypes: MessageTypes = .all {
didSet {
setEnabledMessageTypes(enabledMessageTypes, message: nil)
}
}
override var intrinsicContentSize: CGSize { CGSize.zero }
var lastSearchedText: String? { nil }
// MARK: UI Components
private var bottomStackView: UIStackView?
private lazy var attachmentsButton = ExpandingAttachmentsButton(delegate: delegate)
private lazy var voiceMessageButton: InputViewButton = {
@ -29,6 +45,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
return result
}()
private lazy var sendButton: InputViewButton = {
let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
result.isHidden = true
@ -66,6 +83,17 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
let maxWidth = UIScreen.main.bounds.width - 2 * InputViewButton.expandedSize - 2 * Values.smallSpacing - 2 * (Values.mediumSpacing - adjustment)
return InputTextView(delegate: self, maxWidth: maxWidth)
}()
private lazy var disabledInputLabel: UILabel = {
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: Values.smallFontSize)
label.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
label.textAlignment = .center
label.alpha = 0
return label
}()
private lazy var additionalContentContainer = UIView()
@ -109,6 +137,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
bottomStackView.axis = .horizontal
bottomStackView.spacing = Values.smallSpacing
bottomStackView.alignment = .center
self.bottomStackView = bottomStackView
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ additionalContentContainer, bottomStackView ])
mainStackView.axis = .vertical
@ -119,6 +148,14 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
mainStackView.pin(.top, to: .bottom, of: separator)
mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self)
mainStackView.pin(.bottom, to: .bottom, of: self)
addSubview(disabledInputLabel)
disabledInputLabel.pin(.top, to: .top, of: mainStackView)
disabledInputLabel.pin(.left, to: .left, of: mainStackView)
disabledInputLabel.pin(.right, to: .right, of: mainStackView)
disabledInputLabel.set(.height, to: InputViewButton.expandedSize)
// Mentions
insertSubview(mentionsViewContainer, belowSubview: mainStackView)
mentionsViewContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: self)
@ -168,6 +205,9 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
}
private func autoGenerateLinkPreviewIfPossible() {
// Don't allow link previews on 'none' or 'textOnly' input
guard enabledMessageTypes == .all else { return }
// Suggest that the user enable link previews if they haven't already and we haven't
// told them about link previews yet
let text = inputTextView.text!
@ -216,6 +256,29 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
}.retainUntilComplete()
}
func setEnabledMessageTypes(_ messageTypes: MessageTypes, message: String?) {
guard enabledMessageTypes != messageTypes else { return }
enabledMessageTypes = messageTypes
disabledInputLabel.text = (message ?? "")
attachmentsButton.isUserInteractionEnabled = (messageTypes == .all)
voiceMessageButton.isUserInteractionEnabled = (messageTypes == .all)
UIView.animate(withDuration: 0.3) { [weak self] in
self?.bottomStackView?.alpha = (messageTypes != .none ? 1 : 0)
self?.attachmentsButton.alpha = (messageTypes == .all ?
1 :
(messageTypes == .textOnly ? 0.4 : 0)
)
self?.voiceMessageButton.alpha = (messageTypes == .all ?
1 :
(messageTypes == .textOnly ? 0.4 : 0)
)
self?.disabledInputLabel.alpha = (messageTypes != .none ? 0 : 1)
}
}
// MARK: Interaction
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Needed so that the user can tap the buttons when the expanding attachments button is expanded

View File

@ -98,6 +98,8 @@ final class InputViewButton : UIView {
// We want to detect both taps and long presses
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isUserInteractionEnabled else { return }
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
expand()
invalidateLongPressIfNeeded()
@ -109,12 +111,16 @@ final class InputViewButton : UIView {
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isUserInteractionEnabled else { return }
if isLongPress {
delegate?.handleInputViewButtonLongPressMoved(self, with: touches.first!)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isUserInteractionEnabled else { return }
collapse()
if !isLongPress {
delegate?.handleInputViewButtonTapped(self)

View File

@ -165,6 +165,13 @@ public class LongTextViewController: OWSViewController {
// MARK: - Actions
@objc func shareButtonPressed() {
AttachmentSharing.showShareUI(forText: fullText)
let shareVC = UIActivityViewController(activityItems: [ fullText ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
self.present(shareVC, animated: true, completion: nil)
}
}

View File

@ -121,7 +121,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
private static let authorLabelInset: CGFloat = 12
private static let replyButtonSize: CGFloat = 24
private static let maxBubbleTranslationX: CGFloat = 40
private static let swipeToReplyThreshold: CGFloat = 130
private static let swipeToReplyThreshold: CGFloat = 110
static let smallCornerRadius: CGFloat = 4
static let largeCornerRadius: CGFloat = 18
static let contactThreadHSpacing = Values.mediumSpacing

View File

@ -277,7 +277,7 @@ CGFloat kIconViewLength = 24;
contents.title = NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen");
BOOL isNoteToSelf = self.thread.isNoteToSelf;
__weak OWSConversationSettingsViewController *weakSelf = self;
OWSTableSection *section = [OWSTableSection new];
@ -332,7 +332,7 @@ CGFloat kIconViewLength = 24;
} actionBlock:^{
[weakSelf tappedConversationSearch];
}]];
// Disappearing messages
if (![self isOpenGroup]) {
[section addItem:[OWSTableItem itemWithCustomCellBlock:^{
@ -430,7 +430,7 @@ CGFloat kIconViewLength = 24;
[slider autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:rowLabel];
[slider autoPinTrailingToSuperviewMargin];
[slider autoPinBottomToSuperviewMargin];
cell.userInteractionEnabled = !strongSelf.hasLeftGroup;
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(

View File

@ -75,13 +75,17 @@ final class ConversationTitleView : UIView {
private func getTitle() -> String {
if let thread = thread as? TSGroupThread {
return thread.groupModel.groupName!
} else if thread.isNoteToSelf() {
}
else if thread.isNoteToSelf() {
return "Note to Self"
} else {
}
else {
let sessionID = (thread as! TSContactThread).contactSessionID()
var result = sessionID
Storage.read { transaction in
result = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? "Anonymous"
let displayName: String = ((Storage.shared.getContact(with: sessionID)?.displayName(for: .regular)) ?? sessionID)
let middleTruncatedHexKey: String = "\(sessionID.prefix(4))...\(sessionID.suffix(4))"
result = (displayName == sessionID ? middleTruncatedHexKey : displayName)
}
return result
}

View File

@ -66,7 +66,7 @@ final class JoinOpenGroupModal : Modal {
guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: url) else {
let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
return presentingViewController!.present(alert, animated: true, completion: nil)
return presentingViewController!.presentAlert(alert)
}
presentingViewController!.dismiss(animated: true, completion: nil)
Storage.shared.write { [presentingViewController = self.presentingViewController!] transaction in
@ -78,7 +78,7 @@ final class JoinOpenGroupModal : Modal {
.catch(on: DispatchQueue.main) { error in
let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentingViewController.present(alert, animated: true, completion: nil)
presentingViewController.presentAlert(alert)
}
}
}

View File

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

View File

@ -73,7 +73,7 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
tabBar.pin(.leading, to: .leading, of: view)
let tabBarInset: CGFloat
if #available(iOS 13, *) {
tabBarInset = navigationBar.height()
tabBarInset = UIDevice.current.isIPad ? navigationBar.height() + 20 : navigationBar.height()
} else {
tabBarInset = 0
}
@ -332,6 +332,12 @@ private final class EnterPublicKeyVC : UIViewController {
@objc private func sharePublicKey() {
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
NewDMVC.navigationController!.present(shareVC, animated: true, completion: nil)
}

View File

@ -1,5 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
@objc
class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
@ -94,8 +96,23 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
searchBarContainer.set(.height, to: 44)
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
searchBarContainer.addSubview(searchBar)
searchBar.autoPinEdgesToSuperviewMargins()
navigationItem.titleView = searchBarContainer
// On iPad, the cancel button won't show
// See more https://developer.apple.com/documentation/uikit/uisearchbar/1624283-showscancelbutton?language=objc
if UIDevice.current.isIPad {
let ipadCancelButton = UIButton()
ipadCancelButton.setTitle("Cancel", for: .normal)
ipadCancelButton.addTarget(self, action: #selector(cancel(_:)), for: .touchUpInside)
ipadCancelButton.setTitleColor(Colors.text, for: .normal)
searchBarContainer.addSubview(ipadCancelButton)
ipadCancelButton.pin(.trailing, to: .trailing, of: searchBarContainer)
ipadCancelButton.autoVCenterInSuperview()
searchBar.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .trailing)
searchBar.pin(.trailing, to: .leading, of: ipadCancelButton, withInset: -Values.smallSpacing)
} else {
searchBar.autoPinEdgesToSuperviewMargins()
}
}
private func reloadTableData() {
@ -169,6 +186,10 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
tableView.reloadSections([ SearchSection.recent.rawValue ], with: .top)
Storage.shared.clearRecentSearchResults()
}
@objc func cancel(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
}
}

View File

@ -6,6 +6,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
private var threads: YapDatabaseViewMappings!
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var tableViewTopConstraint: NSLayoutConstraint!
private var unreadMessageRequestCount: UInt {
OWSMessageUtils.sharedManager().unreadMessageRequestCount()
}
private var threadCount: UInt {
threads.numberOfItems(inGroup: TSInboxGroup)
@ -34,6 +37,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 +136,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 +171,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 unreadMessageRequestCount > 0 && !CurrentAppContext().appUserDefaults()[.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(unreadMessageRequestCount))
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 +231,107 @@ 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 && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
}
}
// If there are no unread message requests then hide the message request banner
if unreadMessageRequestCount == 0 {
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = true
}
return reload()
}
}
var sectionChanges = NSArray()
var rowChanges = NSArray()
ext.getSectionChanges(&sectionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
// Separate out the changes for new message requests and the inbox (so we can avoid updating for
// new messages within an existing message request)
let messageRequestChanges = rowChanges
.compactMap { $0 as? YapDatabaseViewRowChange }
.filter { $0.originalGroup == TSMessageRequestGroup || $0.finalGroup == TSMessageRequestGroup }
let inboxRowChanges = rowChanges
.compactMap { $0 as? YapDatabaseViewRowChange }
.filter { $0.originalGroup == TSInboxGroup || $0.finalGroup == TSInboxGroup }
guard sectionChanges.count > 0 || inboxRowChanges.count > 0 || messageRequestChanges.count > 0 else { return }
tableView.beginUpdates()
rowChanges.forEach { rowChange in
let rowChange = rowChange as! YapDatabaseViewRowChange
// If we need to unhide the message request row and then re-insert it
if !messageRequestChanges.isEmpty {
// If there are no unread message requests then hide the message request banner
if unreadMessageRequestCount == 0 && tableView.numberOfRows(inSection: 0) == 1 {
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = true
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
}
else {
if tableView.numberOfRows(inSection: 0) == 1 && Int(unreadMessageRequestCount) <= 0 {
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
}
else if tableView.numberOfRows(inSection: 0) == 0 && Int(unreadMessageRequestCount) > 0 && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
}
}
}
inboxRowChanges.forEach { rowChange in
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)
}
else if rowChange.originalGroup == TSInboxGroup && rowChange.finalGroup == TSMessageRequestGroup {
tableView.deleteRows(at: [ rowChange.indexPath! ], with: .automatic)
}
default: break
}
}
tableView.endUpdates()
@ -237,9 +344,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 == TSMessageRequestGroup {
return
}
tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
default: break
}
}
tableView.endUpdates()
@ -289,11 +405,13 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
profilePictureViewContainer.addSubview(pathStatusView)
pathStatusView.pin(.trailing, to: .trailing, of: profilePictureViewContainer)
pathStatusView.pin(.bottom, to: .bottom, of: profilePictureViewContainer)
// Left bar button item
let leftBarButtonItem = UIBarButtonItem(customView: profilePictureViewContainer)
leftBarButtonItem.accessibilityLabel = "Settings button"
leftBarButtonItem.isAccessibilityElement = true
navigationItem.leftBarButtonItem = leftBarButtonItem
// Right bar button item - search button
let rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(showSearchUI))
rightBarButtonItem.accessibilityLabel = "Search button"
@ -308,19 +426,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
CurrentAppContext().appUserDefaults()[.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.presentAlert(alert)
}
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 +534,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
@ -410,6 +556,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
@objc private func openSettings() {
let settingsVC = SettingsVC()
let navigationController = OWSNavigationController(rootViewController: settingsVC)
navigationController.modalPresentationStyle = .fullScreen
present(navigationController, animated: true, completion: nil)
}
@ -424,12 +571,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
@objc func joinOpenGroup() {
let joinOpenGroupVC = JoinOpenGroupVC()
let navigationController = OWSNavigationController(rootViewController: joinOpenGroupVC)
if UIDevice.current.isIPad {
navigationController.modalPresentationStyle = .fullScreen
}
present(navigationController, animated: true, completion: nil)
}
@objc func createNewDM() {
let newDMVC = NewDMVC()
let navigationController = OWSNavigationController(rootViewController: newDMVC)
if UIDevice.current.isIPad {
navigationController.modalPresentationStyle = .fullScreen
}
present(navigationController, animated: true, completion: nil)
}
@ -437,12 +590,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
func createNewDMFromDeepLink(sessionID: String) {
let newDMVC = NewDMVC(sessionID: sessionID)
let navigationController = OWSNavigationController(rootViewController: newDMVC)
if UIDevice.current.isIPad {
navigationController.modalPresentationStyle = .fullScreen
}
present(navigationController, animated: true, completion: nil)
}
@objc func createClosedGroup() {
let newClosedGroupVC = NewClosedGroupVC()
let navigationController = OWSNavigationController(rootViewController: newClosedGroupVC)
if UIDevice.current.isIPad {
navigationController.modalPresentationStyle = .fullScreen
}
present(navigationController, animated: true, completion: nil)
}
@ -450,8 +609,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
private func thread(at index: Int) -> TSThread? {
var thread: TSThread? = nil
dbConnection.read { transaction in
// Note: Section needs to be '1' as we now have 'TSMessageRequests' as the 0th section
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
thread = ext.object(atRow: UInt(index), inSection: 1, with: self.threads) as? TSThread
}
return thread
}

View File

@ -0,0 +1,441 @@
// 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 result: UITableView = UITableView()
result.translatesAutoresizingMaskIntoConstraints = false
result.backgroundColor = .clear
result.separatorStyle = .none
result.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier)
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
result.dataSource = self
result.delegate = self
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
result.showsVerticalScrollIndicator = false
return result
}()
private lazy var emptyStateLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.font = UIFont.systemFont(ofSize: Values.smallFontSize)
result.text = NSLocalizedString("MESSAGE_REQUESTS_EMPTY_TEXT", comment: "")
result.textColor = Colors.text
result.textAlignment = .center
result.numberOfLines = 0
result.isHidden = true
return result
}()
private lazy var fadeView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.setGradient(Gradients.homeVCFade)
return result
}()
private lazy var clearAllButton: Button = {
let result: Button = Button(style: .destructiveOutline, size: .large)
result.translatesAutoresizingMaskIntoConstraints = false
result.setTitle(NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL", comment: ""), for: .normal)
result.setBackgroundImage(
Colors.destructive
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
.toImage(isDarkMode: isDarkMode),
for: .highlighted
)
result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
return result
}()
// 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(emptyStateLabel)
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),
emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing),
emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing),
emptyStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.mediumSpacing),
emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
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.safeAreaLayoutGuide.bottomAnchor,
constant: -Values.largeSpacing
),
// Note: The '182' is to match the 'Next' button on the New DM page (which doesn't have a fixed width)
clearAllButton.widthAnchor.constraint(equalToConstant: 182),
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)
emptyStateLabel.isHidden = (messageRequestCount != 0)
emptyStateLabel.isHidden = (messageRequestCount != 0)
}
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
// NOTE: This code is very finicky and crashes easily. Modify with care.
AssertIsOnMainThread()
// If we don't capture `threads` here, a race condition can occur where the
// `thread.snapshotOfLastUpdate != firstSnapshot - 1` check below evaluates to
// `false`, but `threads` then changes between that check and the
// `ext.getSectionChanges(&sectionChanges, rowChanges: &rowChanges, for: notifications, with: threads)`
// line. This causes `tableView.endUpdates()` to crash with an `NSInternalInconsistencyException`.
let threads = threads!
// Create a stable state for the connection and jump to the latest commit
let notifications = dbConnection.beginLongLivedReadTransaction()
guard !notifications.isEmpty else { return }
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
let hasChanges = ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications)
guard hasChanges else { return }
if let firstChangeSet = notifications[0].userInfo {
let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
return reload() // The code below will crash if we try to process multiple commits at once
}
}
var sectionChanges = NSArray()
var rowChanges = NSArray()
ext.getSectionChanges(&sectionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
tableView.beginUpdates()
rowChanges.forEach { rowChange in
let rowChange = rowChange as! YapDatabaseViewRowChange
let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil
switch rowChange.type {
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
default: break
}
}
tableView.endUpdates()
// HACK: Moves can have conflicts with the other 3 types of change.
// Just batch perform all the moves separately to prevent crashing.
// Since all the changes are from the original state to the final state,
// it will still be correct if we pick the moves out.
tableView.beginUpdates()
rowChanges.forEach { rowChange in
let rowChange = rowChange as! YapDatabaseViewRowChange
let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil
switch rowChange.type {
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
default: break
}
}
tableView.endUpdates()
clearAllButton.isHidden = (messageRequestCount == 0)
emptyStateLabel.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
self?.delete(thread)
}
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
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE", comment: ""), message: nil, preferredStyle: .actionSheet)
alertVC.addAction(UIAlertAction(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON", comment: ""), style: .destructive) { _ in
// Clear the requests
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 {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
}
}
}
}
)
})
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
self.present(alertVC, animated: true, completion: nil)
}
private func delete(_ thread: TSThread) {
guard let uniqueId: String = thread.uniqueId else { return }
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
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()
}
}
}
)
})
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
self.present(alertVC, animated: true, completion: nil)
}
// MARK: - Convenience
private func thread(at index: Int) -> TSThread? {
var thread: TSThread? = nil
dbConnection.read { transaction in
let ext: YapDatabaseViewTransaction? = transaction.ext(TSThreadDatabaseViewExtensionName) as? YapDatabaseViewTransaction
thread = ext?.object(atRow: UInt(index), inSection: 0, with: self.threads) as? TSThread
}
return thread
}
private func threadViewModel(at index: Int) -> ThreadViewModel? {
guard let thread = thread(at: index), let uniqueId: String = thread.uniqueId else { return nil }
if let cachedThreadViewModel = threadViewModelCache[uniqueId] {
return cachedThreadViewModel
}
else {
var threadViewModel: ThreadViewModel? = nil
dbConnection.read { transaction in
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
}
threadViewModelCache[uniqueId] = threadViewModel
return threadViewModel
}
}
}

View File

@ -1,4 +1,5 @@
import UIKit
import SessionUIKit
final class NewConversationButtonSet : UIView {
private var isUserDragging = false
@ -8,7 +9,7 @@ final class NewConversationButtonSet : UIView {
var delegate: NewConversationButtonSetDelegate?
// MARK: Settings
private let spacing = Values.largeSpacing
private let spacing = Values.veryLargeSpacing
private let iconSize = CGFloat(24)
private let maxDragDistance = CGFloat(56)
private let dragMargin = CGFloat(16)
@ -21,6 +22,39 @@ final class NewConversationButtonSet : UIView {
private lazy var createClosedGroupButton = NewConversationButton(isMainButton: false, icon: #imageLiteral(resourceName: "Group").scaled(to: CGSize(width: iconSize, height: iconSize)))
private lazy var joinOpenGroupButton = NewConversationButton(isMainButton: false, icon: #imageLiteral(resourceName: "Globe").scaled(to: CGSize(width: iconSize, height: iconSize)))
private lazy var newDMLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_DIRECT_MESSAGE", comment: "").uppercased()
result.textColor = Colors.grey
result.textAlignment = .center
return result
}()
private lazy var createClosedGroupLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_CLOSED_GROUP", comment: "").uppercased()
result.textColor = Colors.grey
result.textAlignment = .center
return result
}()
private lazy var joinOpenGroupLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.font = UIFont.systemFont(ofSize: Values.verySmallFontSize)
result.text = NSLocalizedString("NEW_CONVERSATION_MENU_OPEN_GROUP", comment: "").uppercased()
result.textColor = Colors.grey
result.textAlignment = .center
return result
}()
// MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
@ -42,15 +76,24 @@ final class NewConversationButtonSet : UIView {
joinOpenGroupButton.accessibilityLabel = "Join open group button"
joinOpenGroupButton.isAccessibilityElement = true
let inset = (NewConversationButtonSet.expandedButtonSize - NewConversationButtonSet.collapsedButtonSize) / 2
addSubview(joinOpenGroupLabel)
addSubview(joinOpenGroupButton)
horizontalButtonConstraints[joinOpenGroupButton] = joinOpenGroupButton.pin(.left, to: .left, of: self, withInset: inset)
verticalButtonConstraints[joinOpenGroupButton] = joinOpenGroupButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
joinOpenGroupLabel.center(.horizontal, in: joinOpenGroupButton)
joinOpenGroupLabel.pin(.top, to: .bottom, of: joinOpenGroupButton, withInset: 8)
addSubview(newDMLabel)
addSubview(newDMButton)
newDMButton.center(.horizontal, in: self)
verticalButtonConstraints[newDMButton] = newDMButton.pin(.top, to: .top, of: self, withInset: inset)
newDMLabel.center(.horizontal, in: newDMButton)
newDMLabel.pin(.top, to: .bottom, of: newDMButton, withInset: 8)
addSubview(createClosedGroupLabel)
addSubview(createClosedGroupButton)
horizontalButtonConstraints[createClosedGroupButton] = createClosedGroupButton.pin(.right, to: .right, of: self, withInset: -inset)
verticalButtonConstraints[createClosedGroupButton] = createClosedGroupButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
createClosedGroupLabel.center(.horizontal, in: createClosedGroupButton)
createClosedGroupLabel.pin(.top, to: .bottom, of: createClosedGroupButton, withInset: 8)
addSubview(mainButton)
mainButton.center(.horizontal, in: self)
mainButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
@ -74,14 +117,17 @@ final class NewConversationButtonSet : UIView {
@objc private func handleCreateNewClosedGroupButtonTapped() { delegate?.createClosedGroup() }
private func expand(isUserDragging: Bool) {
let buttons = [ joinOpenGroupButton, newDMButton, createClosedGroupButton ]
let views = [ joinOpenGroupButton, joinOpenGroupLabel, newDMButton, newDMLabel, createClosedGroupButton, createClosedGroupLabel ]
UIView.animate(withDuration: 0.25, animations: {
buttons.forEach { $0.alpha = 1 }
views.forEach { $0.alpha = 1 }
let inset = (NewConversationButtonSet.expandedButtonSize - NewConversationButtonSet.collapsedButtonSize) / 2
let size = NewConversationButtonSet.collapsedButtonSize
self.joinOpenGroupButton.frame = CGRect(origin: CGPoint(x: inset, y: self.height() - size - inset), size: CGSize(width: size, height: size))
self.joinOpenGroupLabel.center = CGPoint(x: self.joinOpenGroupButton.center.x, y: self.joinOpenGroupButton.frame.maxY + 8 + (self.joinOpenGroupLabel.bounds.height / 2))
self.newDMButton.frame = CGRect(center: CGPoint(x: self.bounds.center.x, y: inset + size / 2), size: CGSize(width: size, height: size))
self.newDMLabel.center = CGPoint(x: self.newDMButton.center.x, y: self.newDMButton.frame.maxY + 8 + (self.newDMLabel.bounds.height / 2))
self.createClosedGroupButton.frame = CGRect(origin: CGPoint(x: self.width() - size - inset, y: self.height() - size - inset), size: CGSize(width: size, height: size))
self.createClosedGroupLabel.center = CGPoint(x: self.createClosedGroupButton.center.x, y: self.createClosedGroupButton.frame.maxY + 8 + (self.createClosedGroupLabel.bounds.height / 2))
}, completion: { _ in
self.isUserDragging = isUserDragging
})
@ -90,7 +136,12 @@ final class NewConversationButtonSet : UIView {
private func collapse(withAnimation isAnimated: Bool) {
isUserDragging = false
let buttons = [ joinOpenGroupButton, newDMButton, createClosedGroupButton ]
let labels = [ joinOpenGroupLabel, newDMLabel, createClosedGroupLabel ]
UIView.animate(withDuration: isAnimated ? 0.25 : 0) {
labels.forEach { label in
label.alpha = 0
label.center = self.mainButton.center
}
buttons.forEach { button in
button.alpha = 0
let size = NewConversationButtonSet.collapsedButtonSize

View File

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

View File

@ -3,7 +3,6 @@
//
#import "MediaDetailViewController.h"
#import "AttachmentSharing.h"
#import "ConversationViewItem.h"
#import "Session-Swift.h"
#import "TSAttachmentStream.h"

View File

@ -380,8 +380,20 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
}
let attachmentStream = currentViewController.galleryItem.attachmentStream
AttachmentSharing.showShareUI(forAttachment: attachmentStream) { activityType in
let shareVC = UIActivityViewController(activityItems: [ attachmentStream.originalMediaURL! ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
shareVC.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in
if let activityError = activityError {
SNLog("Failed to share with activityError: \(activityError)")
} else if completed {
SNLog("Did share with activityType: \(activityType.debugDescription)")
}
guard let activityType = activityType, activityType == .saveToCameraRoll,
let tsMessage = currentViewController.galleryItem.message as? TSIncomingMessage, let thread = tsMessage.thread as? TSContactThread else { return }
let message = DataExtractionNotification()
@ -389,8 +401,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
Storage.write { transaction in
MessageSender.send(message, in: thread, using: transaction)
}
}
self.present(shareVC, animated: true, completion: nil)
}
@objc

View File

@ -151,12 +151,19 @@ extension AppDelegate {
let job = MessageSendJob(message: configurationMessage, destination: destination)
JobQueue.shared.add(job, using: transaction)
}
userDefaults[.lastConfigurationSync] = Date()
// Only update the 'lastConfigurationSync' timestamp if we have done the first sync (Don't want
// a new device config sync to override config syncs from other devices)
if userDefaults[.hasSyncedInitialConfiguration] {
userDefaults[.lastConfigurationSync] = Date()
}
}
func forceSyncConfigurationNowIfNeeded() -> Promise<Void> {
guard Storage.shared.getUser()?.name != nil,
let configurationMessage = ConfigurationMessage.getCurrent() else { return Promise.value(()) }
func forceSyncConfigurationNowIfNeeded(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Void> {
guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else {
return Promise.value(())
}
let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey())
let (promise, seal) = Promise<Void>.pending()
Storage.writeSync { transaction in

View File

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

View File

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

View File

@ -107,6 +107,11 @@
<string>SpaceMono-Bold.ttf</string>
<string>SpaceMono-Regular.ttf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>UIApplicationShortcutItems</key>
<array>
<dict>

View File

@ -40,7 +40,6 @@
#import <SignalCoreKit/OWSAsserts.h>
#import <SignalCoreKit/OWSLogs.h>
#import <SignalCoreKit/Threading.h>
#import <SignalUtilitiesKit/AttachmentSharing.h>
#import <SignalUtilitiesKit/ContactTableViewCell.h>
#import <SessionMessagingKit/Environment.h>
#import <SessionMessagingKit/OWSAudioPlayer.h>

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Blockieren";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blockieren?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@ freigeben?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Freigeben";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ wurde blockiert.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Benutzer blockiert";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ wurde freigegeben.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Vorhandene Mitglieder können dich jetzt wieder zur Gruppe hinzufügen.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blockierte Benutzer können dich nicht mehr anrufen oder dir Nachrichten senden.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Block";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
/* Label for generic done button. */
@ -626,3 +632,18 @@
"SEARCH_SECTION_MESSAGES" = "Messages";
"SEARCH_SECTION_RECENT" = "Recent";
"RECENT_SEARCH_LAST_MESSAGE_DATETIME" = "last message: %@";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Bloquear";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "¿Bloquear %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "¿Desbloquear %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ ha sido bloqueado.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Usuario Bloqueado";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ ha sido desbloqueado.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Los miembros pueden añadirte de nuevo al grupo.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Los contactos bloqueados no podrán llamarte ni enviarte mensajes.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "مسدود کردن";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ مسدود شود؟";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@از حالت مسدود خارج شود؟";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "رفع مسدودی";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "کاربر مسدود شده است";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ از حالت مسدودی خارج شد.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "اعضای موجود می‌توانند شما را دوباره به گروه اضافه کنند.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "کاربری که مسدود شده است، امکان تماس یا ارسال پیام به شما را ندارد.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Estä";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Estä %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Poista esto yhteystiedolta %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Poista esto";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ on estetty.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Käyttäjä estetty";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ esto on poistettu.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Ryhmän jäsenet voivat nyt lisätä sinut takaisin ryhmään.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Estetyt käyttäjät eivät voi soittaa sinulle tai lähettää sinulle viestejä.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Bloquer";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquer %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Débloquer %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Débloquer";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ a été bloqué.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Utilisateur Bloqué";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ a été débloqué.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Les membres actuels peuvent désormais vous ajouter au groupe de nouveau.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Les utilisateurs bloqués ne pourront ni vous appeler ni vous envoyer des messages.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "ब्लॉक";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ को ब्लॉक करें?";
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "अनब्लॉक करें";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ को ब्लॉक कर दिया गया है";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "यूजर ब्लॉक किया हुआ है";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "अवरुद्ध उपयोगकर्ता आपको कॉल नहीं कर पाएंगे या आपको संदेश नहीं भेज पाएंगे।";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Blokiraj";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokiraj %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Deblokiraj %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Deblokiraj";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ je blokiran.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Korisnik blokiran";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ je deblokiran.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Sadašnji članovi sada vas mogu dodati u grupu.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blokirani korisnici neće vas moći nazvati niti poslati poruke.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Blokir";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokir %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Buka blokir %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Buka blokir";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ telah diblokir.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Pengguna diblokir";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ telah dibuka blokirnya";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Pengguna terblokir tidak bisa menghubungi atau mengirimkan pesan kepada Anda.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Blocca";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloccare %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Sbloccare %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Sblocca";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ è stato bloccato.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Utente bloccato";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ buvo atblokuota(-as).";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Dabar, esami dalyviai gali ir vėl pridėti jus į grupę.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Gli utenti bloccati non potranno chiamarti o inviarti messaggi.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "ブロックする";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ をブロックしますか?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@のブロックを解除しますか?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "ブロックを解除する";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@はブロックされました。";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "ユーザがブロックされました";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ のブロックは解除されています。";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "既存のメンバーは、あなたをグループに再加入させることができます。";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "ブロックされたユーザは、あなたにメッセージや通話を発信することができなくなります。";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Blokkeren";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blokkeren?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "%@ deblokkeren?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Deblokkeren";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ is geblokkeerd.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Gebruiker Geblokkeerd";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ is gedeblokkeerd.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Bestaande leden kunnen je nu opnieuw toevoegen aan de groep.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Geblokkeerde gebruikers zijn niet in staat om u te bellen of berichten te sturen.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Zablokuj";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Zablokować %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Odblokować %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokuj";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ został zablokowany.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Użytkownik zablokowany";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "Odblokowano %@.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Istniejący członkowie mogą teraz ponownie dodać Cię do grupy.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Zablokowani użytkownicy nie będą mogli do Ciebie dzwonić ani wysyłać Ci wiadomości.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Bloquear";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquear %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Desbloquear %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ foi bloqueado.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Usuário Bloqueado";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "Você desbloqueou %@.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Membros existentes podem te adicionar ap grupo novamente.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Você não receberá mais ligações e mensagens de quem bloquear.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Заблокировать";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Заблокировать %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Разблокировать %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Разблокировать";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "Пользователь %@ был заблокирован.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Пользователь заблокирован";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ был(-a) разблокирован(-a).";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Теперь участники группы могут снова добавить вас в группу.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Заблокированные пользователи не смогут звонить или отправлять сообщения Вам.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Block";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "Oseba %@ je bila odblokirana";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Blokovať";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokovať %@?";
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokovať";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ bol/a zablokovaný/á.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Používateľ blokovaný";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blokovaný používateľ vám nebude mocť volať ani posielať správy.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Blockera";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blockera %@?";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Avblockera %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Avblockera";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ har blockerats.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Användare blockerad";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@har blivit avblockerad.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Befintliga medlemmar kan nu lägga dig till gruppen igen.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blockerade användare kommer inte att kunna ringa dig eller skicka meddelanden.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "บล็อก";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "บล็อก %@ ไหม";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "เลิกบล็อก %@ หรือไม่";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "เลิกบล็อก";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ บล็อกแล้ว";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "คนบล็อกแล้ว";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "คนที่บล็อกแล้วส่งข้อความและโทรมาหาไม่ได้";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "Block";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "封鎖";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "封鎖 %@";
/* A format for the 'unblock user' action sheet title. Embeds {{the unblocked user's name or phone number}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "Unblock %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "解除封鎖";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "已封鎖 %@。";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "使用者已封鎖";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "%@ has been unblocked.";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "Existing members can now add you to the group again.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "被您封鎖的使用者將無法傳送訊息與撥打電話給您";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -94,12 +94,18 @@
"BLOCK_LIST_BLOCK_BUTTON" = "加入黑名单";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "屏蔽 %@";
/* A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}. */
"BLOCK_LIST_UNBLOCK_TITLE_FORMAT" = "从黑名单中移除 %@ 吗?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "从黑名单中移除";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "已屏蔽 %@。";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "用户已屏蔽";
/* Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT" = "已取消屏蔽 %@。";
/* Alert body after unblocking a group. */
"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY" = "现有成员可再次将您加入群组。";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "被屏蔽的用户将无法向您发起通话,或发送消息。";
/* Label for generic done button. */
@ -600,3 +606,18 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "No pending message requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
"MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE" = "An error occurred when trying to accept this message request";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
"NEW_CONVERSATION_MENU_CLOSED_GROUP" = "Closed Group";

View File

@ -4,6 +4,8 @@
import Foundation
import PromiseKit
import SessionMessagingKit
import SignalUtilitiesKit
/// There are two primary components in our system notification integration:
///
@ -88,7 +90,7 @@ let kNotificationDelayForBackgroumdPoll: TimeInterval = 5
let kAudioNotificationsThrottleCount = 2
let kAudioNotificationsThrottleInterval: TimeInterval = 5
protocol NotificationPresenterAdaptee: class {
protocol NotificationPresenterAdaptee: AnyObject {
func registerNotificationSettings() -> Promise<Void>
@ -157,10 +159,35 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
}
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
guard !thread.isMuted else { return }
guard let threadId = thread.uniqueId else { return }
// If the thread is a message request and the user hasn't hidden message requests then we need
// to check if this is the only message request thread (group threads can't be message requests
// so just ignore those and if the user has hidden message requests then we want to show the
// notification regardless of how many message requests there are)
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
let dbConnection: YapDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
dbConnection.objectCacheLimit = 2
dbConnection.beginLongLivedReadTransaction() // 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)
let threads: YapDatabaseViewMappings = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName)
dbConnection.read { transaction in
threads.update(with: transaction) // Perform the initial update
}
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
dbConnection.endLongLivedReadTransaction()
// Allow this to show a notification if there are no message requests (ie. this is the first one)
guard numMessageRequests == 0 else { return }
}
else if thread.isMessageRequest() && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
// If there are other interactions on this thread already then don't show the notification
if thread.numberOfInteractions() > 1 { return }
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
}
let identifier: String = incomingMessage.notificationIdentifier ?? UUID().uuidString
let isBackgroudPoll = identifier == threadId
@ -185,36 +212,44 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
let senderName = Storage.shared.getContact(with: incomingMessage.authorId, using: transaction)?.displayName(for: context) ?? incomingMessage.authorId
let notificationTitle: String?
let previewType = preferences.notificationPreviewType(with: transaction)
switch previewType {
case .noNameNoPreview:
notificationTitle = "Session"
case .nameNoPreview, .namePreview:
switch thread {
case is TSContactThread:
notificationTitle = senderName
case is TSGroupThread:
var groupName = thread.name()
if groupName.count < 1 {
groupName = MessageStrings.newGroupDefaultTitle
}
notificationTitle = isBackgroudPoll ? groupName : String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
default:
owsFailDebug("unexpected thread: \(thread)")
return
}
default:
notificationTitle = "Session"
}
var notificationBody: String?
let previewType = preferences.notificationPreviewType(with: transaction)
switch previewType {
case .noNameNoPreview, .nameNoPreview:
notificationBody = NotificationStrings.incomingMessageBody
case .namePreview:
notificationBody = messageText
default:
notificationBody = NotificationStrings.incomingMessageBody
case .noNameNoPreview:
notificationTitle = "Session"
case .nameNoPreview, .namePreview:
switch thread {
case is TSContactThread:
notificationTitle = (thread.isMessageRequest() ? "Session" : senderName)
case is TSGroupThread:
var groupName = thread.name()
if groupName.count < 1 {
groupName = MessageStrings.newGroupDefaultTitle
}
notificationTitle = isBackgroudPoll ? groupName : String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
default:
owsFailDebug("unexpected thread: \(thread)")
return
}
default:
notificationTitle = "Session"
}
switch previewType {
case .noNameNoPreview, .nameNoPreview: notificationBody = NotificationStrings.incomingMessageBody
case .namePreview: notificationBody = messageText
default: notificationBody = NotificationStrings.incomingMessageBody
}
// If it's a message request then overwrite the body to be something generic (only show a notification
// when receiving a new message request if there aren't any others or the user had hidden them)
if thread.isMessageRequest() {
notificationBody = NSLocalizedString("MESSAGE_REQUESTS_NOTIFICATION", comment: "")
}
assert((notificationBody ?? notificationTitle) != nil)
@ -230,12 +265,15 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
DispatchQueue.main.async {
notificationBody = MentionUtilities.highlightMentions(in: notificationBody!, threadID: thread.uniqueId!)
let sound = self.requestSound(thread: thread)
self.adaptee.notify(category: category,
title: notificationTitle,
body: notificationBody ?? "",
userInfo: userInfo,
sound: sound,
replacingIdentifier: identifier)
self.adaptee.notify(
category: category,
title: notificationTitle,
body: notificationBody ?? "",
userInfo: userInfo,
sound: sound,
replacingIdentifier: identifier
)
}
}

View File

@ -53,11 +53,11 @@ public enum PushRegistrationError: Error {
Logger.info("")
return firstly { () -> Promise<Void> in
return self.registerUserNotificationSettings()
self.registerUserNotificationSettings()
}.then { (_) -> Promise<(pushToken: String, voipToken: String)> in
guard !Platform.isSimulator else {
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")
}
#if targetEnvironment(simulator)
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")
#endif
return self.registerForVanillaPushToken().then { vanillaPushToken -> Promise<(pushToken: String, voipToken: String)> in
self.registerForVoipPushToken().map { voipPushToken in

View File

@ -91,7 +91,7 @@ final class PNModeVC : BaseVC, OptionViewDelegate {
let title = NSLocalizedString("vc_pn_mode_no_option_picked_modal_title", comment: "")
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
return present(alert, animated: true, completion: nil)
return presentAlert(alert)
}
UserDefaults.standard[.isUsingFullAPNs] = (selectedOptionView == apnsOptionView)
TSAccountManager.sharedInstance().didRegister()

View File

@ -61,7 +61,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
tabBar.pin(.leading, to: .leading, of: view)
let tabBarInset: CGFloat
if #available(iOS 13, *) {
tabBarInset = navigationBar.height()
tabBarInset = UIDevice.current.isIPad ? navigationBar.height() + 20 : navigationBar.height()
} else {
tabBarInset = 0
}

View File

@ -210,6 +210,12 @@ private final class ViewMyQRCodeVC : UIViewController {
@objc private func shareQRCode() {
let qrCode = QRCode.generate(for: getUserHexEncodedPublicKey(), hasBackground: true)
let shareVC = UIActivityViewController(activityItems: [ qrCode ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
qrCodeVC.navigationController!.present(shareVC, animated: true, completion: nil)
}
}

View File

@ -139,9 +139,6 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("vc_settings_title", comment: ""))
// Navigation bar buttons
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
backButton.tintColor = Colors.text
navigationItem.backBarButtonItem = backButton
updateNavigationBarButtons()
// Profile picture view
let profilePictureTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditProfilePictureUI))
@ -254,8 +251,6 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
pathStatusView.pin(.leading, to: .trailing, of: pathButton.titleLabel!, withInset: Values.smallSpacing)
pathStatusView.autoVCenterInSuperview()
pathButton.titleEdgeInsets = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: Values.smallSpacing)
return [
getSeparator(),
pathButton,
@ -264,6 +259,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)),
@ -392,7 +389,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
self?.presentAlert(alert)
}
}
}, requiresSync: true)
@ -491,6 +488,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
@objc private func sharePublicKey() {
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
navigationController!.present(shareVC, animated: true, completion: nil)
}
@ -509,6 +512,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
@ -526,6 +534,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
@objc private func sendInvitation() {
let invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \(getUserHexEncodedPublicKey()) !"
let shareVC = UIActivityViewController(activityItems: [ invitation ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
navigationController!.present(shareVC, animated: true, completion: nil)
}

View File

@ -64,7 +64,16 @@ final class ShareLogsModal : Modal {
if let latestLogFilePath = logFilePaths.first {
let latestLogFileURL = URL(fileURLWithPath: latestLogFilePath)
self.dismiss(animated: true, completion: {
AttachmentSharing.showShareUI(for: latestLogFileURL)
if let vc = CurrentAppContext().frontmostViewController() {
let shareVC = UIActivityViewController(activityItems: [ latestLogFileURL ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = vc.view
shareVC.popoverPresentationController?.sourceRect = vc.view.bounds
}
vc.present(shareVC, animated: true, completion: nil)
}
})
}
}

View File

@ -46,6 +46,7 @@ class BaseVC : UIViewController {
internal func setUpNavBarStyle() {
guard let navigationBar = navigationController?.navigationBar else { return }
if #available(iOS 15.0, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
@ -59,6 +60,11 @@ class BaseVC : UIViewController {
navigationBar.isTranslucent = false
navigationBar.barTintColor = Colors.navigationBarBackground
}
// Back button (to appear on pushed screen)
let backButton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
backButton.tintColor = Colors.text
navigationItem.backBarButtonItem = backButton
}
internal func setNavBarTitle(_ title: String, customFontSize: CGFloat? = nil) {

View File

@ -118,7 +118,8 @@ final class ConversationCell : UITableViewCell {
}()
// MARK: Settings
private static let unreadCountViewSize: CGFloat = 20
public static let unreadCountViewSize: CGFloat = 20
private static let statusIndicatorSize: CGFloat = 14
// MARK: Initialization
@ -172,6 +173,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
@ -356,15 +358,20 @@ final class ConversationCell : UITableViewCell {
if threadViewModel.isGroupThread {
if threadViewModel.name.isEmpty {
return "Unknown Group"
} else {
}
else {
return threadViewModel.name
}
} else {
}
else {
if threadViewModel.threadRecord.isNoteToSelf() {
return NSLocalizedString("NOTE_TO_SELF", comment: "")
} else {
let hexEncodedPublicKey = threadViewModel.contactSessionID!
return Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey
}
else {
let hexEncodedPublicKey: String = threadViewModel.contactSessionID!
let displayName: String = (Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey)
let middleTruncatedHexKey: String = "\(hexEncodedPublicKey.prefix(4))...\(hexEncodedPublicKey.suffix(4))"
return (displayName == hexEncodedPublicKey ? middleTruncatedHexKey : displayName)
}
}
}

View File

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

View File

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

View File

@ -1,23 +0,0 @@
import Sodium
enum KeyPairUtilities {
static func generate(from seed: Data) -> (ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
assert(seed.count == 16)
let padding = Data(repeating: 0, count: 16)
let ed25519KeyPair = Sodium().sign.keyPair(seed: (seed + padding).bytes)!
let x25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: ed25519KeyPair.publicKey)!
let x25519SecretKey = Sodium().sign.toX25519(ed25519SecretKey: ed25519KeyPair.secretKey)!
let x25519KeyPair = ECKeyPair(publicKey: Data(x25519PublicKey), privateKey: Data(x25519SecretKey))!
return (ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
}
static func store(seed: Data, ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
let dbConnection = OWSIdentityManager.shared().dbConnection
let collection = OWSPrimaryStorageIdentityKeyStoreCollection
dbConnection.setObject(seed.toHexString(), forKey: LKSeedKey, inCollection: collection)
dbConnection.setObject(ed25519KeyPair.secretKey.toHexString(), forKey: LKED25519SecretKey, inCollection: collection)
dbConnection.setObject(ed25519KeyPair.publicKey.toHexString(), forKey: LKED25519PublicKey, inCollection: collection)
dbConnection.setObject(x25519KeyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: collection)
}
}

View File

@ -0,0 +1,255 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionMessagingKit
enum MockDataGenerator {
// Note: This was taken from TensorFlow's Random (https://github.com/apple/swift/blob/bc8f9e61d333b8f7a625f74d48ef0b554726e349/stdlib/public/TensorFlow/Random.swift)
// the complex approach is needed due to an issue with Swift's randomElement(using:)
// generation (see https://stackoverflow.com/a/64897775 for more info)
struct ARC4RandomNumberGenerator: RandomNumberGenerator {
var state: [UInt8] = Array(0...255)
var iPos: UInt8 = 0
var jPos: UInt8 = 0
init<T: BinaryInteger>(seed: T) {
self.init(
seed: (0..<(UInt64.bitWidth / UInt64.bitWidth)).map { index in
UInt8(truncatingIfNeeded: seed >> (UInt8.bitWidth * index))
}
)
}
init(seed: [UInt8]) {
precondition(seed.count > 0, "Length of seed must be positive")
precondition(seed.count <= 256, "Length of seed must be at most 256")
// Note: Have to use a for loop instead of a 'forEach' otherwise
// it doesn't work properly (not sure why...)
var j: UInt8 = 0
for i: UInt8 in 0...255 {
j &+= S(i) &+ seed[Int(i) % seed.count]
swapAt(i, j)
}
}
/// Produce the next random UInt64 from the stream, and advance the internal state
mutating func next() -> UInt64 {
// Note: Have to use a for loop instead of a 'forEach' otherwise
// it doesn't work properly (not sure why...)
var result: UInt64 = 0
for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
result <<= UInt8.bitWidth
result += UInt64(nextByte())
}
return result
}
/// Helper to access the state
private func S(_ index: UInt8) -> UInt8 {
return state[Int(index)]
}
/// Helper to swap elements of the state
private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
state.swapAt(Int(i), Int(j))
}
/// Generates the next byte in the keystream.
private mutating func nextByte() -> UInt8 {
iPos &+= 1
jPos &+= S(iPos)
swapAt(iPos, jPos)
return S(S(iPos) &+ S(jPos))
}
}
static func generateMockData() {
// Don't re-generate the mock data if it already exists
var existingMockDataThread: TSContactThread?
Storage.read { transaction in
existingMockDataThread = TSContactThread.getWithContactSessionID("MockDatabaseThread", transaction: transaction)
}
guard existingMockDataThread == nil else { return }
/// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will also take a long time):
/// Generating the threads & content - ~3s per 100
/// Writing to the database - ~10s per 1000
/// Updating the UI - ~10s per 1000
let dmThreadCount: Int = 100
let closedGroupThreadCount: Int = 0
let openGroupThreadCount: Int = 0
let maxMessagesPerThread: Int = 50
let dmRandomSeed: Int = 1111
let cgRandomSeed: Int = 2222
let ogRandomSeed: Int = 3333
// FIXME: Make sure this data doesn't go off device somehow?
Storage.shared.write { anyTransaction in
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return }
// First create the thread used to indicate that the mock data has been generated
_ = TSContactThread.getOrCreateThread(withContactSessionID: "MockDatabaseThread", transaction: transaction)
// Multiple spaces to make it look more like words
let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) }
let timestampNow: TimeInterval = Date().timeIntervalSince1970
let userSessionId: String = getUserHexEncodedPublicKey()
// MARK: - -- DM Thread
var dmThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: dmRandomSeed)
(0..<dmThreadCount).forEach { threadIndex in
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) })
let randomSessionId: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &dmThreadRandomGenerator) ?? 0)
// Generate the thread
let thread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: randomSessionId, transaction: transaction)
thread.shouldBeVisible = true
// Generate the contact
let contact = Contact(sessionID: randomSessionId)
contact.name = (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
.joined()
contact.isApproved = (!isMessageRequest || Bool.random(using: &dmThreadRandomGenerator))
contact.didApproveMe = (!isMessageRequest && Bool.random(using: &dmThreadRandomGenerator))
Storage.shared.setContact(contact, using: transaction)
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
(0..<numMessages).forEach { index in
let isIncoming: Bool = (
Bool.random(using: &dmThreadRandomGenerator) &&
(!isMessageRequest || contact.isApproved)
)
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
let message: VisibleMessage = VisibleMessage()
message.sender = (isIncoming ? randomSessionId : userSessionId)
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
message.text = (0..<messageLength)
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
.joined()
if isIncoming {
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
tsMessage.save(with: transaction)
}
else {
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
tsMessage.save(with: transaction)
}
}
// Save the thread
thread.save(with: transaction)
}
// MARK: - -- Closed Group
var cgThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: cgRandomSeed)
(0..<closedGroupThreadCount).forEach { threadIndex in
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
let groupName: String = (0..<groupNameLength)
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
.joined()
let numGroupMembers: Int = ((0..<5).randomElement(using: &cgThreadRandomGenerator) ?? 0)
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &cgThreadRandomGenerator) ?? 0)
// Generate the Contacts in the group
var members: [String] = [userSessionId]
(0..<numGroupMembers).forEach { _ in
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
let randomSessionId: String = KeyPairUtilities.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
let contact = Contact(sessionID: randomSessionId)
contact.name = (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
.joined()
Storage.shared.setContact(contact, using: transaction)
members.append(randomSessionId)
}
let groupId: Data = LKGroupUtilities.getEncodedClosedGroupIDAsData(randomGroupPublicKey)
let group: TSGroupModel = TSGroupModel(
title: groupName,
memberIds: members,
image: nil,
groupId: groupId,
groupType: .closedGroup,
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
)
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
// Add the group to the user's set of public keys to poll for and store the key pair
let encryptionKeyPair = Curve25519.generateKeyPair()
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: randomGroupPublicKey, using: transaction)
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
(0..<numMessages).forEach { index in
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
let message: VisibleMessage = VisibleMessage()
message.sender = (members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId)
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
message.text = (0..<messageLength)
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
.joined()
if message.sender != userSessionId {
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
tsMessage.save(with: transaction)
}
else {
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
tsMessage.save(with: transaction)
}
}
// Save the thread
thread.save(with: transaction)
}
// MARK: - --Open Group
var ogThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: ogRandomSeed)
(0..<openGroupThreadCount).forEach { threadIndex in
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let serverName: String = (0..<serverNameLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
let roomName: String = (0..<roomNameLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
// Create the open group model and the thread
let openGroup: OpenGroupV2 = OpenGroupV2(server: serverName, room: roomName, name: roomName, publicKey: randomGroupPublicKey, imageID: nil)
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [])
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
Storage.shared.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
}
}
}
}

View File

@ -1,14 +0,0 @@
// Created by Michael Kirk on 12/23/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
import Foundation
struct Platform {
static let isSimulator: Bool = {
var isSim = false
#if arch(i386) || arch(x86_64)
isSim = true
#endif
return isSim
}()
}

View File

@ -1,16 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (CameraPermissions)
- (void)ows_askForCameraPermissions:(void (^)(void))permissionsGrantedCallback;
- (void)ows_askForCameraPermissions:(void (^)(void))permissionsGrantedCallback
failureCallback:(nullable void (^)(void))failureCallback;
@end
NS_ASSUME_NONNULL_END

View File

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

View File

@ -55,10 +55,16 @@ extension Storage {
@objc public func getAllContacts() -> Set<Contact> {
var result: Set<Contact> = []
Storage.read { transaction in
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in
guard let contact = object as? Contact else { return }
result.insert(contact)
}
result = self.getAllContacts(with: transaction)
}
return result
}
@objc public func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact> {
var result: Set<Contact> = []
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in
guard let contact = object as? Contact else { return }
result.insert(contact)
}
return result
}

View File

@ -8,11 +8,14 @@
NS_ASSUME_NONNULL_BEGIN
extern NSString *const TSInboxGroup;
extern NSString *const TSMessageRequestGroup;
extern NSString *const TSArchiveGroup;
extern NSString *const TSShareExtensionGroup;
extern NSString *const TSUnreadIncomingMessagesGroup;
extern NSString *const TSSecondaryDevicesGroup;
extern NSString *const TSThreadDatabaseViewExtensionName;
extern NSString *const TSThreadShareExtensionDatabaseViewExtensionName;
extern NSString *const TSMessageDatabaseViewExtensionName;
extern NSString *const TSMessageDatabaseViewExtensionName_Legacy;

View File

@ -9,14 +9,20 @@
#import "TSIncomingMessage.h"
#import "TSOutgoingMessage.h"
#import "TSThread.h"
#import "OWSBlockingManager.h"
#import <YapDatabase/YapDatabaseAutoView.h>
#import <YapDatabase/YapDatabaseCrossProcessNotification.h>
#import <YapDatabase/YapDatabaseViewTypes.h>
#import <SessionUtilitiesKit/AppContext.h>
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const TSInboxGroup = @"TSInboxGroup";
NSString *const TSMessageRequestGroup = @"TSMessageRequestGroup";
NSString *const TSArchiveGroup = @"TSArchiveGroup";
NSString *const TSShareExtensionGroup = @"TSShareExtensionGroup";
NSString *const TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup";
NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup";
@ -25,6 +31,8 @@ NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup";
// -> TSThreadDatabaseViewExtensionName2 to work around https://github.com/yapstudios/YapDatabase/issues/324
NSString *const TSThreadDatabaseViewExtensionName = @"TSThreadDatabaseViewExtensionName2";
NSString *const TSThreadShareExtensionDatabaseViewExtensionName = @"TSThreadShareExtensionDatabaseViewExtensionName";
// We sort interactions by a monotonically increasing counter.
//
// Previously we sorted the interactions database by local timestamp, which was problematic if the local clock changed.
@ -234,7 +242,15 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
}
TSThread *thread = (TSThread *)object;
if (thread.shouldBeVisible) {
if ([thread isMessageRequestUsingTransaction:transaction]) {
// 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];
@ -258,6 +274,53 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
[[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"4" options:options];
[storage asyncRegisterExtension:databaseView withName:TSThreadDatabaseViewExtensionName];
YapDatabaseView *shareExtensionThreadView = [storage registeredExtension:TSThreadShareExtensionDatabaseViewExtensionName];
if (shareExtensionThreadView) {
return;
}
YapDatabaseViewGrouping *shareExtensionViewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *(
YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) {
if (![object isKindOfClass:[TSThread class]]) {
return nil;
}
TSThread *thread = (TSThread *)object;
if (thread.isMessageRequest) {
return nil;
}
else {
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
NSUInteger threadMessageCount = [viewTransaction numberOfItemsInGroup:thread.uniqueId];
if (threadMessageCount < 1) {
return nil;
}
if (!thread.isGroupThread) {
TSContactThread *contactThead = (TSContactThread *)thread;
SNContact *contact = [LKStorage.shared getContactWithSessionID:[contactThead contactSessionID]];
if (contact == nil || !contact.didApproveMe) {
return nil;
}
}
}
return TSShareExtensionGroup;
}];
YapDatabaseViewSorting *shareExtensionViewSorting = [self threadSorting];
YapDatabaseViewOptions *shareExtensionOptions = [[YapDatabaseViewOptions alloc] init];
shareExtensionOptions.isPersistent = YES;
shareExtensionOptions.allowedCollections =
[[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSThread collection]]];
YapDatabaseView *shareExtensionDatabaseView =
[[YapDatabaseAutoView alloc] initWithGrouping:shareExtensionViewGrouping sorting:shareExtensionViewSorting versionTag:@"1" options:shareExtensionOptions];
[storage asyncRegisterExtension:shareExtensionDatabaseView withName:TSThreadShareExtensionDatabaseViewExtensionName];
}
+ (YapDatabaseViewSorting *)threadSorting {

View File

@ -0,0 +1,111 @@
extension ConfigurationMessage {
public static func getCurrent(with transaction: YapDatabaseReadWriteTransaction? = nil) -> 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
let populateDataClosure: (YapDatabaseReadTransaction) -> () = { 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
}
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
var truncatedContacts = storage.getAllContacts(with: transaction)
if truncatedContacts.count > 200 {
truncatedContacts = Set(Array(truncatedContacts)[0..<200])
}
truncatedContacts.forEach { contact in
let publicKey = contact.sessionID
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
// Want to sync contacts for visible threads and blocked contacts between devices
guard
publicKey != currentUserPublicKey && (
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true ||
SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey)
)
else {
return
}
// Can just default the 'hasX' values to true as they will be set to this
// when converting to proto anyway
let profilePictureURL = contact.profilePictureURL
let profileKey = contact.profileEncryptionKey?.keyData
let contact = ConfigurationMessage.Contact(
publicKey: publicKey,
displayName: (contact.name ?? publicKey),
profilePictureURL: profilePictureURL,
profileKey: profileKey,
hasIsApproved: true,
isApproved: contact.isApproved,
hasIsBlocked: true,
isBlocked: contact.isBlocked,
hasDidApproveMe: true,
didApproveMe: contact.didApproveMe
)
contacts.insert(contact)
contactCount += 1
}
}
// If we are provided with a transaction then read the data based on the state of the database
// from within the transaction rather than the state in disk
if let transaction: YapDatabaseReadWriteTransaction = transaction {
populateDataClosure(transaction)
}
else {
Storage.read { transaction in populateDataClosure(transaction) }
}
return ConfigurationMessage(
displayName: displayName,
profilePictureURL: profilePictureURL,
profileKey: profileKey,
closedGroups: closedGroups,
openGroups: openGroups,
contacts: contacts
)
}
}

View File

@ -193,14 +193,38 @@ extension ConfigurationMessage {
public var displayName: String?
public var profilePictureURL: String?
public var profileKey: Data?
public var hasIsApproved: Bool
public var isApproved: Bool
public var hasIsBlocked: Bool
public var isBlocked: Bool
public var hasDidApproveMe: 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?,
hasIsApproved: Bool,
isApproved: Bool,
hasIsBlocked: Bool,
isBlocked: Bool,
hasDidApproveMe: Bool,
didApproveMe: Bool
) {
self.publicKey = publicKey
self.displayName = displayName
self.profilePictureURL = profilePictureURL
self.profileKey = profileKey
self.hasIsApproved = hasIsApproved
self.isApproved = isApproved
self.hasIsBlocked = hasIsBlocked
self.isBlocked = isBlocked
self.hasDidApproveMe = hasDidApproveMe
self.didApproveMe = didApproveMe
}
public required init?(coder: NSCoder) {
@ -210,6 +234,12 @@ extension ConfigurationMessage {
self.displayName = displayName
self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String?
self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data?
self.hasIsApproved = (coder.decodeObject(forKey: "hasIsApproved") as? Bool ?? false)
self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false)
self.hasIsBlocked = (coder.decodeObject(forKey: "hasIsBlocked") as? Bool ?? false)
self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false)
self.hasDidApproveMe = (coder.decodeObject(forKey: "hasDidApproveMe") as? Bool ?? false)
self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false)
}
public func encode(with coder: NSCoder) {
@ -217,14 +247,28 @@ extension ConfigurationMessage {
coder.encode(displayName, forKey: "displayName")
coder.encode(profilePictureURL, forKey: "profilePictureURL")
coder.encode(profileKey, forKey: "profileKey")
coder.encode(hasIsApproved, forKey: "hasIsApproved")
coder.encode(isApproved, forKey: "isApproved")
coder.encode(hasIsBlocked, forKey: "hasIsBlocked")
coder.encode(isBlocked, forKey: "isBlocked")
coder.encode(hasDidApproveMe, forKey: "hasDidApproveMe")
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,
hasIsApproved: proto.hasIsApproved,
isApproved: proto.isApproved,
hasIsBlocked: proto.hasIsBlocked,
isBlocked: proto.isBlocked,
hasDidApproveMe: proto.hasDidApproveMe,
didApproveMe: proto.didApproveMe
)
guard result.isValid else { return nil }
return result
}
@ -235,6 +279,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) }
if hasIsApproved { result.setIsApproved(isApproved) }
if hasIsBlocked { result.setIsBlocked(isBlocked) }
if hasDidApproveMe { result.setDidApproveMe(didApproveMe) }
do {
return try result.build()
} catch {

View File

@ -0,0 +1,63 @@
import SessionUtilitiesKit
@objc(SNMessageRequestResponse)
public final class MessageRequestResponse: ControlMessage {
public var isApproved: Bool
// MARK: - Initialization
public init(isApproved: Bool) {
self.isApproved = isApproved
super.init()
}
// MARK: - Coding
public required init?(coder: NSCoder) {
guard let isApproved: Bool = coder.decodeObject(forKey: "isApproved") as? Bool else { return nil }
self.isApproved = isApproved
super.init(coder: coder)
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
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 isApproved = messageRequestResponseProto.isApproved
return MessageRequestResponse(isApproved: isApproved)
}
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
let messageRequestResponseProto = SNProtoMessageRequestResponse.builder(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(
isApproved: \(isApproved)
)
"""
}
}

View File

@ -140,16 +140,16 @@ NS_ASSUME_NONNULL_BEGIN
return YES;
}
- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt
- (void)markAsReadNowWithTrySendReadReceipt:(BOOL)trySendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction;
{
[self markAsReadAtTimestamp:[NSDate millisecondTimestamp]
sendReadReceipt:sendReadReceipt
trySendReadReceipt:trySendReadReceipt
transaction:transaction];
}
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt
trySendReadReceipt:(BOOL)trySendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction;
{
if (_read && readTimestamp >= self.expireStartedAt) {
@ -174,7 +174,7 @@ NS_ASSUME_NONNULL_BEGIN
expirationStartedAt:readTimestamp
transaction:transaction];
if (sendReadReceipt) {
if (trySendReadReceipt) {
[OWSReadReceiptManager.sharedManager messageWasReadLocally:self];
}
}

View File

@ -16,7 +16,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) {
TSInfoMessageTypeDisappearingMessagesUpdate,
TSInfoMessageTypeScreenshotNotification,
TSInfoMessageTypeMediaSavedNotification,
TSInfoMessageTypeCall
TSInfoMessageTypeCall,
TSInfoMessageTypeMessageRequestAccepted
};
typedef NS_ENUM(NSInteger, TSInfoMessageCallState) {

View File

@ -141,6 +141,8 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
return _customMessage != nil ? _customMessage : NSLocalizedString(@"GROUP_UPDATED", @"");
case TSInfoMessageTypeCall:
return [self getCallMessagePreviewTextWithTransaction:transaction];
case TSInfoMessageTypeMessageRequestAccepted:
return NSLocalizedString(@"MESSAGE_REQUESTS_ACCEPTED", @"");
default:
break;
}
@ -166,7 +168,7 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
}
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt
trySendReadReceipt:(BOOL)trySendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
if (_read) {
@ -176,7 +178,7 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
_read = YES;
[self saveWithTransaction:transaction];
// Ignore sendReadReceipt, it doesn't apply to info messages.
// Ignore trySendReadReceipt, it doesn't apply to info messages.
}
@end

View File

@ -117,7 +117,8 @@ public final class OpenGroupManagerV2 : NSObject {
// https://143.198.213.225:443/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
// 143.198.213.255:80/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
let useTLS = (url.scheme == "https")
let room = String(url.path.dropFirst()) // Drop the leading slash
let updatedPath = (url.path.starts(with: "/r/") ? url.path.substring(from: 2) : url.path)
let room = String(updatedPath.dropFirst()) // Drop the leading slash
let queryParts = query.split(separator: "=")
guard !room.isEmpty && !room.contains("/"), queryParts.count == 2, queryParts[0] == "public_key" else { return nil }
let publicKey = String(queryParts[1])

View File

@ -450,6 +450,103 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
#endif
// MARK: - SNProtoMessageRequestResponse
@objc public class SNProtoMessageRequestResponse: NSObject {
// MARK: - SNProtoMessageRequestResponseBuilder
@objc public class func builder(isApproved: Bool) -> SNProtoMessageRequestResponseBuilder {
return SNProtoMessageRequestResponseBuilder(isApproved: isApproved)
}
// asBuilder() constructs a builder that reflects the proto's contents.
@objc public func asBuilder() -> SNProtoMessageRequestResponseBuilder {
let builder = SNProtoMessageRequestResponseBuilder(isApproved: isApproved)
return builder
}
@objc public class SNProtoMessageRequestResponseBuilder: NSObject {
private var proto = SessionProtos_MessageRequestResponse()
@objc fileprivate override init() {}
@objc fileprivate init(isApproved: Bool) {
super.init()
setIsApproved(isApproved)
}
@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 isApproved: Bool
private init(proto: SessionProtos_MessageRequestResponse,
isApproved: Bool) {
self.proto = proto
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.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,
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 {
@ -484,6 +581,9 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
if let _value = unsendRequest {
builder.setUnsendRequest(_value)
}
if let _value = messageRequestResponse {
builder.setMessageRequestResponse(_value)
}
return builder
}
@ -521,6 +621,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)
}
@ -546,6 +650,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
@objc public let unsendRequest: SNProtoUnsendRequest?
@objc public let messageRequestResponse: SNProtoMessageRequestResponse?
private init(proto: SessionProtos_Content,
dataMessage: SNProtoDataMessage?,
callMessage: SNProtoCallMessage?,
@ -553,7 +659,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
typingMessage: SNProtoTypingMessage?,
configurationMessage: SNProtoConfigurationMessage?,
dataExtractionNotification: SNProtoDataExtractionNotification?,
unsendRequest: SNProtoUnsendRequest?) {
unsendRequest: SNProtoUnsendRequest?,
messageRequestResponse: SNProtoMessageRequestResponse?) {
self.proto = proto
self.dataMessage = dataMessage
self.callMessage = callMessage
@ -562,6 +669,7 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
self.configurationMessage = configurationMessage
self.dataExtractionNotification = dataExtractionNotification
self.unsendRequest = unsendRequest
self.messageRequestResponse = messageRequestResponse
}
@objc
@ -610,6 +718,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 -
@ -621,7 +734,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
typingMessage: typingMessage,
configurationMessage: configurationMessage,
dataExtractionNotification: dataExtractionNotification,
unsendRequest: unsendRequest)
unsendRequest: unsendRequest,
messageRequestResponse: messageRequestResponse)
return result
}
@ -2603,6 +2717,15 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
if let _value = profileKey {
builder.setProfileKey(_value)
}
if hasIsApproved {
builder.setIsApproved(isApproved)
}
if hasIsBlocked {
builder.setIsBlocked(isBlocked)
}
if hasDidApproveMe {
builder.setDidApproveMe(didApproveMe)
}
return builder
}
@ -2635,6 +2758,18 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
proto.profileKey = valueParam
}
@objc public func setIsApproved(_ valueParam: Bool) {
proto.isApproved = valueParam
}
@objc public func setIsBlocked(_ valueParam: Bool) {
proto.isBlocked = valueParam
}
@objc public func setDidApproveMe(_ valueParam: Bool) {
proto.didApproveMe = valueParam
}
@objc public func build() throws -> SNProtoConfigurationMessageContact {
return try SNProtoConfigurationMessageContact.parseProto(proto)
}
@ -2670,6 +2805,27 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
return proto.hasProfileKey
}
@objc public var isApproved: Bool {
return proto.isApproved
}
@objc public var hasIsApproved: Bool {
return proto.hasIsApproved
}
@objc public var isBlocked: Bool {
return proto.isBlocked
}
@objc public var hasIsBlocked: Bool {
return proto.hasIsBlocked
}
@objc public var didApproveMe: Bool {
return proto.didApproveMe
}
@objc public var hasDidApproveMe: Bool {
return proto.hasDidApproveMe
}
private init(proto: SessionProtos_ConfigurationMessage.Contact,
publicKey: Data,
name: String) {

View File

@ -229,6 +229,28 @@ 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 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 _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
@ -297,6 +319,15 @@ struct SessionProtos_Content {
/// Clears the value of `unsendRequest`. Subsequent reads from it will return its default value.
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() {}
@ -1165,6 +1196,36 @@ struct SessionProtos_ConfigurationMessage {
/// Clears the value of `profileKey`. Subsequent reads from it will return its default value.
mutating func clearProfileKey() {self._profileKey = nil}
/// added for msg requests
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}
/// added for msg requests
var isBlocked: Bool {
get {return _isBlocked ?? false}
set {_isBlocked = newValue}
}
/// Returns true if `isBlocked` has been explicitly set.
var hasIsBlocked: Bool {return self._isBlocked != nil}
/// 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() {}
@ -1173,6 +1234,9 @@ struct SessionProtos_ConfigurationMessage {
fileprivate var _name: String? = nil
fileprivate var _profilePicture: String? = nil
fileprivate var _profileKey: Data? = nil
fileprivate var _isApproved: Bool? = nil
fileprivate var _isBlocked: Bool? = nil
fileprivate var _didApproveMe: Bool? = nil
}
init() {}
@ -1680,6 +1744,43 @@ extension SessionProtos_UnsendRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes
}
}
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: "isApproved"),
]
public var isInitialized: Bool {
if self._isApproved == nil {return false}
return true
}
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularBoolField(value: &self._isApproved) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._isApproved {
try visitor.visitSingularBoolField(value: v, fieldNumber: 1)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SessionProtos_MessageRequestResponse, rhs: SessionProtos_MessageRequestResponse) -> Bool {
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 = [
@ -1690,6 +1791,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
7: .same(proto: "configurationMessage"),
8: .same(proto: "dataExtractionNotification"),
9: .same(proto: "unsendRequest"),
10: .same(proto: "messageRequestResponse"),
]
fileprivate class _StorageClass {
@ -1700,6 +1802,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
var _configurationMessage: SessionProtos_ConfigurationMessage? = nil
var _dataExtractionNotification: SessionProtos_DataExtractionNotification? = nil
var _unsendRequest: SessionProtos_UnsendRequest? = nil
var _messageRequestResponse: SessionProtos_MessageRequestResponse? = nil
static let defaultInstance = _StorageClass()
@ -1713,6 +1816,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
_configurationMessage = source._configurationMessage
_dataExtractionNotification = source._dataExtractionNotification
_unsendRequest = source._unsendRequest
_messageRequestResponse = source._messageRequestResponse
}
}
@ -1732,6 +1836,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
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
}
}
@ -1751,6 +1856,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
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
}
}
@ -1780,6 +1886,9 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
if let v = _storage._unsendRequest {
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
}
if let v = _storage._messageRequestResponse {
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
}
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1796,6 +1905,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
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}
@ -2679,6 +2789,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
2: .same(proto: "name"),
3: .same(proto: "profilePicture"),
4: .same(proto: "profileKey"),
5: .same(proto: "isApproved"),
6: .same(proto: "isBlocked"),
7: .same(proto: "didApproveMe"),
]
public var isInitialized: Bool {
@ -2697,6 +2810,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
case 2: try { try decoder.decodeSingularStringField(value: &self._name) }()
case 3: try { try decoder.decodeSingularStringField(value: &self._profilePicture) }()
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
}
}
@ -2715,6 +2831,15 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
if let v = self._profileKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
}
if let v = self._isApproved {
try visitor.visitSingularBoolField(value: v, fieldNumber: 5)
}
if let v = self._isBlocked {
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
}
if let v = self._didApproveMe {
try visitor.visitSingularBoolField(value: v, fieldNumber: 7)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -2723,6 +2848,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
if lhs._name != rhs._name {return false}
if lhs._profilePicture != rhs._profilePicture {return false}
if lhs._profileKey != rhs._profileKey {return false}
if lhs._isApproved != rhs._isApproved {return false}
if lhs._isBlocked != rhs._isBlocked {return false}
if lhs._didApproveMe != rhs._didApproveMe {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View File

@ -41,6 +41,11 @@ message UnsendRequest {
required string author = 2;
}
message MessageRequestResponse {
// @required
required bool isApproved = 1; // Whether the request was approved
}
message Content {
optional DataMessage dataMessage = 1;
optional CallMessage callMessage = 3;
@ -49,6 +54,7 @@ message Content {
optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 8;
optional UnsendRequest unsendRequest = 9;
optional MessageRequestResponse messageRequestResponse = 10;
}
message CallMessage {
@ -202,6 +208,9 @@ message ConfigurationMessage {
required string name = 2;
optional string profilePicture = 3;
optional bytes profileKey = 4;
optional bool isApproved = 5; // added for msg requests
optional bool isBlocked = 6; // added for msg requests
optional bool didApproveMe = 7; // added for msg requests
}
repeated ClosedGroup closedGroups = 1;

View File

@ -12,6 +12,7 @@
#import "TSGroupThread.h"
#import "YapDatabaseConnection+OWS.h"
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@ -250,7 +251,27 @@ NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingMan
forKey:kOWSBlockingManager_BlockedGroupMapKey
inCollection:kOWSBlockingManager_BlockListCollection];
// Update the contact blocked state (so sync'ing won't be busted)
NSMutableArray<SNContact *> *contactsToUpdate = [[NSMutableArray alloc] init];
[[[LKStorage shared] getAllContacts] enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, BOOL * _Nonnull stop) {
// If the blocked flag doesn't match then add it to the array to be saved
BOOL contactInBlockedList = [blockedPhoneNumbers containsObject:obj.sessionID];
if (obj.isBlocked != contactInBlockedList) {
obj.isBlocked = contactInBlockedList;
[contactsToUpdate addObject:obj];
}
}];
if ([contactsToUpdate count] > 0) {
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
[contactsToUpdate enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[[LKStorage shared] setContact:obj usingTransaction:transaction];
}];
}];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (sendSyncMessage) {

View File

@ -18,9 +18,11 @@ extension MessageReceiver {
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
case let message as UnsendRequest: handleUnsendRequest(message, using: transaction)
case let message as CallMessage: handleCallMessage(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()
}
var isMainAppAndActive = false
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
isMainAppAndActive = sharedUserDefaults.bool(forKey: "isMainAppActive")
@ -190,15 +192,23 @@ extension MessageReceiver {
SNLog("Configuration message received.")
let storage = SNMessagingKitConfiguration.shared.storage
let transaction = transaction as! YapDatabaseReadWriteTransaction
let messageSentTimestamp: TimeInterval = TimeInterval((message.sentTimestamp ?? 0) / 1000) // `sentTimestamp` is in ms
let lastConfigTimestamp: TimeInterval = (UserDefaults.standard[.lastConfigurationSync]?.timeIntervalSince1970 ?? Date(timeIntervalSince1970: 0).timeIntervalSince1970)
// Profile
var userProfileKey: OWSAES256Key? = nil
if let profileKey = message.profileKey { userProfileKey = OWSAES256Key(data: profileKey) }
updateProfileIfNeeded(publicKey: userPublicKey, name: message.displayName, profilePictureURL: message.profilePictureURL,
profileKey: userProfileKey, sentTimestamp: message.sentTimestamp!, transaction: transaction)
// Initial configuration sync
if !UserDefaults.standard[.hasSyncedInitialConfiguration] {
UserDefaults.standard[.hasSyncedInitialConfiguration] = true
NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil)
if !UserDefaults.standard[.hasSyncedInitialConfiguration] || messageSentTimestamp > lastConfigTimestamp {
if !UserDefaults.standard[.hasSyncedInitialConfiguration] {
UserDefaults.standard[.hasSyncedInitialConfiguration] = true
NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil)
}
UserDefaults.standard[.lastConfigurationSync] = Date(timeIntervalSince1970: messageSentTimestamp)
// Contacts
for contactInfo in message.contacts {
let sessionID = contactInfo.publicKey!
@ -206,11 +216,54 @@ extension MessageReceiver {
if let profileKey = contactInfo.profileKey { contact.profileEncryptionKey = OWSAES256Key(data: profileKey) }
contact.profilePictureURL = contactInfo.profilePictureURL
contact.name = contactInfo.displayName
// Note: We only update these values if the proto actually has values for them (this is to
// prevent an edge case where an old client could override the values with default values
// since they aren't included)
if contactInfo.hasIsApproved { contact.isApproved = contactInfo.isApproved }
if contactInfo.hasIsBlocked { contact.isBlocked = contactInfo.isBlocked }
if contactInfo.hasDidApproveMe { 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)
// If the contact is blocked
if contactInfo.hasIsBlocked && contactInfo.isBlocked {
// If this message changed them to the blocked state and there is an existing thread
// associated with them that is a message request thread then delete it (assume
// that the current user had deleted that message request)
if
contactInfo.isBlocked != OWSBlockingManager.shared().isRecipientIdBlocked(sessionID),
let thread: TSContactThread = TSContactThread.fetch(for: sessionID, using: transaction),
thread.isMessageRequest(using: transaction)
{
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
}
}
else {
// Otherwise create and save the thread
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
}
}
// FIXME: 'OWSBlockingManager' manages it's own dbConnection and transactions so we have to dispatch this to prevent deadlocks
DispatchQueue.global().async {
for contactInfo in message.contacts {
let sessionID = contactInfo.publicKey!
if contactInfo.hasIsBlocked && contactInfo.isBlocked != OWSBlockingManager.shared().isRecipientIdBlocked(sessionID) {
if contactInfo.isBlocked {
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionID)
}
else {
OWSBlockingManager.shared().removeBlockedPhoneNumber(sessionID)
}
}
}
}
// Closed groups
let allClosedGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
for closedGroup in message.closedGroups {
@ -379,8 +432,24 @@ extension MessageReceiver {
if let tsOutgoingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSOutgoingMessage,
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) {
// Mark previous messages as read if there is a sync message
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: tsOutgoingMessage.sortId, thread: thread)
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: tsOutgoingMessage.sortId, thread: thread, trySendReadReceipt: true)
}
// 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 {
updateContactApprovalStatusIfNeeded(
senderSessionId: senderSessionId,
threadId: message.threadID,
forceConfigSync: false,
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 }
@ -469,10 +538,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)
@ -481,18 +566,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
@ -736,4 +827,80 @@ extension MessageReceiver {
// Perform the update
update(groupID, thread, group)
}
// MARK: - Message Requests
private static func updateContactApprovalStatusIfNeeded(
senderSessionId: String,
threadId: String?,
forceConfigSync: Bool,
using transaction: Any
) {
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return }
let userPublicKey: String = getUserHexEncodedPublicKey()
// If the sender of the message was the current user
if senderSessionId == userPublicKey {
// Retrieve the contact for the thread the message was sent to (excluding 'NoteToSelf' threads) and if
// the contact isn't flagged as approved then do so
guard let threadId: String = threadId else { return }
guard let thread: TSContactThread = TSContactThread.fetch(uniqueId: threadId, transaction: transaction), !thread.isNoteToSelf() else { return }
guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else { return }
guard !contact.isApproved else { return }
contact.isApproved = true
Storage.shared.setContact(contact, using: transaction)
}
else {
// The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to
// someone without approving them)
guard let contact: Contact = Storage.shared.getContact(with: senderSessionId, using: transaction) else { return }
guard !contact.didApproveMe else { return }
contact.didApproveMe = true
Storage.shared.setContact(contact, using: transaction)
}
// Force a config sync to ensure all devices know the contact approval state if desired (Note: This logic
// should match the behaviour in AppDelegate.forceSyncConfigurationNowIfNeeded())
guard forceConfigSync else { return }
// Note: We MUST run this async as we need to ensure the database `transaction` has finished before we generate
// a new configuration message (otherwise the `contact` will be loaded direct from the database and the
// `didApproveMe` value won't have been updated)
DispatchQueue.global(qos: .background).async {
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 were sent from the current user
guard message.sender != 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.fetch(for: senderId, using: transaction) {
let infoMessage = TSInfoMessage(
timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()),
in: thread,
messageType: .messageRequestAccepted
)
infoMessage.save(with: transaction)
}
updateContactApprovalStatusIfNeeded(
senderSessionId: senderId,
threadId: nil,
forceConfigSync: true,
using: transaction
)
}
}

View File

@ -129,6 +129,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 }
if let callMessage = CallMessage.fromProto(proto) { return callMessage }
return nil

View File

@ -45,7 +45,7 @@ extern NSString *const kIncomingMessageMarkedAsReadNotification;
// This method can be called from any thread.
- (void)messageWasReadLocally:(TSIncomingMessage *)message;
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread;
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread trySendReadReceipt:(BOOL)trySendReadReceipt;
#pragma mark - Settings

View File

@ -180,13 +180,13 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
#pragma mark - Mark as Read Locally
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread
- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread trySendReadReceipt:(BOOL)trySendReadReceipt
{
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self markAsReadBeforeSortId:sortId
thread:thread
readTimestamp:[NSDate millisecondTimestamp]
wasLocal:YES
trySendReadReceipt:trySendReadReceipt
transaction:transaction];
}];
}
@ -254,7 +254,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
- (void)markAsReadBeforeSortId:(uint64_t)sortId
thread:(TSThread *)thread
readTimestamp:(uint64_t)readTimestamp
wasLocal:(BOOL)wasLocal
trySendReadReceipt:(BOOL)trySendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
NSMutableArray<id<OWSReadTracking>> *newlyReadList = [NSMutableArray new];
@ -285,7 +285,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
}
for (id<OWSReadTracking> readItem in newlyReadList) {
[readItem markAsReadAtTimestamp:readTimestamp sendReadReceipt:wasLocal transaction:transaction];
[readItem markAsReadAtTimestamp:readTimestamp trySendReadReceipt:trySendReadReceipt transaction:transaction];
}
}

View File

@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
* Used both for *responding* to a remote read receipt and in response to the local user's activity.
*/
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt
trySendReadReceipt:(BOOL)trySendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end

View File

@ -86,7 +86,7 @@ public class TypingIndicatorsImpl : NSObject, TypingIndicators {
@objc
public func didStartTypingOutgoingInput(inThread thread: TSThread) {
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
return
}
outgoingIndicators.didStartTypingOutgoingInput()
@ -94,7 +94,7 @@ public class TypingIndicatorsImpl : NSObject, TypingIndicators {
@objc
public func didStopTypingOutgoingInput(inThread thread: TSThread) {
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread), !thread.isMessageRequest() else {
return
}
outgoingIndicators.didStopTypingOutgoingInput()

View File

@ -18,6 +18,7 @@ public protocol SessionMessagingKitStorageProtocol {
func getUserED25519KeyPair() -> Box.KeyPair?
func getUser() -> Contact?
func getAllContacts() -> Set<Contact>
func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact>
// MARK: - Closed Groups

View File

@ -57,6 +57,32 @@ 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)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
return (
self.shouldBeVisible &&
!self.isNoteToSelf && (
contact == nil ||
!contact.isApproved
)
);
}
- (BOOL)isGroupThread
{
return NO;

View File

@ -54,6 +54,14 @@ 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;
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction;
#pragma mark Interactions
- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction usingBlock:(void (^)(TSInteraction *interaction, BOOL *stop))block;

View File

@ -132,6 +132,16 @@ BOOL IsNoteToSelfEnabled(void)
return [self.contactSessionID isEqual:[SNGeneralUtilities getUserPublicKey]];
}
// Override in ContactThread
- (BOOL)isMessageRequest {
return NO;
}
// Override in ContactThread
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
return NO;
}
#pragma mark To be subclassed.
- (BOOL)isGroupThread {
@ -311,7 +321,7 @@ BOOL IsNoteToSelfEnabled(void)
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) {
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] sendReadReceipt:YES transaction:transaction];
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] trySendReadReceipt:YES transaction:transaction];
}
}

View File

@ -41,72 +41,110 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
let senderPublicKey = message.sender!
var senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .regular) ?? senderPublicKey
let snippet: String
var userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
var isMessageRequest: Bool = false
switch message {
case let visibleMessage as VisibleMessage:
let tsIncomingMessageID = try MessageReceiver.handleVisibleMessage(visibleMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: false, using: transaction)
guard let tsMessage = TSMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction) else {
return self.completeSilenty()
}
let thread = tsMessage.thread(with: transaction)
let threadID = thread.uniqueId!
userInfo[NotificationServiceExtension.threadIdKey] = threadID
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
case let visibleMessage as VisibleMessage:
let tsIncomingMessageID = try MessageReceiver.handleVisibleMessage(visibleMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: false, using: transaction)
guard let tsMessage = TSMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction) else {
return self.completeSilenty()
}
if let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction), let group = thread as? TSGroupThread,
group.groupModel.groupType == .closedGroup { // Should always be true because we don't get PNs for open groups
senderDisplayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderDisplayName, group.groupModel.groupName ?? MessageStrings.newGroupDefaultTitle)
if group.isOnlyNotifyingForMentions && !tsIncomingMessage.isUserMentioned {
// Ignore PNs if the group is set to only notify for mentions
return self.completeSilenty()
let thread = tsMessage.thread(with: transaction)
let threadID = thread.uniqueId!
userInfo[NotificationServiceExtension.threadIdKey] = threadID
snippet = tsMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
?? "You've got a new message"
if let tsIncomingMessage = tsMessage as? TSIncomingMessage {
// Ignore PNs if the thread is muted
if thread.isMuted { return self.completeSilenty() }
if let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction), let group = thread as? TSGroupThread,
group.groupModel.groupType == .closedGroup { // Should always be true because we don't get PNs for open groups
senderDisplayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderDisplayName, group.groupModel.groupName ?? MessageStrings.newGroupDefaultTitle)
if group.isOnlyNotifyingForMentions && !tsIncomingMessage.isUserMentioned {
// Ignore PNs if the group is set to only notify for mentions
return self.completeSilenty()
}
}
// If the thread is a message request and the user hasn't hidden message requests then we need
// to check if this is the only message request thread (group threads can't be message requests
// so just ignore those and if the user has hidden message requests then we want to show the
// notification regardless of how many message requests there are)
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
let dbConnection: YapDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
dbConnection.objectCacheLimit = 2
dbConnection.beginLongLivedReadTransaction() // 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)
let threads: YapDatabaseViewMappings = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName)
dbConnection.read { transaction in
threads.update(with: transaction) // Perform the initial update
}
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
dbConnection.endLongLivedReadTransaction()
// Allow this to show a notification if there are no message requests (ie. this is the first one)
guard numMessageRequests == 0 else { return self.completeSilenty() }
}
else if thread.isMessageRequest() && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
// If there are other interactions on this thread already then don't show the notification
if thread.numberOfInteractions() > 1 { return }
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
}
isMessageRequest = thread.isMessageRequest()
// Store the notification ID for unsend requests to later cancel this notification
tsIncomingMessage.setNotificationIdentifier(request.identifier, transaction: transaction)
}
// Store the notification ID for unsend requests to later cancel this notification
tsIncomingMessage.setNotificationIdentifier(request.identifier, transaction: transaction)
} else {
let semaphore = DispatchSemaphore(value: 0)
let center = UNUserNotificationCenter.current()
center.getDeliveredNotifications { notifications in
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == threadID})
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
}
semaphore.wait()
}
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
case let unsendRequest as UnsendRequest:
MessageReceiver.handleUnsendRequest(unsendRequest, using: transaction)
return self.completeSilenty()
case let closedGroupControlMessage as ClosedGroupControlMessage:
// TODO: We could consider actually handling the update here. Not sure if there's enough time though, seeing as though
// in some cases we need to send messages (e.g. our sender key) to a number of other users.
switch closedGroupControlMessage.kind {
case .new(_, let name, _, _, _, _): snippet = "\(senderDisplayName) added you to \(name)"
default: return self.completeSilenty()
}
case let callMessage as CallMessage:
MessageReceiver.handleCallMessage(callMessage, using: transaction)
guard case .preOffer = callMessage.kind else { return self.completeSilenty() }
if !SSKPreferences.areCallsEnabled {
if let sender = callMessage.sender, let thread = TSContactThread.fetch(for: sender, using: transaction), thread.hasOutgoingInteraction(with: transaction) {
let infoMessage = TSInfoMessage.from(callMessage, associatedWith: thread)
infoMessage.updateCallInfoMessage(.missed, using: transaction)
else {
let semaphore = DispatchSemaphore(value: 0)
let center = UNUserNotificationCenter.current()
center.getDeliveredNotifications { notifications in
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == threadID})
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
}
semaphore.wait()
}
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
case let unsendRequest as UnsendRequest:
MessageReceiver.handleUnsendRequest(unsendRequest, using: transaction)
return self.completeSilenty()
}
notificationContent.userInfo = userInfo
notificationContent.badge = 1
notificationContent.title = "Session"
notificationContent.body = "\(senderDisplayName) is calling..."
return self.handleSuccessForIncomingCall(for: notificationContent, callMessage: callMessage)
default: return self.completeSilenty()
case let closedGroupControlMessage as ClosedGroupControlMessage:
// TODO: We could consider actually handling the update here. Not sure if there's enough time though, seeing as though
// in some cases we need to send messages (e.g. our sender key) to a number of other users.
switch closedGroupControlMessage.kind {
case .new(_, let name, _, _, _, _): snippet = "\(senderDisplayName) added you to \(name)"
default: return self.completeSilenty()
}
case let callMessage as CallMessage:
MessageReceiver.handleCallMessage(callMessage, using: transaction)
guard case .preOffer = callMessage.kind else { return self.completeSilenty() }
if !SSKPreferences.areCallsEnabled {
if let sender = callMessage.sender, let thread = TSContactThread.fetch(for: sender, using: transaction), thread.hasOutgoingInteraction(with: transaction) {
let infoMessage = TSInfoMessage.from(callMessage, associatedWith: thread)
infoMessage.updateCallInfoMessage(.missed, using: transaction)
}
return self.completeSilenty()
}
notificationContent.userInfo = userInfo
notificationContent.badge = 1
notificationContent.title = "Session"
notificationContent.body = "\(senderDisplayName) is calling..."
return self.handleSuccessForIncomingCall(for: notificationContent, callMessage: callMessage)
default: return self.completeSilenty()
}
if (senderPublicKey == userPublicKey) {
// Ignore PNs for messages sent by the current user
// after handling the message. Otherwise the closed
@ -115,21 +153,35 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
}
notificationContent.userInfo = userInfo
notificationContent.badge = 1
let notificationsPreference = Environment.shared.preferences!.notificationPreviewType()
switch notificationsPreference {
case .namePreview:
notificationContent.title = senderDisplayName
notificationContent.body = snippet
case .nameNoPreview:
notificationContent.title = senderDisplayName
notificationContent.body = NotificationStrings.incomingMessageBody
case .noNameNoPreview:
notificationContent.title = "Session"
notificationContent.body = NotificationStrings.incomingMessageBody
default: break
case .namePreview:
notificationContent.title = senderDisplayName
notificationContent.body = snippet
case .nameNoPreview:
notificationContent.title = senderDisplayName
notificationContent.body = NotificationStrings.incomingMessageBody
case .noNameNoPreview:
notificationContent.title = "Session"
notificationContent.body = NotificationStrings.incomingMessageBody
default: break
}
// If it's a message request then overwrite the body to be something generic (only show a notification
// when receiving a new message request if there aren't any others or the user had hidden them)
if isMessageRequest {
notificationContent.title = "Session"
notificationContent.body = NSLocalizedString("MESSAGE_REQUESTS_NOTIFICATION", comment: "")
}
self.handleSuccess(for: notificationContent)
} catch {
}
catch {
if let error = error as? MessageReceiver.Error, error.isRetryable {
self.handleFailure(for: notificationContent)
}

View File

@ -225,7 +225,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: { _ in
self.extensionContext!.cancelRequest(withError: error)
}))
present(alert, animated: true, completion: nil)
presentAlert(alert)
}
// MARK: Attachment Prep

View File

@ -11,7 +11,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
var shareVC: ShareVC?
private var threadCount: UInt {
threads.numberOfItems(inGroup: TSInboxGroup)
threads.numberOfItems(inGroup: TSShareExtensionGroup)
}
private lazy var dbConnection: YapDatabaseConnection = {
@ -65,8 +65,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
// Threads
dbConnection.beginLongLivedReadTransaction() // 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)
threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
threads.setIsReversed(true, forGroup: TSInboxGroup)
threads = YapDatabaseViewMappings(groups: [ TSShareExtensionGroup ], view: TSThreadShareExtensionDatabaseViewExtensionName) // The extension should be registered at this point
threads.setIsReversed(true, forGroup: TSShareExtensionGroup)
dbConnection.read { transaction in
self.threads.update(with: transaction) // Perform the initial update
}
@ -222,7 +222,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
private func thread(at index: Int) -> TSThread? {
var thread: TSThread? = nil
dbConnection.read { transaction in
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
let ext = transaction.ext(TSThreadShareExtensionDatabaseViewExtensionName) as! YapDatabaseViewTransaction
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
}
return thread

View File

@ -6,7 +6,7 @@ public final class Button : UIButton {
private var heightConstraint: NSLayoutConstraint!
public enum Style {
case unimportant, regular, prominentOutline, prominentFilled, regularBorderless
case unimportant, regular, prominentOutline, prominentFilled, regularBorderless, destructiveOutline
}
public enum Size {
@ -41,6 +41,7 @@ public final class Button : UIButton {
case .prominentOutline: fillColor = UIColor.clear
case .prominentFilled: fillColor = isLightMode ? Colors.text : Colors.accent
case .regularBorderless: fillColor = UIColor.clear
case .destructiveOutline: fillColor = UIColor.clear
}
let borderColor: UIColor
switch style {
@ -49,6 +50,7 @@ public final class Button : UIButton {
case .prominentOutline: borderColor = isLightMode ? Colors.text : Colors.accent
case .prominentFilled: borderColor = isLightMode ? Colors.text : Colors.accent
case .regularBorderless: borderColor = UIColor.clear
case .destructiveOutline: borderColor = Colors.destructive
}
let textColor: UIColor
switch style {
@ -57,6 +59,7 @@ public final class Button : UIButton {
case .prominentOutline: textColor = isLightMode ? Colors.text : Colors.accent
case .prominentFilled: textColor = isLightMode ? UIColor.white : Colors.text
case .regularBorderless: textColor = Colors.text
case .destructiveOutline: textColor = Colors.destructive
}
let height: CGFloat
switch size {

View File

@ -4,18 +4,21 @@ public final class SearchBar : UISearchBar {
public override init(frame: CGRect) {
super.init(frame: frame)
setUpStyle()
setUpSessionStyle()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
setUpStyle()
setUpSessionStyle()
}
}
public extension UISearchBar {
private func setUpStyle() {
func setUpSessionStyle() {
searchBarStyle = .minimal // Hide the border around the search bar
barStyle = .black // Use Apple's black design as a base
tintColor = Colors.accent // The cursor color
tintColor = Colors.text // The cursor color
let searchImage = #imageLiteral(resourceName: "searchbar_search").withTint(Colors.searchBarPlaceholder)!
setImage(searchImage, for: .search, state: .normal)
let clearImage = #imageLiteral(resourceName: "searchbar_clear").withTint(Colors.searchBarPlaceholder)!

View File

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

View File

@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF9",
"green" : "0xF9",
"red" : "0xF9"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More