Merge branch 'dev' into emoji-reacts

This commit is contained in:
Ryan Zhao 2022-05-30 12:07:33 +10:00
commit d3675c4fe8
22 changed files with 179 additions and 111 deletions

View file

@ -10,7 +10,7 @@ abstract_target 'GlobalDependencies' do
pod 'CryptoSwift'
pod 'Sodium', '~> 0.9.1'
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/oxen-io/session-ios-yap-database.git', branch: 'signal-release'
pod 'GoogleWebRTC'
pod 'WebRTC-lib'
pod 'SocketRocket', '~> 0.5.1'
target 'Session' do

View file

@ -21,7 +21,6 @@ PODS:
- Curve25519Kit (2.1.0):
- CocoaLumberjack
- SignalCoreKit
- GoogleWebRTC (1.1.31999)
- Mantle (2.1.0):
- Mantle/extobjc (= 2.1.0)
- Mantle/extobjc (2.1.0)
@ -52,6 +51,7 @@ PODS:
- SQLCipher/standard (4.5.0):
- SQLCipher/common
- SwiftProtobuf (1.5.0)
- WebRTC-lib (96.0.0)
- YapDatabase/SQLCipher (3.1.1):
- YapDatabase/SQLCipher/Core (= 3.1.1)
- YapDatabase/SQLCipher/Extensions (= 3.1.1)
@ -125,7 +125,6 @@ DEPENDENCIES:
- AFNetworking
- CryptoSwift
- Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit.git`)
- GoogleWebRTC
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
- NVActivityIndicatorView
- PromiseKit
@ -136,6 +135,7 @@ DEPENDENCIES:
- SocketRocket (~> 0.5.1)
- Sodium (~> 0.9.1)
- SwiftProtobuf (~> 1.5.0)
- WebRTC-lib
- YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`)
- YYImage (from `https://github.com/signalapp/YYImage`)
- ZXingObjC
@ -145,7 +145,6 @@ SPEC REPOS:
- AFNetworking
- CocoaLumberjack
- CryptoSwift
- GoogleWebRTC
- NVActivityIndicatorView
- OpenSSL-Universal
- PromiseKit
@ -156,6 +155,7 @@ SPEC REPOS:
- Sodium
- SQLCipher
- SwiftProtobuf
- WebRTC-lib
- ZXingObjC
EXTERNAL SOURCES:
@ -195,7 +195,6 @@ SPEC CHECKSUMS:
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
GoogleWebRTC: b39a78c4f5cc6b0323415b9233db03a2faa7b0f0
Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
OpenSSL-Universal: e7311447fd2419f57420c79524b641537387eff2
@ -208,10 +207,11 @@ SPEC CHECKSUMS:
Sodium: 23d11554ecd556196d313cf6130d406dfe7ac6da
SQLCipher: 98dc22f27c0b1790d39e710d440f22a466ebdb59
SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2
WebRTC-lib: 508fe02efa0c1a3a8867082a77d24c9be5d29aeb
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 610d1ebc4e559cf746dc3ae0ae7c78b011373d4c
PODFILE CHECKSUM: a3d89a6cc8735285fd51348ca05cea71f2c28872
COCOAPODS: 1.11.2

View file

@ -142,6 +142,7 @@
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; };
7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; };
7B703747283CA919000DCF35 /* Storage+Calls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B703746283CA919000DCF35 /* Storage+Calls.swift */; };
7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; };
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; };
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; };
@ -1137,6 +1138,7 @@
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
7B703746283CA919000DCF35 /* Storage+Calls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Calls.swift"; sourceTree = "<group>"; };
7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = "<group>"; };
7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = "<group>"; };
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
@ -2780,6 +2782,7 @@
B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */,
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */,
C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */,
7B703746283CA919000DCF35 /* Storage+Calls.swift */,
C33FDA69255A57F900E217F9 /* SSKPreferences.swift */,
C33FDB25255A580900E217F9 /* TSDatabaseSecondaryIndexes.h */,
C33FDB20255A580900E217F9 /* TSDatabaseSecondaryIndexes.m */,
@ -4727,6 +4730,7 @@
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
C3A3A156256E1B91004D228D /* ProtoUtils.m in Sources */,
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
7B703747283CA919000DCF35 /* Storage+Calls.swift in Sources */,
C352A32F2557549C00338F3E /* NotifyPNServerJob.swift in Sources */,
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */,
C300A5F22554B09800555489 /* MessageSender.swift in Sources */,
@ -5213,7 +5217,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 343;
CURRENT_PROJECT_VERSION = 345;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -5238,7 +5242,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.12.8;
MARKETING_VERSION = 1.12.9;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -5286,7 +5290,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 343;
CURRENT_PROJECT_VERSION = 345;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -5316,7 +5320,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.12.8;
MARKETING_VERSION = 1.12.9;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -5352,7 +5356,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 343;
CURRENT_PROJECT_VERSION = 345;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -5375,7 +5379,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.12.8;
MARKETING_VERSION = 1.12.9;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -5426,7 +5430,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 343;
CURRENT_PROJECT_VERSION = 345;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -5454,7 +5458,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.12.8;
MARKETING_VERSION = 1.12.9;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6362,7 +6366,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 343;
CURRENT_PROJECT_VERSION = 345;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6402,7 +6406,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.12.8;
MARKETING_VERSION = 1.12.9;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -6435,7 +6439,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 343;
CURRENT_PROJECT_VERSION = 345;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6475,7 +6479,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.12.8;
MARKETING_VERSION = 1.12.9;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View file

@ -2,12 +2,11 @@
import WebRTC
import Foundation
#if arch(arm64)
// Note: 'RTCMTLVideoView' requires arm64 (so won't work on the simulator which
// we need to build for x86_64 due to WebRTC not supporting arm64 simulator builds)
typealias TargetView = RTCMTLVideoView
#else
#if targetEnvironment(simulator)
// Note: 'RTCMTLVideoView' doesn't seem to work on the simulator so use 'RTCEAGLVideoView' instead
typealias TargetView = RTCEAGLVideoView
#else
typealias TargetView = RTCMTLVideoView
#endif
// MARK: RemoteVideoView
@ -17,6 +16,7 @@ class RemoteVideoView: TargetView {
override func renderFrame(_ frame: RTCVideoFrame?) {
super.renderFrame(frame)
guard let frame = frame else { return }
DispatchMainThreadSafe {
let frameRatio = Double(frame.height) / Double(frame.width)
let frameRotation = frame.rotation
@ -47,7 +47,9 @@ class RemoteVideoView: TargetView {
// Assume we're already setup for the correct orientation.
break
}
#if arch(arm64)
#if targetEnvironment(simulator)
#else
if let rotationOverride = rotationOverride {
self.rotationOverride = NSNumber(value: rotationOverride.rawValue)
if [ RTCVideoRotation._0, RTCVideoRotation._180 ].contains(rotationOverride) {
@ -86,7 +88,8 @@ class LocalVideoView: TargetView {
// sometimes the rotationOverride is not working
// if it is only set once on initialization
self.rotationOverride = NSNumber(value: RTCVideoRotation._0.rawValue)
#if arch(arm64)
#if targetEnvironment(simulator)
#else
self.videoContentMode = .scaleAspectFill
#endif
}

View file

@ -16,9 +16,16 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
private var top: NSLayoutConstraint?
private var bottom: NSLayoutConstraint?
#if arch(arm64)
// Note: 'RTCMTLVideoView' requires arm64 (so won't work on the simulator which
// we need to build for x86_64 due to WebRTC not supporting arm64 simulator builds)
#if targetEnvironment(simulator)
// Note: 'RTCMTLVideoView' doesn't seem to work on the simulator so use 'RTCEAGLVideoView' instead
private lazy var remoteVideoView: RTCEAGLVideoView = {
let result = RTCEAGLVideoView()
result.delegate = self
result.alpha = self.callVC.call.isRemoteVideoEnabled ? 1 : 0
result.backgroundColor = .black
return result
}()
#else
private lazy var remoteVideoView: RTCMTLVideoView = {
let result = RTCMTLVideoView()
result.delegate = self
@ -27,14 +34,6 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
result.backgroundColor = .black
return result
}()
#else
private lazy var remoteVideoView: RTCEAGLVideoView = {
let result = RTCEAGLVideoView()
result.delegate = self
result.alpha = self.callVC.call.isRemoteVideoEnabled ? 1 : 0
result.backgroundColor = .black
return result
}()
#endif
// MARK: Initialization

View file

@ -32,6 +32,8 @@ public class ConversationSearchController : NSObject {
@objc
public let resultsBar: SearchResultsBar = SearchResultsBar()
private var lastSearchText: String?
// MARK: Initializer
@ -88,8 +90,10 @@ extension ConversationSearchController : UISearchResultsUpdating {
return
}
let searchText = FullTextSearchFinder.normalize(text: rawSearchText)
lastSearchText = searchText
guard searchText.count >= ConversationSearchController.kMinimumSearchTextLength else {
lastSearchText = nil
self.resultsBar.updateResults(resultSet: nil)
self.delegate?.conversationSearchController(self, didUpdateSearchResults: nil)
return
@ -102,7 +106,7 @@ extension ConversationSearchController : UISearchResultsUpdating {
}
resultSet = self.dbSearcher.searchWithinConversation(thread: self.thread, searchText: searchText, transaction: transaction)
}, completionBlock: { [weak self] in
guard let self = self else {
guard let self = self, searchText == self.lastSearchText else {
return
}
self.resultsBar.updateResults(resultSet: resultSet)
@ -121,7 +125,6 @@ extension ConversationSearchController : SearchResultsBarDelegate {
return
}
BenchEventStart(title: "Conversation Search Nav", eventId: "Conversation Search Nav: \(searchResult.messageId)")
self.delegate?.conversationSearchController(self, didSelectMessageId: searchResult.messageId)
}
}

View file

@ -429,12 +429,12 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
Storage.write { transaction in
self.thread.setDraft(text, transaction: transaction)
}
self.resignFirstResponder()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
mediaCache.removeAllObjects()
self.resignFirstResponder()
}
override func appDidBecomeActive(_ notification: Notification) {
@ -929,7 +929,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectMessageId interactionID: String) {
scrollToInteraction(with: interactionID)
scrollToInteraction(with: interactionID, highlighted: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.highlightFocusedMessageIfNeeded()
}
}
func scrollToInteraction(with interactionID: String, position: UITableView.ScrollPosition = .middle, isAnimated: Bool = true, highlighted: Bool = false) {

View file

@ -333,13 +333,7 @@ NS_ASSUME_NONNULL_BEGIN
selector:@selector(applicationWillEnterForeground:)
name:OWSApplicationWillEnterForegroundNotification
object:nil];
}
- (void)viewDidLoad
{
[self addNotificationListeners];
[self touchDbAsync];
}
- (void)touchDbAsync

View file

@ -146,7 +146,7 @@ final class LinkPreviewView : UIView {
// Body text view
bodyTextViewContainer.subviews.forEach { $0.removeFromSuperview() }
if let viewItem = viewItem {
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: sentLinkPreviewTextColor, searchText: delegate.lastSearchedText, delegate: delegate)
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: sentLinkPreviewTextColor, delegate: delegate)
self.bodyTextView = bodyTextView
bodyTextViewContainer.addSubview(bodyTextView)
bodyTextView.pin(to: bodyTextViewContainer, withInset: 12)

View file

@ -369,7 +369,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
stackView.addArrangedSubview(quoteViewContainer)
}
// Body text view
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, searchText: delegate?.lastSearchedText, delegate: self)
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, delegate: self)
self.bodyTextView = bodyTextView
stackView.addArrangedSubview(bodyTextView)
// Constraints
@ -401,7 +401,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
if let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0 {
let inset: CGFloat = 12
let maxWidth = size.width - 2 * inset
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, searchText: delegate?.lastSearchedText, delegate: self)
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, delegate: self)
self.bodyTextView = bodyTextView
stackView.addArrangedSubview(UIView(wrapping: bodyTextView, withInsets: UIEdgeInsets(top: 0, left: inset, bottom: inset, right: inset)))
}
@ -438,9 +438,8 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
let documentView = DocumentView(viewItem: viewItem, textColor: bodyLabelTextColor)
stackView.addArrangedSubview(documentView)
// Body text view
if let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0,
let delegate = delegate { // delegate should always be set at this point
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, searchText: delegate.lastSearchedText, delegate: self)
if let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0 {
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, delegate: self)
self.bodyTextView = bodyTextView
stackView.addArrangedSubview(bodyTextView)
}
@ -769,7 +768,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
}
static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, searchText: String?, delegate: UITextViewDelegate & BodyTextViewDelegate) -> UITextView {
static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, delegate: UITextViewDelegate & BodyTextViewDelegate) -> UITextView {
// Take care of:
// Highlighting mentions
// Linkification
@ -783,20 +782,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
.font : UIFont.systemFont(ofSize: getFontSize(for: viewItem))
]
let attributedText = NSMutableAttributedString(attributedString: MentionUtilities.highlightMentions(in: message.body ?? "", isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: attributes))
if let searchText = searchText, searchText.count >= ConversationSearchController.kMinimumSearchTextLength {
let normalizedSearchText = FullTextSearchFinder.normalize(text: searchText)
do {
let regex = try NSRegularExpression(pattern: NSRegularExpression.escapedPattern(for: normalizedSearchText), options: .caseInsensitive)
let matches = regex.matches(in: attributedText.string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: (attributedText.string as NSString).length))
for match in matches {
guard match.range.location + match.range.length < attributedText.length else { continue }
attributedText.addAttribute(.backgroundColor, value: UIColor.white, range: match.range)
attributedText.addAttribute(.foregroundColor, value: UIColor.black, range: match.range)
}
} catch {
// Do nothing
}
}
result.attributedText = attributedText
result.dataDetectorTypes = .link
result.backgroundColor = .clear

View file

@ -92,6 +92,19 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
selectionPanGesture.delegate = self
self.selectionPanGesture = selectionPanGesture
collectionView.addGestureRecognizer(selectionPanGesture)
if #available(iOS 14, *) {
if PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited {
let addSeletedPhotoButton = UIBarButtonItem.init(barButtonSystemItem: .add, target: self, action: #selector(addSelectedPhoto))
self.navigationItem.rightBarButtonItem = addSeletedPhotoButton
}
}
}
@objc func addSelectedPhoto(_ sender: Any) {
if #available(iOS 14, *) {
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self)
}
}
var selectionPanGesture: UIPanGestureRecognizer?

View file

@ -56,10 +56,15 @@ extension AppDelegate {
}
}
private func insertCallInfoMessage(for message: CallMessage, using transaction: YapDatabaseReadWriteTransaction) -> TSInfoMessage {
private func insertCallInfoMessage(for message: CallMessage, using transaction: YapDatabaseReadWriteTransaction) -> TSInfoMessage? {
guard let sender = message.sender, let uuid = message.uuid else { return nil }
var receivedCalls = Storage.shared.getReceivedCalls(for: sender, using: transaction)
guard !receivedCalls.contains(uuid) else { return nil }
let thread = TSContactThread.getOrCreateThread(withContactSessionID: message.sender!, transaction: transaction)
let infoMessage = TSInfoMessage.from(message, associatedWith: thread)
infoMessage.save(with: transaction)
receivedCalls.insert(message.uuid!)
Storage.shared.setReceivedCalls(to: receivedCalls, for: sender, using: transaction)
return infoMessage
}
@ -78,20 +83,22 @@ extension AppDelegate {
guard CurrentAppContext().isMainApp else { return }
guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestamp: timestamp) else {
// Add missed call message for call offer messages from more than one minute
let infoMessage = self.insertCallInfoMessage(for: message, using: transaction)
infoMessage.updateCallInfoMessage(.missed, using: transaction)
let thread = TSContactThread.getOrCreateThread(withContactSessionID: message.sender!, transaction: transaction)
SSKEnvironment.shared.notificationsManager?.notifyUser(forIncomingCall: infoMessage, in: thread, transaction: transaction)
if let infoMessage = self.insertCallInfoMessage(for: message, using: transaction) {
infoMessage.updateCallInfoMessage(.missed, using: transaction)
let thread = TSContactThread.getOrCreateThread(withContactSessionID: message.sender!, transaction: transaction)
SSKEnvironment.shared.notificationsManager?.notifyUser(forIncomingCall: infoMessage, in: thread, transaction: transaction)
}
return
}
guard SSKPreferences.areCallsEnabled else {
let infoMessage = self.insertCallInfoMessage(for: message, using: transaction)
infoMessage.updateCallInfoMessage(.permissionDenied, using: transaction)
let thread = TSContactThread.getOrCreateThread(withContactSessionID: message.sender!, transaction: transaction)
SSKEnvironment.shared.notificationsManager?.notifyUser(forIncomingCall: infoMessage, in: thread, transaction: transaction)
let contactName = Storage.shared.getContact(with: message.sender!, using: transaction)?.displayName(for: Contact.Context.regular) ?? message.sender!
DispatchQueue.main.async {
self.showMissedCallTipsIfNeeded(caller: contactName)
if let infoMessage = self.insertCallInfoMessage(for: message, using: transaction) {
infoMessage.updateCallInfoMessage(.permissionDenied, using: transaction)
let thread = TSContactThread.getOrCreateThread(withContactSessionID: message.sender!, transaction: transaction)
SSKEnvironment.shared.notificationsManager?.notifyUser(forIncomingCall: infoMessage, in: thread, transaction: transaction)
let contactName = Storage.shared.getContact(with: message.sender!, using: transaction)?.displayName(for: Contact.Context.regular) ?? message.sender!
DispatchQueue.main.async {
self.showMissedCallTipsIfNeeded(caller: contactName)
}
}
return
}
@ -106,7 +113,7 @@ extension AppDelegate {
// Handle UI
if let caller = message.sender, let uuid = message.uuid {
let call = SessionCall(for: caller, uuid: uuid, mode: .answer)
call.callMessageID = infoMessage.uniqueId
call.callMessageID = infoMessage?.uniqueId
self.showCallUIForCall(call)
}
}

View file

@ -42,16 +42,19 @@ public final class BackgroundPoller : NSObject {
return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) {
SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).then(on: DispatchQueue.main) { rawResponse -> Promise<Void> in
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
var processedMessages: [JSON] = []
let promises = messages.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process if one of the
// messages failed to parse.
guard let envelope = SNProtoEnvelope.from(json),
let data = try? envelope.serializedData() else { return nil }
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: true)
processedMessages.append(json)
return job.execute()
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value & `receivedMessageHashes`
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: SnodeAPI.defaultNamespace, associatedWith: publicKey, from: lastRawMessage)
SnodeAPI.updateReceivedMessages(from: processedMessages, associatedWith: publicKey)
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects
}
@ -82,16 +85,19 @@ public final class BackgroundPoller : NSObject {
for result in results {
if case .fulfilled(let rawResponse) = result {
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
var processedMessages: [JSON] = []
let jobPromises = messages.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process if one of the
// messages failed to parse.
guard let envelope = SNProtoEnvelope.from(json),
let data = try? envelope.serializedData() else { return nil }
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: true)
processedMessages.append(json)
return job.execute()
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value & `receivedMessageHashes`
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: namespaces[index], associatedWith: publicKey, from: lastRawMessage)
SnodeAPI.updateReceivedMessages(from: processedMessages, associatedWith: publicKey)
promises += jobPromises
}
index += 1

