Various tweaks and fixes
Fixed an issue where the GlobalSearch push animation could be jittery Fixed a crash which could occur when returning from the background on certain screens Removed the keyboard dismiss animation when pushing from global search to a conversation (apparently this is how iMessage avoids the animation bug...) Updated to the latest version of GRDB Updated the Atomic wrapper to use the ReadWrite lock for less blocking behaviours Updated the audio attachment icon to be consistent with Android & Desktop Updated the QuoteView to omit the "author" if we don't have their name and the quote can't be found
This commit is contained in:
parent
4dfe243965
commit
5b5f4a4e88
|
@ -27,7 +27,7 @@ PODS:
|
|||
- DifferenceKit/Core (1.3.0)
|
||||
- DifferenceKit/UIKitExtension (1.3.0):
|
||||
- DifferenceKit/Core
|
||||
- GRDB.swift/SQLCipher (6.10.1):
|
||||
- GRDB.swift/SQLCipher (6.13.0):
|
||||
- SQLCipher (>= 3.4.2)
|
||||
- libwebp (1.2.1):
|
||||
- libwebp/demux (= 1.2.1)
|
||||
|
@ -222,7 +222,7 @@ SPEC CHECKSUMS:
|
|||
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
|
||||
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
|
||||
DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca
|
||||
GRDB.swift: 1cc67278f1a9878d6eb1b849485518112b79cab7
|
||||
GRDB.swift: fe420b1af49ec519c7e96e07887ee44f5dfa2b78
|
||||
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
|
||||
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
|
||||
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
|
||||
|
@ -242,6 +242,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: e9443a8235dbff1fc342aa9bf08bbc66923adf68
|
||||
PODFILE CHECKSUM: f2f07345491c3a64dd6a526e87381a0e46a231d2
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -169,7 +169,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
}()
|
||||
|
||||
lazy var snInputView: InputView = InputView(
|
||||
threadVariant: self.viewModel.threadData.threadVariant,
|
||||
threadVariant: self.viewModel.initialThreadVariant,
|
||||
delegate: self
|
||||
)
|
||||
|
||||
|
@ -180,6 +180,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
result.layer.cornerRadius = (ConversationVC.unreadCountViewSize / 2)
|
||||
result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize)
|
||||
result.set(.height, to: ConversationVC.unreadCountViewSize)
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -361,12 +362,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
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.scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16)
|
||||
|
||||
messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16)
|
||||
scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
|
||||
scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint
|
||||
scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16)
|
||||
scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16)
|
||||
|
||||
messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10)
|
||||
messageRequestBlockButton.center(.horizontal, in: messageRequestView)
|
||||
|
||||
|
@ -483,7 +484,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges(didReturnFromBackground: true)
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges(didReturnFromBackground: true)
|
||||
}
|
||||
|
||||
recoverInputView()
|
||||
|
||||
if !isShowingSearchUI {
|
||||
|
|
|
@ -53,27 +53,65 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
// MARK: - Initialization
|
||||
|
||||
init(threadId: String, threadVariant: SessionThread.Variant, focusedInteractionId: Int64?) {
|
||||
// If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest
|
||||
// unread interaction and start focused around that one
|
||||
let targetInteractionId: Int64? = {
|
||||
if let focusedInteractionId: Int64 = focusedInteractionId { return focusedInteractionId }
|
||||
typealias InitialData = (
|
||||
targetInteractionId: Int64?,
|
||||
currentUserIsClosedGroupMember: Bool?,
|
||||
openGroupPermissions: OpenGroup.Permissions?,
|
||||
blindedKey: String?
|
||||
)
|
||||
|
||||
let initialData: InitialData? = Storage.shared.read { db -> InitialData in
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
|
||||
|
||||
return Storage.shared.read { db in
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
return try Interaction
|
||||
// If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest
|
||||
// unread interaction and start focused around that one
|
||||
let targetInteractionId: Int64? = (focusedInteractionId != nil ? focusedInteractionId :
|
||||
try Interaction
|
||||
.select(.id)
|
||||
.filter(interaction[.wasRead] == false)
|
||||
.filter(interaction[.threadId] == threadId)
|
||||
.order(interaction[.timestampMs].asc)
|
||||
.asRequest(of: Int64.self)
|
||||
.fetchOne(db)
|
||||
}
|
||||
}()
|
||||
)
|
||||
let currentUserIsClosedGroupMember: Bool? = (threadVariant != .closedGroup ? nil :
|
||||
try GroupMember
|
||||
.filter(groupMember[.groupId] == threadId)
|
||||
.filter(groupMember[.profileId] == getUserHexEncodedPublicKey(db))
|
||||
.filter(groupMember[.role] == GroupMember.Role.standard)
|
||||
.isNotEmpty(db)
|
||||
)
|
||||
let openGroupPermissions: OpenGroup.Permissions? = (threadVariant != .openGroup ? nil :
|
||||
try OpenGroup
|
||||
.filter(id: threadId)
|
||||
.select(.permissions)
|
||||
.asRequest(of: OpenGroup.Permissions.self)
|
||||
.fetchOne(db)
|
||||
)
|
||||
let blindedKey: String? = SessionThread.getUserHexEncodedBlindedKey(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
|
||||
return (
|
||||
targetInteractionId,
|
||||
currentUserIsClosedGroupMember,
|
||||
openGroupPermissions,
|
||||
blindedKey
|
||||
)
|
||||
}
|
||||
|
||||
self.threadId = threadId
|
||||
self.initialThreadVariant = threadVariant
|
||||
self.focusedInteractionId = targetInteractionId
|
||||
self.focusedInteractionId = initialData?.targetInteractionId
|
||||
self.threadData = SessionThreadViewModel(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
currentUserIsClosedGroupMember: initialData?.currentUserIsClosedGroupMember,
|
||||
openGroupPermissions: initialData?.openGroupPermissions
|
||||
).populatingCurrentUserBlindedKey(currentUserBlindedPublicKeyForThisThread: initialData?.blindedKey)
|
||||
self.pagedDataObserver = nil
|
||||
|
||||
// Note: Since this references self we need to finish initializing before setting it, we
|
||||
|
@ -93,7 +131,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
// If we don't have a `initialFocusedId` then default to `.pageBefore` (it'll query
|
||||
// from a `0` offset)
|
||||
guard let initialFocusedId: Int64 = targetInteractionId else {
|
||||
guard let initialFocusedId: Int64 = initialData?.targetInteractionId else {
|
||||
self?.pagedDataObserver?.load(.pageBefore)
|
||||
return
|
||||
}
|
||||
|
@ -105,21 +143,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
// MARK: - Thread Data
|
||||
|
||||
/// This value is the current state of the view
|
||||
public private(set) lazy var threadData: SessionThreadViewModel = SessionThreadViewModel(
|
||||
threadId: self.threadId,
|
||||
threadVariant: self.initialThreadVariant,
|
||||
currentUserIsClosedGroupMember: (self.initialThreadVariant != .closedGroup ?
|
||||
nil :
|
||||
Storage.shared.read { db in
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == self.threadId)
|
||||
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
|
||||
.isNotEmpty(db)
|
||||
}
|
||||
)
|
||||
)
|
||||
.populatingCurrentUserBlindedKey()
|
||||
public private(set) var threadData: SessionThreadViewModel
|
||||
|
||||
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
|
||||
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance
|
||||
|
|
|
@ -37,8 +37,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
|
|||
set { inputTextView.selectedRange = newValue }
|
||||
}
|
||||
|
||||
var inputTextViewIsFirstResponder: Bool { inputTextView.isFirstResponder }
|
||||
|
||||
var enabledMessageTypes: MessageInputTypes = .all {
|
||||
didSet {
|
||||
setEnabledMessageTypes(enabledMessageTypes, message: nil)
|
||||
|
@ -440,10 +438,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
|
|||
override func resignFirstResponder() -> Bool {
|
||||
inputTextView.resignFirstResponder()
|
||||
}
|
||||
|
||||
func inputTextViewBecomeFirstResponder() {
|
||||
inputTextView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
// Not relevant in this case
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
final class QuoteView: UIView {
|
||||
static let thumbnailSize: CGFloat = 48
|
||||
|
@ -237,17 +238,27 @@ final class QuoteView: UIView {
|
|||
.compactMap { $0 }
|
||||
.asSet()
|
||||
.contains(authorId)
|
||||
|
||||
let authorLabel = UILabel()
|
||||
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
authorLabel.text = (isCurrentUser ?
|
||||
"MEDIA_GALLERY_SENDER_NAME_YOU".localized() :
|
||||
Profile.displayName(
|
||||
authorLabel.text = {
|
||||
guard !isCurrentUser else { return "MEDIA_GALLERY_SENDER_NAME_YOU".localized() }
|
||||
guard body != nil else {
|
||||
// When we can't find the quoted message we want to hide the author label
|
||||
return Profile.displayNameNoFallback(
|
||||
id: authorId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
|
||||
return Profile.displayName(
|
||||
id: authorId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
)
|
||||
}()
|
||||
authorLabel.themeTextColor = targetThemeColor
|
||||
authorLabel.lineBreakMode = .byTruncatingTail
|
||||
authorLabel.isHidden = (authorLabel.text == nil)
|
||||
|
||||
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)
|
||||
authorLabel.set(.height, to: authorLabelSize.height)
|
||||
|
|
|
@ -91,14 +91,17 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
setupNavigationBar()
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
searchBar.becomeFirstResponder()
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
searchBar.resignFirstResponder()
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
|
@ -138,10 +141,6 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
}
|
||||
}
|
||||
|
||||
private func reloadTableData() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: - Update Search Results
|
||||
|
||||
private func refreshSearchResults() {
|
||||
|
@ -155,9 +154,11 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
let searchText = rawSearchText.stripped
|
||||
|
||||
guard searchText.count > 0 else {
|
||||
guard searchText != (lastSearchText ?? "") else { return }
|
||||
|
||||
searchResultSet = defaultSearchResults
|
||||
lastSearchText = nil
|
||||
reloadTableData()
|
||||
tableView.reloadData()
|
||||
return
|
||||
}
|
||||
guard lastSearchText != searchText else { return }
|
||||
|
@ -212,7 +213,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
.compactMap { $0 }
|
||||
.flatMap { $0 }
|
||||
self?.isLoading = false
|
||||
self?.reloadTableData()
|
||||
self?.tableView.reloadData()
|
||||
self?.refreshTimer = nil
|
||||
|
||||
default: break
|
||||
|
@ -283,18 +284,12 @@ extension GlobalSearchViewController {
|
|||
return
|
||||
}
|
||||
|
||||
if let presentedVC = self.presentedViewController {
|
||||
presentedVC.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
let viewControllers: [UIViewController] = (self.navigationController?
|
||||
.viewControllers)
|
||||
.defaulting(to: [])
|
||||
.appending(
|
||||
ConversationVC(threadId: threadId, threadVariant: threadVariant, focusedInteractionId: focusedInteractionId)
|
||||
)
|
||||
|
||||
self.navigationController?.setViewControllers(viewControllers, animated: true)
|
||||
let viewController: ConversationVC = ConversationVC(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
focusedInteractionId: focusedInteractionId
|
||||
)
|
||||
self.navigationController?.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
|
|
@ -308,7 +308,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges(didReturnFromBackground: true)
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges(didReturnFromBackground: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
@ -393,8 +396,18 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
// in from a frame of CGRect.zero)
|
||||
guard hasLoadedInitialThreadData else {
|
||||
hasLoadedInitialThreadData = true
|
||||
UIView.performWithoutAnimation {
|
||||
handleThreadUpdates(updatedData, changeset: changeset, initialLoad: true)
|
||||
|
||||
UIView.performWithoutAnimation { [weak self] in
|
||||
// Hide the 'loading conversations' label (now that we have received conversation data)
|
||||
self?.loadingConversationsLabel.isHidden = true
|
||||
|
||||
// Show the empty state if there is no data
|
||||
self?.emptyStateView.isHidden = (
|
||||
!updatedData.isEmpty &&
|
||||
updatedData.contains(where: { !$0.elements.isEmpty })
|
||||
)
|
||||
|
||||
self?.viewModel.updateThreadData(updatedData)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -162,7 +162,10 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges(didReturnFromBackground: true)
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges(didReturnFromBackground: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
|
|
@ -119,7 +119,10 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate,
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges()
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
extension MediaInfoVC {
|
||||
final class MediaInfoView: UIView {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
extension MediaInfoVC {
|
||||
final class MediaPreviewView: UIView {
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class MediaInfoVC: BaseVC, SessionCarouselViewDelegate {
|
||||
internal static let mediaSize: CGFloat = UIScreen.main.bounds.width - 2 * Values.veryLargeSpacing
|
||||
|
|
|
@ -245,7 +245,10 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges()
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
|
|
@ -175,7 +175,10 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges(didReturnFromBackground: true)
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges(didReturnFromBackground: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
|
|
@ -446,19 +446,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
|
||||
guard CurrentAppContext().isMainApp else { return }
|
||||
|
||||
CurrentAppContext().setMainAppBadgeNumber(
|
||||
Storage.shared
|
||||
/// On application startup the `Storage.read` can be slightly slow while GRDB spins up it's database
|
||||
/// read pools (up to a few seconds), since this read is blocking we want to dispatch it to run async to ensure
|
||||
/// we don't block user interaction while it's running
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
let unreadCount: Int = Storage.shared
|
||||
.read { db in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
|
||||
return try Interaction
|
||||
.filter(Interaction.Columns.wasRead == false)
|
||||
.filter(
|
||||
// Exclude outgoing and deleted messages from the count
|
||||
Interaction.Columns.variant != Interaction.Variant.standardOutgoing &&
|
||||
Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted
|
||||
)
|
||||
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
|
||||
.filter(
|
||||
// Only count mentions if 'onlyNotifyForMentions' is set
|
||||
thread[.onlyNotifyForMentions] == false ||
|
||||
|
@ -482,7 +481,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
.fetchCount(db)
|
||||
}
|
||||
.defaulting(to: 0)
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
CurrentAppContext().setMainAppBadgeNumber(unreadCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 369 B |
Binary file not shown.
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 628 B |
Binary file not shown.
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 893 B |
|
@ -218,9 +218,21 @@ final class PathVC: BaseVC {
|
|||
}
|
||||
|
||||
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
|
||||
let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..."
|
||||
let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "")
|
||||
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
|
||||
let country: String = (IP2Country.isInitialized ?
|
||||
IP2Country.shared.countryNamesCache.wrappedValue[snode.ip].defaulting(to: "Resolving...") :
|
||||
"Resolving..."
|
||||
)
|
||||
|
||||
return getPathRow(
|
||||
title: (isGuardSnode ?
|
||||
"vc_path_guard_node_row_title".localized() :
|
||||
"vc_path_service_node_row_title".localized()
|
||||
),
|
||||
subtitle: country,
|
||||
location: location,
|
||||
dotAnimationStartDelay: dotAnimationStartDelay,
|
||||
dotAnimationRepeatInterval: dotAnimationRepeatInterval
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
|
|
@ -145,7 +145,10 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges(didReturnFromBackground: true)
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges(didReturnFromBackground: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
|
|
@ -132,7 +132,10 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges()
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
|
|
@ -3,16 +3,17 @@ import GRDB
|
|||
import SessionSnodeKit
|
||||
|
||||
final class IP2Country {
|
||||
var countryNamesCache: [String:String] = [:]
|
||||
var countryNamesCache: Atomic<[String: String]> = Atomic([:])
|
||||
|
||||
|
||||
private static let workQueue = DispatchQueue(label: "IP2Country.workQueue", qos: .utility) // It's important that this is a serial queue
|
||||
static var isInitialized = false
|
||||
|
||||
// MARK: Tables
|
||||
/// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains the **lower** bound of an IP
|
||||
/// range and the "registered_country_geoname_id" column contains the ID of the country corresponding to that range. We look up an IP by finding the first index in the
|
||||
/// network column where the value is greater than the IP we're looking up (converted to an integer). The IP we're looking up must then be in the range **before** that
|
||||
/// range.
|
||||
/// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains
|
||||
/// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding
|
||||
/// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking
|
||||
/// up (converted to an integer). The IP we're looking up must then be in the range **before** that range.
|
||||
private lazy var ipv4Table: [String:[Int]] = {
|
||||
let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)!
|
||||
let data = try! Data(contentsOf: url)
|
||||
|
@ -36,15 +37,23 @@ final class IP2Country {
|
|||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Implementation
|
||||
private func cacheCountry(for ip: String) -> String {
|
||||
if let result = countryNamesCache[ip] { return result }
|
||||
let ipAsInt = IPv4.toInt(ip)
|
||||
guard let ipv4TableIndex = ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted
|
||||
let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex]
|
||||
guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" }
|
||||
let result = countryNamesTable["country_name"]![countryNamesTableIndex]
|
||||
countryNamesCache[ip] = result
|
||||
// MARK: - Implementation
|
||||
|
||||
@discardableResult private func cacheCountry(for ip: String, inCache cache: inout [String: String]) -> String {
|
||||
if let result: String = cache[ip] { return result }
|
||||
|
||||
let ipAsInt: Int = IPv4.toInt(ip)
|
||||
|
||||
guard
|
||||
let ipv4TableIndex = ipv4Table["network"]?.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }),
|
||||
let countryID: Int = ipv4Table["registered_country_geoname_id"]?[ipv4TableIndex],
|
||||
let countryNamesTableIndex = countryNamesTable["geoname_id"]?.firstIndex(of: String(countryID)),
|
||||
let result: String = countryNamesTable["country_name"]?[countryNamesTableIndex]
|
||||
else {
|
||||
return "Unknown Country" // Relies on the array being sorted
|
||||
}
|
||||
|
||||
cache[ip] = result
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -58,9 +67,12 @@ final class IP2Country {
|
|||
func populateCacheIfNeeded() -> Bool {
|
||||
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false }
|
||||
|
||||
pathToDisplay.forEach { snode in
|
||||
let _ = self.cacheCountry(for: snode.ip) // Preload if needed
|
||||
countryNamesCache.mutate { [weak self] cache in
|
||||
pathToDisplay.forEach { snode in
|
||||
self?.cacheCountry(for: snode.ip, inCache: &cache) // Preload if needed
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
IP2Country.isInitialized = true
|
||||
NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil)
|
||||
|
|
|
@ -87,6 +87,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
|
||||
// MARK: - Convenience
|
||||
|
||||
public static let variantsToIncrementUnreadCount: [Variant] = [
|
||||
.standardIncoming, .infoCall
|
||||
]
|
||||
|
||||
public var isInfoMessage: Bool {
|
||||
switch self {
|
||||
case .infoClosedGroupCreated, .infoClosedGroupUpdated,
|
||||
|
|
|
@ -100,8 +100,14 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
|||
public var canWrite: Bool {
|
||||
switch threadVariant {
|
||||
case .contact: return true
|
||||
case .closedGroup: return (currentUserIsClosedGroupMember == true) && (interactionVariant?.isGroupLeavingStatus != true)
|
||||
case .openGroup: return openGroupPermissions?.contains(.write) ?? false
|
||||
case .closedGroup:
|
||||
return (
|
||||
currentUserIsClosedGroupMember == true &&
|
||||
interactionVariant?.isGroupLeavingStatus != true
|
||||
)
|
||||
|
||||
case .openGroup:
|
||||
return (openGroupPermissions?.contains(.write) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,6 +247,7 @@ public extension SessionThreadViewModel {
|
|||
threadIsNoteToSelf: Bool = false,
|
||||
contactProfile: Profile? = nil,
|
||||
currentUserIsClosedGroupMember: Bool? = nil,
|
||||
openGroupPermissions: OpenGroup.Permissions? = nil,
|
||||
unreadCount: UInt = 0
|
||||
) {
|
||||
self.rowId = -1
|
||||
|
@ -279,7 +286,7 @@ public extension SessionThreadViewModel {
|
|||
self.openGroupPublicKey = nil
|
||||
self.openGroupProfilePictureData = nil
|
||||
self.openGroupUserCount = nil
|
||||
self.openGroupPermissions = nil
|
||||
self.openGroupPermissions = openGroupPermissions
|
||||
|
||||
// Interaction display info
|
||||
|
||||
|
|
|
@ -85,7 +85,10 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges()
|
||||
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.startObservingChanges()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
@ -6,23 +6,28 @@ import Foundation
|
|||
|
||||
/// The `Atomic<Value>` wrapper is a generic wrapper providing a thread-safe way to get and set a value
|
||||
///
|
||||
/// A write-up on the need for this class and it's approach can be found here:
|
||||
/// A write-up on the need for this class and it's approaches can be found at these links:
|
||||
/// https://www.vadimbulavin.com/atomic-properties/
|
||||
/// https://www.vadimbulavin.com/swift-atomic-properties-with-property-wrappers/
|
||||
/// there is also another approach which can be taken but it requires separate types for collections and results in
|
||||
/// a somewhat inconsistent interface between different `Atomic` wrappers
|
||||
///
|
||||
/// We use a Read-write lock approach because the `DispatchQueue` approach means mutating the property
|
||||
/// occurs on a different thread, and GRDB requires it's changes to be executed on specific threads so using a lock
|
||||
/// is more compatible (and Read-write locks allow for concurrent reads which shouldn't be a huge issue but could
|
||||
/// help reduce cases of blocking)
|
||||
@propertyWrapper
|
||||
public class Atomic<Value> {
|
||||
// Note: Using 'userInteractive' to ensure this can't be blockedby higher priority queues
|
||||
// which could result in the main thread getting blocked
|
||||
private let queue: DispatchQueue = DispatchQueue(
|
||||
label: "io.oxen.\(UUID().uuidString)",
|
||||
qos: .userInteractive
|
||||
)
|
||||
private var value: Value
|
||||
private let lock: ReadWriteLock = ReadWriteLock()
|
||||
|
||||
/// In order to change the value you **must** use the `mutate` function
|
||||
public var wrappedValue: Value {
|
||||
return queue.sync { return value }
|
||||
lock.readLock()
|
||||
let result: Value = value
|
||||
lock.unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections
|
||||
|
@ -36,12 +41,34 @@ public class Atomic<Value> {
|
|||
self.value = initialValue
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value) {
|
||||
self.value = wrappedValue
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
@discardableResult public func mutate<T>(_ mutation: (inout Value) -> T) -> T {
|
||||
return queue.sync {
|
||||
return mutation(&value)
|
||||
lock.writeLock()
|
||||
let result: T = mutation(&value)
|
||||
lock.unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@discardableResult public func mutate<T>(_ mutation: (inout Value) throws -> T) throws -> T {
|
||||
let result: T
|
||||
|
||||
do {
|
||||
lock.writeLock()
|
||||
result = try mutation(&value)
|
||||
lock.unlock()
|
||||
}
|
||||
catch {
|
||||
lock.unlock()
|
||||
throw error
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,3 +77,25 @@ extension Atomic where Value: CustomDebugStringConvertible {
|
|||
return value.debugDescription
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ReadWriteLock
|
||||
|
||||
private class ReadWriteLock {
|
||||
private var rwlock: pthread_rwlock_t = {
|
||||
var rwlock = pthread_rwlock_t()
|
||||
pthread_rwlock_init(&rwlock, nil)
|
||||
return rwlock
|
||||
}()
|
||||
|
||||
func writeLock() {
|
||||
pthread_rwlock_wrlock(&rwlock)
|
||||
}
|
||||
|
||||
func readLock() {
|
||||
pthread_rwlock_rdlock(&rwlock)
|
||||
}
|
||||
|
||||
func unlock() {
|
||||
pthread_rwlock_unlock(&rwlock)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue