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
pull/850/head
Morgan Pretty 1 year ago
parent 4dfe243965
commit 5b5f4a4e88

@ -27,7 +27,7 @@ PODS:
- DifferenceKit/Core (1.3.0) - DifferenceKit/Core (1.3.0)
- DifferenceKit/UIKitExtension (1.3.0): - DifferenceKit/UIKitExtension (1.3.0):
- DifferenceKit/Core - DifferenceKit/Core
- GRDB.swift/SQLCipher (6.10.1): - GRDB.swift/SQLCipher (6.13.0):
- SQLCipher (>= 3.4.2) - SQLCipher (>= 3.4.2)
- libwebp (1.2.1): - libwebp (1.2.1):
- libwebp/demux (= 1.2.1) - libwebp/demux (= 1.2.1)
@ -222,7 +222,7 @@ SPEC CHECKSUMS:
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17 CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6 Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca
GRDB.swift: 1cc67278f1a9878d6eb1b849485518112b79cab7 GRDB.swift: fe420b1af49ec519c7e96e07887ee44f5dfa2b78
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84 Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
@ -242,6 +242,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: e9443a8235dbff1fc342aa9bf08bbc66923adf68 PODFILE CHECKSUM: f2f07345491c3a64dd6a526e87381a0e46a231d2
COCOAPODS: 1.11.3 COCOAPODS: 1.11.3

@ -169,7 +169,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
}() }()
lazy var snInputView: InputView = InputView( lazy var snInputView: InputView = InputView(
threadVariant: self.viewModel.threadData.threadVariant, threadVariant: self.viewModel.initialThreadVariant,
delegate: self delegate: self
) )
@ -180,6 +180,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
result.layer.cornerRadius = (ConversationVC.unreadCountViewSize / 2) result.layer.cornerRadius = (ConversationVC.unreadCountViewSize / 2)
result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize) result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize)
result.set(.height, to: ConversationVC.unreadCountViewSize) result.set(.height, to: ConversationVC.unreadCountViewSize)
result.isHidden = true
return result return result
}() }()
@ -361,12 +362,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
scrollButton.pin(.right, to: .right, of: view, withInset: -20) scrollButton.pin(.right, to: .right, of: view, withInset: -20)
messageRequestView.pin(.left, to: .left, of: view) messageRequestView.pin(.left, to: .left, of: view)
messageRequestView.pin(.right, to: .right, of: view) messageRequestView.pin(.right, to: .right, of: view)
self.messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16) messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16)
self.scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16) 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 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) scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16)
self.scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16) scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16)
messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10) messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10)
messageRequestBlockButton.center(.horizontal, in: messageRequestView) messageRequestBlockButton.center(.horizontal, in: messageRequestView)
@ -483,7 +484,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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() recoverInputView()
if !isShowingSearchUI { if !isShowingSearchUI {

@ -53,27 +53,65 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// MARK: - Initialization // MARK: - Initialization
init(threadId: String, threadVariant: SessionThread.Variant, focusedInteractionId: Int64?) { init(threadId: String, threadVariant: SessionThread.Variant, focusedInteractionId: Int64?) {
// If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest typealias InitialData = (
// unread interaction and start focused around that one targetInteractionId: Int64?,
let targetInteractionId: Int64? = { currentUserIsClosedGroupMember: Bool?,
if let focusedInteractionId: Int64 = focusedInteractionId { return focusedInteractionId } 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 // If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() // unread interaction and start focused around that one
let targetInteractionId: Int64? = (focusedInteractionId != nil ? focusedInteractionId :
return try Interaction try Interaction
.select(.id) .select(.id)
.filter(interaction[.wasRead] == false) .filter(interaction[.wasRead] == false)
.filter(interaction[.threadId] == threadId) .filter(interaction[.threadId] == threadId)
.order(interaction[.timestampMs].asc) .order(interaction[.timestampMs].asc)
.asRequest(of: Int64.self) .asRequest(of: Int64.self)
.fetchOne(db) .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.threadId = threadId
self.initialThreadVariant = threadVariant 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 self.pagedDataObserver = nil
// Note: Since this references self we need to finish initializing before setting it, we // 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 DispatchQueue.global(qos: .userInitiated).async { [weak self] in
// If we don't have a `initialFocusedId` then default to `.pageBefore` (it'll query // If we don't have a `initialFocusedId` then default to `.pageBefore` (it'll query
// from a `0` offset) // from a `0` offset)
guard let initialFocusedId: Int64 = targetInteractionId else { guard let initialFocusedId: Int64 = initialData?.targetInteractionId else {
self?.pagedDataObserver?.load(.pageBefore) self?.pagedDataObserver?.load(.pageBefore)
return return
} }
@ -105,21 +143,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// MARK: - Thread Data // MARK: - Thread Data
/// This value is the current state of the view /// This value is the current state of the view
public private(set) lazy var threadData: SessionThreadViewModel = SessionThreadViewModel( public private(set) var threadData: 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()
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise /// 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 /// performance https://github.com/groue/GRDB.swift#valueobservation-performance

@ -37,8 +37,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
set { inputTextView.selectedRange = newValue } set { inputTextView.selectedRange = newValue }
} }
var inputTextViewIsFirstResponder: Bool { inputTextView.isFirstResponder }
var enabledMessageTypes: MessageInputTypes = .all { var enabledMessageTypes: MessageInputTypes = .all {
didSet { didSet {
setEnabledMessageTypes(enabledMessageTypes, message: nil) setEnabledMessageTypes(enabledMessageTypes, message: nil)
@ -440,10 +438,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
override func resignFirstResponder() -> Bool { override func resignFirstResponder() -> Bool {
inputTextView.resignFirstResponder() inputTextView.resignFirstResponder()
} }
func inputTextViewBecomeFirstResponder() {
inputTextView.becomeFirstResponder()
}
func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {
// Not relevant in this case // Not relevant in this case

@ -3,6 +3,7 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit
final class QuoteView: UIView { final class QuoteView: UIView {
static let thumbnailSize: CGFloat = 48 static let thumbnailSize: CGFloat = 48
@ -237,17 +238,27 @@ final class QuoteView: UIView {
.compactMap { $0 } .compactMap { $0 }
.asSet() .asSet()
.contains(authorId) .contains(authorId)
let authorLabel = UILabel() let authorLabel = UILabel()
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
authorLabel.text = (isCurrentUser ? authorLabel.text = {
"MEDIA_GALLERY_SENDER_NAME_YOU".localized() : guard !isCurrentUser else { return "MEDIA_GALLERY_SENDER_NAME_YOU".localized() }
Profile.displayName( 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, id: authorId,
threadVariant: threadVariant threadVariant: threadVariant
) )
) }()
authorLabel.themeTextColor = targetThemeColor authorLabel.themeTextColor = targetThemeColor
authorLabel.lineBreakMode = .byTruncatingTail authorLabel.lineBreakMode = .byTruncatingTail
authorLabel.isHidden = (authorLabel.text == nil)
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace) let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)
authorLabel.set(.height, to: authorLabelSize.height) authorLabel.set(.height, to: authorLabelSize.height)

@ -91,14 +91,17 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
setupNavigationBar() setupNavigationBar()
} }
public override func viewWillAppear(_ animated: Bool) { public override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewDidAppear(animated)
searchBar.becomeFirstResponder() searchBar.becomeFirstResponder()
} }
public override func viewWillDisappear(_ animated: Bool) { public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
searchBar.resignFirstResponder()
UIView.performWithoutAnimation {
searchBar.resignFirstResponder()
}
} }
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 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 // MARK: - Update Search Results
private func refreshSearchResults() { private func refreshSearchResults() {
@ -155,9 +154,11 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
let searchText = rawSearchText.stripped let searchText = rawSearchText.stripped
guard searchText.count > 0 else { guard searchText.count > 0 else {
guard searchText != (lastSearchText ?? "") else { return }
searchResultSet = defaultSearchResults searchResultSet = defaultSearchResults
lastSearchText = nil lastSearchText = nil
reloadTableData() tableView.reloadData()
return return
} }
guard lastSearchText != searchText else { return } guard lastSearchText != searchText else { return }
@ -212,7 +213,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
.compactMap { $0 } .compactMap { $0 }
.flatMap { $0 } .flatMap { $0 }
self?.isLoading = false self?.isLoading = false
self?.reloadTableData() self?.tableView.reloadData()
self?.refreshTimer = nil self?.refreshTimer = nil
default: break default: break
@ -283,18 +284,12 @@ extension GlobalSearchViewController {
return return
} }
if let presentedVC = self.presentedViewController { let viewController: ConversationVC = ConversationVC(
presentedVC.dismiss(animated: false, completion: nil) threadId: threadId,
} threadVariant: threadVariant,
focusedInteractionId: focusedInteractionId
let viewControllers: [UIViewController] = (self.navigationController? )
.viewControllers) self.navigationController?.pushViewController(viewController, animated: true)
.defaulting(to: [])
.appending(
ConversationVC(threadId: threadId, threadVariant: threadVariant, focusedInteractionId: focusedInteractionId)
)
self.navigationController?.setViewControllers(viewControllers, animated: true)
} }
// MARK: - UITableViewDataSource // MARK: - UITableViewDataSource

@ -308,7 +308,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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) { @objc func applicationDidResignActive(_ notification: Notification) {
@ -393,8 +396,18 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
// in from a frame of CGRect.zero) // in from a frame of CGRect.zero)
guard hasLoadedInitialThreadData else { guard hasLoadedInitialThreadData else {
hasLoadedInitialThreadData = true 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 return
} }

@ -162,7 +162,10 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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) { @objc func applicationDidResignActive(_ notification: Notification) {

@ -119,7 +119,10 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate,
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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) { @objc func applicationDidResignActive(_ notification: Notification) {

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

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

@ -2,7 +2,9 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SignalUtilitiesKit
final class MediaInfoVC: BaseVC, SessionCarouselViewDelegate { final class MediaInfoVC: BaseVC, SessionCarouselViewDelegate {
internal static let mediaSize: CGFloat = UIScreen.main.bounds.width - 2 * Values.veryLargeSpacing 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) { @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) { @objc func applicationDidResignActive(_ notification: Notification) {

@ -175,7 +175,10 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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) { @objc func applicationDidResignActive(_ notification: Notification) {

@ -446,19 +446,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
guard CurrentAppContext().isMainApp else { return } guard CurrentAppContext().isMainApp else { return }
CurrentAppContext().setMainAppBadgeNumber( /// On application startup the `Storage.read` can be slightly slow while GRDB spins up it's database
Storage.shared /// 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 .read { db in
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return try Interaction return try Interaction
.filter(Interaction.Columns.wasRead == false) .filter(Interaction.Columns.wasRead == false)
.filter( .filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
// Exclude outgoing and deleted messages from the count
Interaction.Columns.variant != Interaction.Variant.standardOutgoing &&
Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted
)
.filter( .filter(
// Only count mentions if 'onlyNotifyForMentions' is set // Only count mentions if 'onlyNotifyForMentions' is set
thread[.onlyNotifyForMentions] == false || thread[.onlyNotifyForMentions] == false ||
@ -482,7 +481,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
.fetchCount(db) .fetchCount(db)
} }
.defaulting(to: 0) .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 { 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 country: String = (IP2Country.isInitialized ?
let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "") IP2Country.shared.countryNamesCache.wrappedValue[snode.ip].defaulting(to: "Resolving...") :
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval) "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 // MARK: - Interaction

@ -145,7 +145,10 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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) { @objc func applicationDidResignActive(_ notification: Notification) {

@ -132,7 +132,10 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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) { @objc func applicationDidResignActive(_ notification: Notification) {

@ -3,16 +3,17 @@ import GRDB
import SessionSnodeKit import SessionSnodeKit
final class IP2Country { 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 private static let workQueue = DispatchQueue(label: "IP2Country.workQueue", qos: .utility) // It's important that this is a serial queue
static var isInitialized = false static var isInitialized = false
// MARK: Tables // 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 /// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains
/// 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 /// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding
/// 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 /// 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
/// range. /// 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]] = { private lazy var ipv4Table: [String:[Int]] = {
let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)! let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)!
let data = try! Data(contentsOf: url) let data = try! Data(contentsOf: url)
@ -36,15 +37,23 @@ final class IP2Country {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
// MARK: Implementation // MARK: - Implementation
private func cacheCountry(for ip: String) -> String {
if let result = countryNamesCache[ip] { return result } @discardableResult private func cacheCountry(for ip: String, inCache cache: inout [String: String]) -> String {
let ipAsInt = IPv4.toInt(ip) if let result: String = cache[ip] { return result }
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] let ipAsInt: Int = IPv4.toInt(ip)
guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" }
let result = countryNamesTable["country_name"]![countryNamesTableIndex] guard
countryNamesCache[ip] = result 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 return result
} }
@ -58,9 +67,12 @@ final class IP2Country {
func populateCacheIfNeeded() -> Bool { func populateCacheIfNeeded() -> Bool {
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false } guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false }
pathToDisplay.forEach { snode in countryNamesCache.mutate { [weak self] cache in
let _ = self.cacheCountry(for: snode.ip) // Preload if needed pathToDisplay.forEach { snode in
self?.cacheCountry(for: snode.ip, inCache: &cache) // Preload if needed
}
} }
DispatchQueue.main.async { DispatchQueue.main.async {
IP2Country.isInitialized = true IP2Country.isInitialized = true
NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil) NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil)

@ -87,6 +87,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
// MARK: - Convenience // MARK: - Convenience
public static let variantsToIncrementUnreadCount: [Variant] = [
.standardIncoming, .infoCall
]
public var isInfoMessage: Bool { public var isInfoMessage: Bool {
switch self { switch self {
case .infoClosedGroupCreated, .infoClosedGroupUpdated, case .infoClosedGroupCreated, .infoClosedGroupUpdated,

@ -100,8 +100,14 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public var canWrite: Bool { public var canWrite: Bool {
switch threadVariant { switch threadVariant {
case .contact: return true case .contact: return true
case .closedGroup: return (currentUserIsClosedGroupMember == true) && (interactionVariant?.isGroupLeavingStatus != true) case .closedGroup:
case .openGroup: return openGroupPermissions?.contains(.write) ?? false return (
currentUserIsClosedGroupMember == true &&
interactionVariant?.isGroupLeavingStatus != true
)
case .openGroup:
return (openGroupPermissions?.contains(.write) ?? false)
} }
} }
@ -241,6 +247,7 @@ public extension SessionThreadViewModel {
threadIsNoteToSelf: Bool = false, threadIsNoteToSelf: Bool = false,
contactProfile: Profile? = nil, contactProfile: Profile? = nil,
currentUserIsClosedGroupMember: Bool? = nil, currentUserIsClosedGroupMember: Bool? = nil,
openGroupPermissions: OpenGroup.Permissions? = nil,
unreadCount: UInt = 0 unreadCount: UInt = 0
) { ) {
self.rowId = -1 self.rowId = -1
@ -279,7 +286,7 @@ public extension SessionThreadViewModel {
self.openGroupPublicKey = nil self.openGroupPublicKey = nil
self.openGroupProfilePictureData = nil self.openGroupProfilePictureData = nil
self.openGroupUserCount = nil self.openGroupUserCount = nil
self.openGroupPermissions = nil self.openGroupPermissions = openGroupPermissions
// Interaction display info // Interaction display info

@ -85,7 +85,10 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
} }
@objc func applicationDidBecomeActive(_ notification: Notification) { @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) { @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 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 /// 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/ /// 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 /// 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 /// 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 @propertyWrapper
public class Atomic<Value> { 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 var value: Value
private let lock: ReadWriteLock = ReadWriteLock()
/// In order to change the value you **must** use the `mutate` function /// In order to change the value you **must** use the `mutate` function
public var wrappedValue: Value { 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 /// 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 self.value = initialValue
} }
public init(wrappedValue: Value) {
self.value = wrappedValue
}
// MARK: - Functions // MARK: - Functions
@discardableResult public func mutate<T>(_ mutation: (inout Value) -> T) -> T { @discardableResult public func mutate<T>(_ mutation: (inout Value) -> T) -> T {
return queue.sync { lock.writeLock()
return mutation(&value) 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 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