mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge pull request #777 from mpretty-cyro/fix/url-detection
Fixed a number of bugs
This commit is contained in:
commit
cd9a6eb7a1
13 changed files with 141 additions and 34 deletions
|
@ -318,12 +318,20 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
}
|
||||
}()
|
||||
|
||||
guard shouldMarkAsRead else { return }
|
||||
guard
|
||||
shouldMarkAsRead,
|
||||
let threadVariant: SessionThread.Variant = try? SessionThread
|
||||
.filter(id: interaction.threadId)
|
||||
.select(.variant)
|
||||
.asRequest(of: SessionThread.Variant.self)
|
||||
.fetchOne(db)
|
||||
else { return }
|
||||
|
||||
try Interaction.markAsRead(
|
||||
db,
|
||||
interactionId: interaction.id,
|
||||
threadId: interaction.threadId,
|
||||
threadVariant: threadVariant,
|
||||
includingOlder: false,
|
||||
trySendReadReceipt: false
|
||||
)
|
||||
|
|
|
@ -110,6 +110,7 @@ extension ContextMenuVC {
|
|||
static func actions(
|
||||
for cellViewModel: MessageViewModel,
|
||||
recentEmojis: [EmojiWithSkinTones],
|
||||
currentUserPublicKey: String,
|
||||
currentUserIsOpenGroupModerator: Bool,
|
||||
currentThreadIsMessageRequest: Bool,
|
||||
delegate: ContextMenuActionDelegate?
|
||||
|
@ -163,6 +164,7 @@ extension ContextMenuVC {
|
|||
let canDelete: Bool = (
|
||||
cellViewModel.threadVariant != .openGroup ||
|
||||
currentUserIsOpenGroupModerator ||
|
||||
cellViewModel.authorId == currentUserPublicKey ||
|
||||
cellViewModel.state == .failed
|
||||
)
|
||||
let canBan: Bool = (
|
||||
|
|
|
@ -777,6 +777,7 @@ extension ConversationVC:
|
|||
let actions: [ContextMenuVC.Action] = ContextMenuVC.actions(
|
||||
for: cellViewModel,
|
||||
recentEmojis: (self.viewModel.threadData.recentReactionEmoji ?? []).compactMap { EmojiWithSkinTones(rawValue: $0) },
|
||||
currentUserPublicKey: self.viewModel.threadData.currentUserPublicKey,
|
||||
currentUserIsOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin(
|
||||
self.viewModel.threadData.currentUserPublicKey,
|
||||
for: self.viewModel.threadData.openGroupRoomToken,
|
||||
|
|
|
@ -737,11 +737,17 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
|
||||
// Store the 'sentMessageBeforeUpdate' state locally
|
||||
let didSendMessageBeforeUpdate: Bool = self.viewModel.sentMessageBeforeUpdate
|
||||
let wasOnlyUpdates: Bool = (
|
||||
changeset.count == 1 &&
|
||||
changeset[0].elementUpdated.count == changeset[0].changeCount
|
||||
)
|
||||
self.viewModel.sentMessageBeforeUpdate = false
|
||||
|
||||
// When sending a message we want to reload the UI instantly (with any form of animation the message
|
||||
// sending feels somewhat unresponsive but an instant update feels snappy)
|
||||
guard !didSendMessageBeforeUpdate else {
|
||||
// When sending a message, or if there were only cell updates (ie. read status changes) we want to
|
||||
// reload the UI instantly (with any form of animation the message sending feels somewhat unresponsive
|
||||
// but an instant update feels snappy and without the instant update there is some overlap of the read
|
||||
// status text change even though there shouldn't be any animations)
|
||||
guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else {
|
||||
self.viewModel.updateInteractionData(updatedData)
|
||||
self.tableView.reloadData()
|
||||
|
||||
|
|
|
@ -197,7 +197,17 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
|
||||
return SQL("LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])")
|
||||
}()
|
||||
)
|
||||
),
|
||||
PagedData.ObservedChanges(
|
||||
table: RecipientState.self,
|
||||
columns: [.state, .mostRecentFailureText],
|
||||
joinToPagedType: {
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
|
||||
|
||||
return SQL("LEFT JOIN \(RecipientState.self) ON \(recipientState[.interactionId]) = \(interaction[.id])")
|
||||
}()
|
||||
),
|
||||
],
|
||||
filterSQL: MessageViewModel.filterSQL(threadId: threadId),
|
||||
groupSQL: MessageViewModel.groupSQL,
|
||||
|
@ -405,6 +415,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
else { return }
|
||||
|
||||
let threadId: String = self.threadData.threadId
|
||||
let threadVariant: SessionThread.Variant = self.threadData.threadVariant
|
||||
let trySendReadReceipt: Bool = (self.threadData.threadIsMessageRequest == false)
|
||||
self.lastInteractionIdMarkedAsRead = targetInteractionId
|
||||
|
||||
|
@ -413,6 +424,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
db,
|
||||
interactionId: targetInteractionId,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
includingOlder: true,
|
||||
trySendReadReceipt: trySendReadReceipt
|
||||
)
|
||||
|
|
|
@ -165,6 +165,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
return result
|
||||
}()
|
||||
|
||||
internal lazy var messageStatusLabelPaddingView: UIView = UIView()
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
|
@ -252,6 +254,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
underBubbleStackView.addArrangedSubview(reactionContainerView)
|
||||
underBubbleStackView.addArrangedSubview(messageStatusContainerView)
|
||||
underBubbleStackView.addArrangedSubview(messageStatusLabelPaddingView)
|
||||
|
||||
messageStatusContainerView.addSubview(messageStatusLabel)
|
||||
messageStatusContainerView.addSubview(messageStatusImageView)
|
||||
|
@ -267,6 +270,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
messageStatusLabel.center(.vertical, in: messageStatusContainerView)
|
||||
messageStatusLabel.pin(.leading, to: .leading, of: messageStatusContainerView)
|
||||
messageStatusLabel.pin(.trailing, to: .leading, of: messageStatusImageView, withInset: -2)
|
||||
messageStatusLabelPaddingView.pin(.leading, to: .leading, of: messageStatusContainerView)
|
||||
messageStatusLabelPaddingView.pin(.trailing, to: .trailing, of: messageStatusContainerView)
|
||||
}
|
||||
|
||||
override func setUpGestureRecognizers() {
|
||||
|
@ -429,6 +434,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
!cellViewModel.isLast
|
||||
)
|
||||
)
|
||||
messageStatusLabelPaddingView.isHidden = (
|
||||
messageStatusContainerView.isHidden ||
|
||||
cellViewModel.isLast
|
||||
)
|
||||
|
||||
// Set the height of the underBubbleStackView to 0 if it has no content (need to do this
|
||||
// otherwise it can randomly stretch)
|
||||
|
@ -1121,11 +1130,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
return [:]
|
||||
}
|
||||
|
||||
// Note: The 'String.count' value is based on actual character counts whereas
|
||||
// NSAttributedString and NSRange are both based on UTF-16 encoded lengths, so
|
||||
// in order to avoid strings which contain emojis breaking strings which end
|
||||
// with URLs we need to use the 'String.utf16.count' value when creating the range
|
||||
return detector
|
||||
.matches(
|
||||
in: attributedText.string,
|
||||
options: [],
|
||||
range: NSRange(location: 0, length: attributedText.string.count)
|
||||
range: NSRange(location: 0, length: attributedText.string.utf16.count)
|
||||
)
|
||||
.reduce(into: [:]) { result, match in
|
||||
guard
|
||||
|
|
|
@ -546,6 +546,7 @@ class NotificationActionHandler {
|
|||
db,
|
||||
interactionId: interaction.id,
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant,
|
||||
includingOlder: true,
|
||||
trySendReadReceipt: true
|
||||
)
|
||||
|
@ -600,6 +601,7 @@ class NotificationActionHandler {
|
|||
.asRequest(of: Int64.self)
|
||||
.fetchOne(db),
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant,
|
||||
includingOlder: true,
|
||||
trySendReadReceipt: true
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import Reachability
|
||||
import SessionUIKit
|
||||
|
||||
final class PathStatusView: UIView {
|
||||
|
@ -42,6 +43,7 @@ final class PathStatusView: UIView {
|
|||
// MARK: - Initialization
|
||||
|
||||
private let size: Size
|
||||
private let reachability: Reachability = Reachability.forInternetConnection()
|
||||
|
||||
init(size: Size = .small) {
|
||||
self.size = size
|
||||
|
@ -73,15 +75,34 @@ final class PathStatusView: UIView {
|
|||
self.set(.width, to: self.size.pointSize)
|
||||
self.set(.height, to: self.size.pointSize)
|
||||
|
||||
setStatus(to: (!OnionRequestAPI.paths.isEmpty ? .connected : .connecting))
|
||||
switch (reachability.isReachable(), OnionRequestAPI.paths.isEmpty) {
|
||||
case (false, _): setStatus(to: .error)
|
||||
case (true, true): setStatus(to: .connecting)
|
||||
case (true, false): setStatus(to: .connected)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
private func registerObservers() {
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleBuildingPathsNotification), name: .buildingPaths, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handlePathsBuiltNotification), name: .pathsBuilt, object: nil)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleBuildingPathsNotification),
|
||||
name: .buildingPaths,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handlePathsBuiltNotification),
|
||||
name: .pathsBuilt,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(reachabilityChanged),
|
||||
name: .reachabilityChanged,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func setStatus(to status: Status) {
|
||||
|
@ -102,10 +123,34 @@ final class PathStatusView: UIView {
|
|||
}
|
||||
|
||||
@objc private func handleBuildingPathsNotification() {
|
||||
guard reachability.isReachable() else {
|
||||
setStatus(to: .error)
|
||||
return
|
||||
}
|
||||
|
||||
setStatus(to: .connecting)
|
||||
}
|
||||
|
||||
@objc private func handlePathsBuiltNotification() {
|
||||
guard reachability.isReachable() else {
|
||||
setStatus(to: .error)
|
||||
return
|
||||
}
|
||||
|
||||
setStatus(to: .connected)
|
||||
}
|
||||
|
||||
@objc private func reachabilityChanged() {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in self?.reachabilityChanged() }
|
||||
return
|
||||
}
|
||||
|
||||
guard reachability.isReachable() else {
|
||||
setStatus(to: .error)
|
||||
return
|
||||
}
|
||||
|
||||
setStatus(to: (!OnionRequestAPI.paths.isEmpty ? .connected : .connecting))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import GRDB
|
||||
import LocalAuthentication
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
@ -99,7 +100,23 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
|
|||
title: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE".localized(),
|
||||
subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(),
|
||||
rightAccessory: .toggle(.settingBool(key: .isScreenLockEnabled)),
|
||||
onTap: {
|
||||
onTap: { [weak self] in
|
||||
// Make sure the device has a passcode set before allowing screen lock to
|
||||
// be enabled (Note: This will always return true on a simulator)
|
||||
guard LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) else {
|
||||
self?.transitionToScreen(
|
||||
ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE".localized(),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
),
|
||||
transitionType: .present
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Storage.shared.write { db in
|
||||
db[.isScreenLockEnabled] = !db[.isScreenLockEnabled]
|
||||
}
|
||||
|
|
|
@ -445,6 +445,7 @@ public extension Interaction {
|
|||
_ db: Database,
|
||||
interactionId: Int64?,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
includingOlder: Bool,
|
||||
trySendReadReceipt: Bool
|
||||
) throws {
|
||||
|
@ -480,8 +481,9 @@ public extension Interaction {
|
|||
))
|
||||
)
|
||||
|
||||
// If we want to send read receipts then try to add the 'SendReadReceiptsJob'
|
||||
if trySendReadReceipt {
|
||||
// If we want to send read receipts and it's a contact thread then try to add the
|
||||
// 'SendReadReceiptsJob'
|
||||
if trySendReadReceipt && threadVariant == .contact {
|
||||
JobRunner.upsert(
|
||||
db,
|
||||
job: SendReadReceiptsJob.createOrUpdateIfNeeded(
|
||||
|
|
|
@ -98,32 +98,24 @@ extension SendReadReceiptsJob {
|
|||
// MARK: - Convenience
|
||||
|
||||
public extension SendReadReceiptsJob {
|
||||
/// This method upserts a 'sendReadReceipts' job to include the timestamps for the specified `interactionIds`
|
||||
///
|
||||
/// **Note:** This method assumes that the provided `interactionIds` are valid and won't filter out any invalid ids so
|
||||
/// ensure that is done correctly beforehand
|
||||
@discardableResult static func createOrUpdateIfNeeded(_ db: Database, threadId: String, interactionIds: [Int64]) -> Job? {
|
||||
guard db[.areReadReceiptsEnabled] == true else { return nil }
|
||||
|
||||
// Retrieve the timestampMs values for the specified interactions
|
||||
let maybeTimestampMsValues: [Int64]? = try? Int64.fetchAll(
|
||||
db,
|
||||
Interaction
|
||||
.select(.timestampMs)
|
||||
.filter(interactionIds.contains(Interaction.Columns.id))
|
||||
// Only `standardIncoming` incoming interactions should have read receipts sent
|
||||
.filter(Interaction.Columns.variant == Interaction.Variant.standardIncoming)
|
||||
.filter(Interaction.Columns.wasRead == false) // Only send for unread messages
|
||||
.joining(
|
||||
// Don't send read receipts in group threads
|
||||
required: Interaction.thread
|
||||
.filter(SessionThread.Columns.variant != SessionThread.Variant.closedGroup)
|
||||
.filter(SessionThread.Columns.variant != SessionThread.Variant.openGroup)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
let timestampMsValues: [Int64] = (try? Interaction
|
||||
.select(.timestampMs)
|
||||
.filter(interactionIds.contains(Interaction.Columns.id))
|
||||
.distinct()
|
||||
.asRequest(of: Int64.self)
|
||||
.fetchAll(db))
|
||||
.defaulting(to: [])
|
||||
|
||||
// If there are no timestamp values then do nothing
|
||||
guard
|
||||
let timestampMsValues: [Int64] = maybeTimestampMsValues,
|
||||
!timestampMsValues.isEmpty
|
||||
else { return nil }
|
||||
guard !timestampMsValues.isEmpty else { return nil }
|
||||
|
||||
// Try to get an existing job (if there is one that's not running)
|
||||
if
|
||||
|
|
|
@ -19,7 +19,12 @@ extension MessageReceiver {
|
|||
|
||||
guard
|
||||
let interactionId: Int64 = maybeInteraction?.id,
|
||||
let interaction: Interaction = maybeInteraction
|
||||
let interaction: Interaction = maybeInteraction,
|
||||
let threadVariant: SessionThread.Variant = try SessionThread
|
||||
.filter(id: interaction.threadId)
|
||||
.select(.variant)
|
||||
.asRequest(of: SessionThread.Variant.self)
|
||||
.fetchOne(db)
|
||||
else { return }
|
||||
|
||||
// Mark incoming messages as read and remove any of their notifications
|
||||
|
@ -28,6 +33,7 @@ extension MessageReceiver {
|
|||
db,
|
||||
interactionId: interactionId,
|
||||
threadId: interaction.threadId,
|
||||
threadVariant: threadVariant,
|
||||
includingOlder: false,
|
||||
trySendReadReceipt: false
|
||||
)
|
||||
|
|
|
@ -405,6 +405,7 @@ extension MessageReceiver {
|
|||
db,
|
||||
interactionId: interactionId,
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant,
|
||||
includingOlder: true,
|
||||
trySendReadReceipt: true
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue