Merge pull request #850 from mpretty-cyro/fix/general-bug-fixes

Various tweaks and fixes
pull/854/head
Morgan Pretty 2 years ago committed by GitHub
commit e061bb02cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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…
Cancel
Save