View file

@ -8,7 +8,7 @@ extension WebRTCSession: RTCDataChannelDelegate {
dataChannelConfiguration.isOrdered = true
dataChannelConfiguration.isNegotiated = true
dataChannelConfiguration.channelId = 548
guard let dataChannel = peerConnection.dataChannel(forLabel: "CONTROL", configuration: dataChannelConfiguration) else {
guard let dataChannel = peerConnection?.dataChannel(forLabel: "CONTROL", configuration: dataChannelConfiguration) else {
SNLog("[Calls] Couldn't create data channel.")
return nil
}

View file

@ -4,12 +4,12 @@ extension WebRTCSession {
public func handleICECandidates(_ candidate: [RTCIceCandidate]) {
SNLog("[Calls] Received ICE candidate message.")
candidate.forEach { peerConnection.add($0) }
candidate.forEach { peerConnection?.add($0) }
}
public func handleRemoteSDP(_ sdp: RTCSessionDescription, from sessionID: String) {
SNLog("[Calls] Received remote SDP: \(sdp.sdp).")
peerConnection.setRemoteDescription(sdp, completionHandler: { [weak self] error in
peerConnection?.setRemoteDescription(sdp, completionHandler: { [weak self] error in
if let error = error {
SNLog("[Calls] Couldn't set SDP due to error: \(error).")
} else {

View file

@ -35,7 +35,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
/// Represents a WebRTC connection between the user and a remote peer. Provides methods to connect to a
/// remote peer, maintain and monitor the connection, and close the connection once it's no longer needed.
internal lazy var peerConnection: RTCPeerConnection = {
internal lazy var peerConnection: RTCPeerConnection? = {
let configuration = RTCConfiguration()
if let defaultICEServer = defaultICEServer {
configuration.iceServers = [ RTCIceServer(urlStrings: defaultICEServer.urls, username: defaultICEServer.username, credential: defaultICEServer.password) ]
@ -67,7 +67,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
}()
internal lazy var remoteVideoTrack: RTCVideoTrack? = {
return peerConnection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack
return peerConnection?.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack
}()
// Data Channel
@ -94,8 +94,8 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
self.uuid = uuid
super.init()
let mediaStreamTrackIDS = ["ARDAMS"]
peerConnection.add(audioTrack, streamIds: mediaStreamTrackIDS)
peerConnection.add(localVideoTrack, streamIds: mediaStreamTrackIDS)
peerConnection?.add(audioTrack, streamIds: mediaStreamTrackIDS)
peerConnection?.add(localVideoTrack, streamIds: mediaStreamTrackIDS)
// Configure audio session
configureAudioSession()
@ -125,12 +125,12 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
SNLog("[Calls] Sending offer message.")
guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) }
let (promise, seal) = Promise<Void>.pending()
peerConnection.offer(for: mediaConstraints(isRestartingICEConnection)) { [weak self] sdp, error in
peerConnection?.offer(for: mediaConstraints(isRestartingICEConnection)) { [weak self] sdp, error in
if let error = error {
seal.reject(error)
} else {
guard let self = self, let sdp = self.correctSessionDescription(sdp: sdp) else { preconditionFailure() }
self.peerConnection.setLocalDescription(sdp) { error in
self.peerConnection?.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't initiate call due to error: \(error).")
return seal.reject(error)
@ -157,12 +157,12 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
SNLog("[Calls] Sending answer message.")
guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) }
let (promise, seal) = Promise<Void>.pending()
peerConnection.answer(for: mediaConstraints(false)) { [weak self] sdp, error in
peerConnection?.answer(for: mediaConstraints(false)) { [weak self] sdp, error in
if let error = error {
seal.reject(error)
} else {
guard let self = self, let sdp = self.correctSessionDescription(sdp: sdp) else { preconditionFailure() }
self.peerConnection.setLocalDescription(sdp) { error in
self.peerConnection?.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't accept call due to error: \(error).")
return seal.reject(error)
@ -221,7 +221,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
}
public func dropConnection() {
peerConnection.close()
peerConnection?.close()
}
private func mediaConstraints(_ isRestartingICEConnection: Bool) -> RTCMediaConstraints {
@ -263,7 +263,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
if state == .connected {
delegate?.webRTCIsConnected()
} else if state == .disconnected {
if self.peerConnection.signalingState == .stable {
if self.peerConnection?.signalingState == .stable {
delegate?.reconnectIfNeeded()
}
}

View file

@ -0,0 +1,16 @@
extension Storage {
private static let receivedCallsCollection = "LokiReceivedCallsCollection"
public func getReceivedCalls(for publicKey: String, using transaction: Any) -> Set<String> {
var result: Set<String>?
guard let transaction = transaction as? YapDatabaseReadTransaction else { return [] }
result = transaction.object(forKey: publicKey, inCollection: Storage.receivedCallsCollection) as? Set<String>
return result ?? []
}
public func setReceivedCalls(to receivedCalls: Set<String>, for publicKey: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(receivedCalls, forKey: publicKey, inCollection: Storage.receivedCallsCollection)
}
}

View file

@ -122,6 +122,7 @@ public final class ClosedGroupPoller : NSObject {
if !rawMessages.isEmpty {
SNLog("Received \(rawMessages.count) new message(s) in closed group with public key: \(groupPublicKey).")
}
var processedMessages: [JSON] = []
rawMessages.forEach { json in
guard let envelope = SNProtoEnvelope.from(json) else { return }
do {
@ -130,13 +131,15 @@ public final class ClosedGroupPoller : NSObject {
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}
processedMessages.append(json)
} catch {
SNLog("Failed to deserialize envelope due to error: \(error).")
}
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value & `receivedMessageHashes`
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: SnodeAPI.closedGroupNamespace, associatedWith: groupPublicKey, from: lastRawMessage)
SnodeAPI.updateReceivedMessages(from: processedMessages, associatedWith: groupPublicKey)
}
promise.catch2 { error in
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")

View file

@ -98,6 +98,7 @@ public final class Poller : NSObject {
if !messages.isEmpty {
SNLog("Received \(messages.count) new message(s).")
}
var processedMessages: [JSON] = []
messages.forEach { json in
guard let envelope = SNProtoEnvelope.from(json) else { return }
do {
@ -106,13 +107,15 @@ public final class Poller : NSObject {
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}
processedMessages.append(json)
} catch {
SNLog("Failed to deserialize envelope due to error: \(error).")
}
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value & `receivedMessageHashes`
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, namespace: SnodeAPI.defaultNamespace, associatedWith: userPublicKey, from: lastRawMessage)
SnodeAPI.updateReceivedMessages(from: processedMessages, associatedWith: userPublicKey)
strongSelf.pollCount += 1
if strongSelf.pollCount == Poller.maxPollCount {

View file

@ -96,6 +96,11 @@ public protocol SessionMessagingKitStorageProtocol {
func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsIncomingMessageID: String, using transaction: Any)
/// Also touches the associated message.
func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any)
// MARK: - Calls
func getReceivedCalls(for publicKey: String, using transaction: Any) -> Set<String>
func setReceivedCalls(to receivedCalls: Set<String>, for publicKey: String, using transaction: Any)
}
extension Storage: SessionMessagingKitStorageProtocol, SessionSnodeKitStorageProtocol {}

View file

@ -499,8 +499,7 @@ public final class SnodeAPI : NSObject {
// MARK: Store
public static func sendMessage(_ message: SnodeMessage, isClosedGroupMessage: Bool, isConfigMessage: Bool) -> Promise<Set<RawResponsePromise>> {
let namespace = isClosedGroupMessage ? closedGroupNamespace : defaultNamespace
return sendMessageUnauthenticated(message, namespace: namespace)
return sendMessageUnauthenticated(message, isClosedGroupMessage: isClosedGroupMessage)
}
// Not in use until we can batch delete and store config messages
@ -534,18 +533,33 @@ public final class SnodeAPI : NSObject {
return promise
}
private static func sendMessageUnauthenticated(_ message: SnodeMessage, namespace: Int) -> Promise<Set<RawResponsePromise>> {
private static func sendMessageUnauthenticated(_ message: SnodeMessage, isClosedGroupMessage: Bool) -> Promise<Set<RawResponsePromise>> {
let (promise, seal) = Promise<Set<RawResponsePromise>>.pending()
let publicKey = Features.useTestnet ? message.recipient.removing05PrefixIfNeeded() : message.recipient
Threading.workQueue.async {
getTargetSnodes(for: publicKey).map2 { targetSnodes in
var rawResponsePromises: Set<RawResponsePromise> = Set()
var parameters = message.toJSON()
parameters["namespace"] = namespace
return Set(targetSnodes.map { targetSnode in
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
parameters["namespace"] = isClosedGroupMessage ? closedGroupNamespace : defaultNamespace
for targetSnode in targetSnodes {
let rawResponsePromise = attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
}
})
rawResponsePromises.insert(rawResponsePromise)
}
// Send closed group messages to default namespace as well
if hardfork == 19 && softfork == 0 && isClosedGroupMessage {
parameters["namespace"] = defaultNamespace
for targetSnode in targetSnodes {
let rawResponsePromise = attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
}
rawResponsePromises.insert(rawResponsePromise)
}
}
return rawResponsePromises
}.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
}
return promise
@ -691,6 +705,20 @@ public final class SnodeAPI : NSObject {
}
}
public static func updateReceivedMessages(from messages: [JSON], associatedWith publicKey: String) {
let oldReceivedMessages = SNSnodeKitConfiguration.shared.storage.getReceivedMessages(for: publicKey)
var newReceivedMessages = oldReceivedMessages
for message in messages {
guard let hash = message["hash"] as? String else { continue }
newReceivedMessages.insert(hash)
}
if oldReceivedMessages != newReceivedMessages {
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNSnodeKitConfiguration.shared.storage.setReceivedMessages(to: newReceivedMessages, for: publicKey, using: transaction)
}
}
}
private static func removeDuplicates(from rawMessages: [JSON], associatedWith publicKey: String) -> [JSON] {
let oldReceivedMessages = SNSnodeKitConfiguration.shared.storage.getReceivedMessages(for: publicKey)
var newReceivedMessages = oldReceivedMessages
@ -703,12 +731,6 @@ public final class SnodeAPI : NSObject {
newReceivedMessages.insert(hash)
return !isDuplicate
}
// Avoid the sync write transaction if possible
if oldReceivedMessages != newReceivedMessages {
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNSnodeKitConfiguration.shared.storage.setReceivedMessages(to: newReceivedMessages, for: publicKey, using: transaction)
}
}
return result
}
@ -738,6 +760,9 @@ public final class SnodeAPI : NSObject {
case 500, 502, 503:
// The snode is unreachable
handleBadSnode()
case 404:
// May caused by invalid open groups
SNLog("Can't reach the server.")
case 406:
SNLog("The user's clock is out of sync with the service node network.")
return Error.clockOutOfSync

View file

@ -110,7 +110,6 @@ extension Storage {
if now >= expirationDate {
Storage.writeSync { transaction in
self.removeLastMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey, using: transaction)
self.setReceivedMessages(to: Set(), for: publicKey, using: transaction)
}
}
}