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:
Morgan Pretty 2023-05-16 09:38:14 +10:00
parent 4dfe243965
commit 5b5f4a4e88
26 changed files with 275 additions and 120 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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) {

View File

@ -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) {

View File

@ -3,6 +3,8 @@
import UIKit
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
import SignalUtilitiesKit
extension MediaInfoVC {
final class MediaInfoView: UIView {

View File

@ -3,6 +3,7 @@
import UIKit
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
extension MediaInfoVC {
final class MediaPreviewView: UIView {

View File

@ -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

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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) {

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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) {

View File

@ -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)
}
}