Fixed a number of crashes and bugs
Fixed a crash which would occur when rendering a message containing both a mention and a url Fixed a crash which could occur during migration due to the openGroupServerMessageId essentially being the max UInt64 value which was overflowing the Int64 storage Fixed a bug where empty read receipt updates were sending messages (even for non one-to-one conversations) Fixed a bug where loading in large numbers of messages (via the poller) was auto scrolling to the bottom if the user was close to the bottom (now limited to <5) Fixed a memory leak with the AllMediaViewController (strong delegate references) Fixed an issue where non-alphanumeric characters would cause issues with global search Fixed an issue where search result highlighting wasn't working properly Fixed an issue where the app switcher UI blocking wasn't working Updated the conversations to mark messages as read while scrolling (rather than all messages when entering/participating in a conversation) Updated the modal button font weight to be closer to the designs Added the ability to delete "unsent" messages
This commit is contained in:
parent
5b1e19dd2e
commit
91802e4812
|
@ -109,6 +109,9 @@ extension ContextMenuVC {
|
|||
delegate: ContextMenuActionDelegate?
|
||||
) -> [Action]? {
|
||||
// No context items for info messages
|
||||
guard cellViewModel.variant != .standardIncomingDeleted else {
|
||||
return [ Action.delete(cellViewModel, delegate) ]
|
||||
}
|
||||
guard cellViewModel.variant == .standardOutgoing || cellViewModel.variant == .standardIncoming else {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@ final class ContextMenuVC: UIViewController {
|
|||
menuView.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX))
|
||||
emojiBar.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX))
|
||||
|
||||
case .standardIncoming:
|
||||
case .standardIncoming, .standardIncomingDeleted:
|
||||
menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX)
|
||||
emojiBar.pin(.left, to: .left, of: view, withInset: targetFrame.minX)
|
||||
|
||||
|
@ -288,8 +288,8 @@ final class ContextMenuVC: UIViewController {
|
|||
let ratio: CGFloat = (frame.width / frame.height)
|
||||
|
||||
// FIXME: Need to update this when an appropriate replacement is added (see https://teng.pub/technical/2021/11/9/uiapplication-key-window-replacement)
|
||||
let topMargin = max(UIApplication.shared.keyWindow!.safeAreaInsets.top, Values.mediumSpacing)
|
||||
let bottomMargin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing)
|
||||
let topMargin = max((UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0), Values.mediumSpacing)
|
||||
let bottomMargin = max((UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0), Values.mediumSpacing)
|
||||
let diffY = finalFrame.height + menuHeight + Self.actionViewHeight + 2 * spacing + topMargin + bottomMargin - UIScreen.main.bounds.height
|
||||
|
||||
if diffY > 0 {
|
||||
|
|
|
@ -1594,6 +1594,14 @@ extension ConversationVC:
|
|||
|
||||
func delete(_ cellViewModel: MessageViewModel) {
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
guard cellViewModel.variant != .standardIncomingDeleted else {
|
||||
Storage.shared.writeAsync { db in
|
||||
_ = try Interaction
|
||||
.filter(id: cellViewModel.id)
|
||||
.deleteAll(db)
|
||||
}
|
||||
return
|
||||
}
|
||||
guard cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardOutgoing else {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -138,11 +138,13 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
result.showsVerticalScrollIndicator = false
|
||||
result.contentInsetAdjustmentBehavior = .never
|
||||
result.keyboardDismissMode = .interactive
|
||||
let bottomInset: CGFloat = viewModel.threadData.canWrite ? Values.mediumSpacing : Values.mediumSpacing + UIApplication.shared.keyWindow!.safeAreaInsets.bottom
|
||||
result.contentInset = UIEdgeInsets(
|
||||
top: 0,
|
||||
leading: 0,
|
||||
bottom: bottomInset,
|
||||
bottom: (viewModel.threadData.canWrite ?
|
||||
Values.mediumSpacing :
|
||||
(Values.mediumSpacing + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0))
|
||||
),
|
||||
trailing: 0
|
||||
)
|
||||
result.registerHeaderFooterView(view: UITableViewHeaderFooterView.self)
|
||||
|
@ -604,11 +606,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
snInputView.text = draft
|
||||
}
|
||||
|
||||
// Now we have done all the needed diffs, update the viewModel with the latest data and mark
|
||||
// all messages as read (we do it in here as the 'threadData' actually contains the last
|
||||
// 'interactionId' for the thread)
|
||||
// Now we have done all the needed diffs update the viewModel with the latest data
|
||||
self.viewModel.updateThreadData(updatedThreadData)
|
||||
self.viewModel.markAllAsRead()
|
||||
|
||||
/// **Note:** This needs to happen **after** we have update the viewModel's thread data
|
||||
if initialLoad || viewModel.threadData.currentUserIsClosedGroupMember != updatedThreadData.currentUserIsClosedGroupMember {
|
||||
|
@ -682,7 +681,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
source: viewModel.interactionData,
|
||||
target: updatedData
|
||||
)
|
||||
let isInsert: Bool = (changeset.map({ $0.elementInserted.count }).reduce(0, +) > 0)
|
||||
let numItemsInserted: Int = changeset.map { $0.elementInserted.count }.reduce(0, +)
|
||||
let isInsert: Bool = (numItemsInserted > 0)
|
||||
let wasLoadingMore: Bool = self.isLoadingMore
|
||||
let wasOffsetCloseToBottom: Bool = self.isCloseToBottom
|
||||
let numItemsInUpdatedData: [Int] = updatedData.map { $0.elements.count }
|
||||
|
@ -758,10 +758,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
}
|
||||
}
|
||||
}
|
||||
else if wasOffsetCloseToBottom && !wasLoadingMore {
|
||||
// Scroll to the bottom if an interaction was just inserted and we either
|
||||
// just sent a message or are close enough to the bottom (wait a tiny fraction
|
||||
// to avoid buggy animation behaviour)
|
||||
else if wasOffsetCloseToBottom && !wasLoadingMore && numItemsInserted < 5 {
|
||||
/// Scroll to the bottom if an interaction was just inserted and we either just sent a message or are close enough to the
|
||||
/// bottom (wait a tiny fraction to avoid buggy animation behaviour)
|
||||
///
|
||||
/// **Note:** We won't automatically scroll to the bottom if 5 or more messages were inserted (to avoid endlessly
|
||||
/// auto-scrolling to the bottom when fetching new pages of data within open groups
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
|
||||
self?.scrollToBottom(isAnimated: true)
|
||||
}
|
||||
|
@ -771,6 +773,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
self.isLoadingMore = false
|
||||
self.autoLoadNextPageIfNeeded()
|
||||
}
|
||||
else {
|
||||
// Need to update the scroll button alpha in case new messages were added but we didn't scroll
|
||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1311,6 +1318,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
)
|
||||
|
||||
self.handleInitialOffsetBounceBug(targetIndexPath: targetIndexPath, at: .bottom)
|
||||
self.viewModel.markAsRead(beforeInclusive: nil)
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
|
@ -1322,8 +1330,41 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
scrollButton.alpha = getScrollButtonOpacity()
|
||||
unreadCountView.alpha = scrollButton.alpha
|
||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||
|
||||
// We want to mark messages as read while we scroll, so grab the newest message and mark
|
||||
// everything older as read
|
||||
//
|
||||
// Note: For the 'tableVisualBottom' we remove the 'Values.mediumSpacing' as that is the distance
|
||||
// the table content appears above the input view
|
||||
let tableVisualBottom: CGFloat = (tableView.frame.maxY - (tableView.contentInset.bottom - Values.mediumSpacing))
|
||||
|
||||
if
|
||||
let visibleIndexPaths: [IndexPath] = self.tableView.indexPathsForVisibleRows,
|
||||
let messagesSection: Int = visibleIndexPaths
|
||||
.first(where: { self.viewModel.interactionData[$0.section].model == .messages })?
|
||||
.section,
|
||||
let newestCellViewModel: MessageViewModel = visibleIndexPaths
|
||||
.sorted()
|
||||
.filter({ $0.section == messagesSection })
|
||||
.compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in
|
||||
guard let frame: CGRect = tableView.cellForRow(at: indexPath)?.frame else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (
|
||||
view.convert(frame, from: tableView),
|
||||
self.viewModel.interactionData[indexPath.section].elements[indexPath.row]
|
||||
)
|
||||
})
|
||||
// Exclude messages that are partially off the bottom of the screen
|
||||
.filter({ $0.frame.maxY <= tableVisualBottom })
|
||||
.last?
|
||||
.cellViewModel
|
||||
{
|
||||
self.viewModel.markAsRead(beforeInclusive: newestCellViewModel.id)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
|
@ -1472,6 +1513,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
// Store the info incase we need to load more data (call will be re-triggered)
|
||||
self.focusedInteractionId = interactionId
|
||||
self.shouldHighlightNextScrollToInteraction = highlight
|
||||
self.viewModel.markAsRead(beforeInclusive: interactionId)
|
||||
|
||||
// Ensure the target interaction has been loaded
|
||||
guard
|
||||
|
|
|
@ -149,6 +149,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
|
||||
// MARK: - Interaction Data
|
||||
|
||||
private var lastInteractionIdMarkedAsRead: Int64?
|
||||
public private(set) var unobservedInteractionDataChanges: [SectionModel]?
|
||||
public private(set) var interactionData: [SectionModel] = []
|
||||
public private(set) var reactionExpandedInteractionIds: Set<Int64> = []
|
||||
|
@ -380,21 +381,30 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
public func markAllAsRead() {
|
||||
// Don't bother marking anything as read if there are no unread interactions (we can rely
|
||||
// on the 'threadData.threadUnreadCount' to always be accurate)
|
||||
/// This method will mark all interactions as read before the specified interaction id, if no id is provided then all interactions for
|
||||
/// the thread will be marked as read
|
||||
public func markAsRead(beforeInclusive interactionId: Int64?) {
|
||||
/// Since this method now gets triggered when scrolling we want to try to optimise it and avoid busying the database
|
||||
/// write queue when it isn't needed, in order to do this we:
|
||||
///
|
||||
/// - Don't bother marking anything as read if there are no unread interactions (we can rely on the
|
||||
/// `threadData.threadUnreadCount` to always be accurate)
|
||||
/// - Don't bother marking anything as read if this was called with the same `interactionId` that we
|
||||
/// previously marked as read (ie. when scrolling and the last message hasn't changed)
|
||||
guard
|
||||
(self.threadData.threadUnreadCount ?? 0) > 0,
|
||||
let lastInteractionId: Int64 = self.threadData.interactionId
|
||||
let targetInteractionId: Int64 = (interactionId ?? self.threadData.interactionId),
|
||||
self.lastInteractionIdMarkedAsRead != targetInteractionId
|
||||
else { return }
|
||||
|
||||
let threadId: String = self.threadData.threadId
|
||||
let trySendReadReceipt: Bool = (self.threadData.threadIsMessageRequest == false)
|
||||
self.lastInteractionIdMarkedAsRead = targetInteractionId
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
try Interaction.markAsRead(
|
||||
db,
|
||||
interactionId: lastInteractionId,
|
||||
interactionId: targetInteractionId,
|
||||
threadId: threadId,
|
||||
includingOlder: true,
|
||||
trySendReadReceipt: trySendReadReceipt
|
||||
|
|
|
@ -1049,51 +1049,52 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
)
|
||||
|
||||
// Custom handle links
|
||||
let links: [String: NSRange] = {
|
||||
guard
|
||||
let body: String = cellViewModel.body,
|
||||
let detector: NSDataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||
else { return [:] }
|
||||
|
||||
var links: [String: NSRange] = [:]
|
||||
let matches = detector.matches(
|
||||
in: body,
|
||||
options: [],
|
||||
range: NSRange(location: 0, length: body.count)
|
||||
)
|
||||
|
||||
for match in matches {
|
||||
guard let matchURL = match.url else { continue }
|
||||
|
||||
/// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and
|
||||
/// set the scheme to 'https' instead as we don't load previews for 'http' so this will result
|
||||
/// in more previews actually getting loaded without forcing the user to enter 'https://' before
|
||||
/// every URL they enter
|
||||
let urlString: String = (matchURL.absoluteString == "http://\(body)" ?
|
||||
"https://\(body)" :
|
||||
matchURL.absoluteString
|
||||
)
|
||||
|
||||
if URL(string: urlString) != nil {
|
||||
links[urlString] = (body as NSString).range(of: urlString)
|
||||
}
|
||||
let links: [URL: NSRange] = {
|
||||
guard let detector: NSDataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
return links
|
||||
return detector
|
||||
.matches(
|
||||
in: attributedText.string,
|
||||
options: [],
|
||||
range: NSRange(location: 0, length: attributedText.string.count)
|
||||
)
|
||||
.reduce(into: [:]) { result, match in
|
||||
guard
|
||||
let matchUrl: URL = match.url,
|
||||
let originalRange: Range = Range(match.range, in: attributedText.string)
|
||||
else { return }
|
||||
|
||||
/// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and
|
||||
/// set the scheme to 'https' instead as we don't load previews for 'http' so this will result
|
||||
/// in more previews actually getting loaded without forcing the user to enter 'https://' before
|
||||
/// every URL they enter
|
||||
let originalString: String = String(attributedText.string[originalRange])
|
||||
|
||||
guard matchUrl.absoluteString != "http://\(originalString)" else {
|
||||
guard let httpsUrl: URL = URL(string: "https://\(originalString)") else {
|
||||
return
|
||||
}
|
||||
|
||||
result[httpsUrl] = match.range
|
||||
return
|
||||
}
|
||||
|
||||
result[matchUrl] = match.range
|
||||
}
|
||||
}()
|
||||
|
||||
for (urlString, range) in links {
|
||||
guard let url: URL = URL(string: urlString) else { continue }
|
||||
|
||||
for (linkUrl, urlRange) in links {
|
||||
attributedText.addAttributes(
|
||||
[
|
||||
.font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)),
|
||||
.foregroundColor: actualTextColor,
|
||||
.underlineColor: actualTextColor,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.attachment: url
|
||||
.attachment: linkUrl
|
||||
],
|
||||
range: range
|
||||
range: urlRange
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1105,7 +1106,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
.map { part -> String in
|
||||
guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part }
|
||||
|
||||
return String(part[part.index(after: part.startIndex)..<part.endIndex])
|
||||
let partRange = (part.index(after: part.startIndex)..<part.index(before: part.endIndex))
|
||||
return String(part[partRange])
|
||||
}
|
||||
.forEach { part in
|
||||
// Highlight all ranges of the text (Note: The search logic only finds
|
||||
|
@ -1114,13 +1116,26 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
normalizedBody
|
||||
.ranges(
|
||||
of: (CurrentAppContext().isRTL ?
|
||||
"\(part.lowercased())(^|[ ])" :
|
||||
"(^|[ ])\(part.lowercased())"
|
||||
"(\(part.lowercased()))(^|[^a-zA-Z0-9])" :
|
||||
"(^|[^a-zA-Z0-9])(\(part.lowercased()))"
|
||||
),
|
||||
options: [.regularExpression]
|
||||
)
|
||||
.forEach { range in
|
||||
let legacyRange: NSRange = NSRange(range, in: normalizedBody)
|
||||
let targetRange: Range<String.Index> = {
|
||||
let term: String = String(normalizedBody[range])
|
||||
|
||||
// If the matched term doesn't actually match the "part" value then it means
|
||||
// we've matched a term after a non-alphanumeric character so need to shift
|
||||
// the range over by 1
|
||||
guard term.starts(with: part.lowercased()) else {
|
||||
return (normalizedBody.index(after: range.lowerBound)..<range.upperBound)
|
||||
}
|
||||
|
||||
return range
|
||||
}()
|
||||
|
||||
let legacyRange: NSRange = NSRange(targetRange, in: normalizedBody)
|
||||
attributedText.addThemeAttribute(.background(backgroundPrimaryColor), range: legacyRange)
|
||||
attributedText.addThemeAttribute(.foreground(textPrimaryColor), range: legacyRange)
|
||||
}
|
||||
|
|
|
@ -331,7 +331,9 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
// PagedDatabaseObserver won't have them so we need to force a re-fetch of the current
|
||||
// data to ensure everything is up to date
|
||||
if didReturnFromBackground {
|
||||
self.viewModel.pagedDataObserver?.reload()
|
||||
DispatchQueue.global(qos: .default).async { [weak self] in
|
||||
self?.viewModel.pagedDataObserver?.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate,
|
|||
private var isAutoLoadingNextPage: Bool = false
|
||||
private var currentTargetOffset: CGPoint?
|
||||
|
||||
public var delegate: DocumentTileViewControllerDelegate?
|
||||
public weak var delegate: DocumentTileViewControllerDelegate?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
private var isAutoLoadingNextPage: Bool = false
|
||||
private var currentTargetOffset: CGPoint?
|
||||
|
||||
public var delegate: MediaTileViewControllerDelegate?
|
||||
public weak var delegate: MediaTileViewControllerDelegate?
|
||||
|
||||
var isInBatchSelectMode = false {
|
||||
didSet {
|
||||
|
|
|
@ -545,7 +545,8 @@ public final class FullConversationCell: UITableViewCell {
|
|||
.map { part -> String in
|
||||
guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part }
|
||||
|
||||
return String(part[part.index(after: part.startIndex)..<part.endIndex])
|
||||
let partRange = (part.index(after: part.startIndex)..<part.index(before: part.endIndex))
|
||||
return String(part[partRange])
|
||||
}
|
||||
.forEach { part in
|
||||
// Highlight all ranges of the text (Note: The search logic only finds results that start
|
||||
|
@ -553,18 +554,31 @@ public final class FullConversationCell: UITableViewCell {
|
|||
normalizedSnippet
|
||||
.ranges(
|
||||
of: (CurrentAppContext().isRTL ?
|
||||
"\(part.lowercased())(^|[ ])" :
|
||||
"(^|[ ])\(part.lowercased())"
|
||||
"(\(part.lowercased()))(^|[^a-zA-Z0-9])" :
|
||||
"(^|[^a-zA-Z0-9])(\(part.lowercased()))"
|
||||
),
|
||||
options: [.regularExpression]
|
||||
)
|
||||
.forEach { range in
|
||||
let targetRange: Range<String.Index> = {
|
||||
let term: String = String(normalizedSnippet[range])
|
||||
|
||||
// If the matched term doesn't actually match the "part" value then it means
|
||||
// we've matched a term after a non-alphanumeric character so need to shift
|
||||
// the range over by 1
|
||||
guard term.starts(with: part.lowercased()) else {
|
||||
return (normalizedSnippet.index(after: range.lowerBound)..<range.upperBound)
|
||||
}
|
||||
|
||||
return range
|
||||
}()
|
||||
|
||||
// Store the range of the first match so we can focus it in the content displayed
|
||||
if firstMatchRange == nil {
|
||||
firstMatchRange = range
|
||||
firstMatchRange = targetRange
|
||||
}
|
||||
|
||||
let legacyRange: NSRange = NSRange(range, in: normalizedSnippet)
|
||||
let legacyRange: NSRange = NSRange(targetRange, in: normalizedSnippet)
|
||||
result.addAttribute(.foregroundColor, value: textColor, range: legacyRange)
|
||||
result.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: legacyRange)
|
||||
}
|
||||
|
|
|
@ -104,13 +104,8 @@ class ScreenLockUI {
|
|||
return .none;
|
||||
}
|
||||
|
||||
if Storage.shared[.appSwitcherPreviewEnabled] {
|
||||
Logger.verbose("desiredUIState: screen protection 4.")
|
||||
return .protection;
|
||||
}
|
||||
|
||||
Logger.verbose("desiredUIState: none 5.")
|
||||
return .none
|
||||
Logger.verbose("desiredUIState: screen protection 4.")
|
||||
return .protection;
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
|
|
@ -49,7 +49,6 @@ public enum SMKLegacy {
|
|||
|
||||
internal static let preferencesCollection = "SignalPreferences"
|
||||
internal static let additionalPreferencesCollection = "SSKPreferences"
|
||||
internal static let preferencesKeyScreenSecurityDisabled = "Screen Security Key"
|
||||
internal static let preferencesKeyLastRecordedPushToken = "LastRecordedPushToken"
|
||||
internal static let preferencesKeyLastRecordedVoipToken = "LastRecordedVoipToken"
|
||||
internal static let preferencesKeyAreLinkPreviewsEnabled = "areLinkPreviewsEnabled"
|
||||
|
|
|
@ -722,7 +722,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
let wasRead: Bool
|
||||
let expiresInSeconds: UInt32?
|
||||
let expiresStartedAtMs: UInt64?
|
||||
let openGroupServerMessageId: UInt64?
|
||||
let openGroupServerMessageId: Int64?
|
||||
let recipientStateMap: [String: SMKLegacy._DBOutgoingMessageRecipientState]?
|
||||
let mostRecentFailureText: String?
|
||||
let quotedMessage: SMKLegacy._DBQuotedMessage?
|
||||
|
@ -737,9 +737,13 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// The legacy code only considered '!= 0' ids as valid so set those
|
||||
// values to be null to avoid the unique constraint (it's also more
|
||||
// correct for the values to be null)
|
||||
openGroupServerMessageId = (legacyMessage.openGroupServerMessageID == 0 ?
|
||||
//
|
||||
// Note: Looks like it was also possible for this to be set to the max
|
||||
// value which overflows when trying to convert to a signed version so
|
||||
// we essentially discard the information in those cases)
|
||||
openGroupServerMessageId = (Int64.zeroingOverflow(legacyMessage.openGroupServerMessageID) == 0 ?
|
||||
nil :
|
||||
legacyMessage.openGroupServerMessageID
|
||||
Int64.zeroingOverflow(legacyMessage.openGroupServerMessageID)
|
||||
)
|
||||
quotedMessage = legacyMessage.quotedMessage
|
||||
|
||||
|
@ -904,8 +908,8 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
authorId: authorId,
|
||||
variant: variant,
|
||||
body: body,
|
||||
timestampMs: Int64(legacyInteraction.timestamp),
|
||||
receivedAtTimestampMs: Int64(legacyInteraction.receivedAtTimestamp),
|
||||
timestampMs: Int64.zeroingOverflow(legacyInteraction.timestamp),
|
||||
receivedAtTimestampMs: Int64.zeroingOverflow(legacyInteraction.receivedAtTimestamp),
|
||||
wasRead: wasRead,
|
||||
hasMention: Interaction.isUserMentioned(
|
||||
db,
|
||||
|
@ -923,7 +927,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
nil
|
||||
),
|
||||
linkPreviewUrl: linkPreview?.urlString, // Only a soft link so save to set
|
||||
openGroupServerMessageId: openGroupServerMessageId.map { Int64($0) },
|
||||
openGroupServerMessageId: openGroupServerMessageId,
|
||||
openGroupWhisperMods: false,
|
||||
openGroupWhisperTo: nil
|
||||
).inserted(db)
|
||||
|
@ -945,7 +949,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
try ControlMessageProcessRecord(
|
||||
threadId: threadId,
|
||||
variant: variant,
|
||||
timestampMs: Int64(legacyInteraction.timestamp)
|
||||
timestampMs: Int64.zeroingOverflow(legacyInteraction.timestamp)
|
||||
)?.insert(db)
|
||||
|
||||
// Remove timestamps we created records for (they will be protected by unique
|
||||
|
@ -1086,7 +1090,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
try Quote(
|
||||
interactionId: interactionId,
|
||||
authorId: quotedMessage.authorId,
|
||||
timestampMs: Int64(quotedMessage.timestamp),
|
||||
timestampMs: Int64.zeroingOverflow(quotedMessage.timestamp),
|
||||
body: quotedMessage.body,
|
||||
attachmentId: attachmentId
|
||||
).insert(db)
|
||||
|
@ -1192,7 +1196,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// entries as "legacy"
|
||||
try ControlMessageProcessRecord.generateLegacyProcessRecords(
|
||||
db,
|
||||
receivedMessageTimestamps: receivedMessageTimestamps.map { Int64($0) }
|
||||
receivedMessageTimestamps: receivedMessageTimestamps.map { Int64.zeroingOverflow($0) }
|
||||
)
|
||||
|
||||
// Clear out processed data (give the memory a change to be freed)
|
||||
|
@ -1448,9 +1452,6 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
db[.lastRecordedVoipToken] = lastVoipToken
|
||||
}
|
||||
|
||||
// Note: The 'preferencesKeyScreenSecurityDisabled' value previously controlled whether the
|
||||
// setting was disabled, this has been inverted to 'appSwitcherPreviewEnabled' so it can default
|
||||
// to 'false' (as most Bool values do)
|
||||
db[.areReadReceiptsEnabled] = (legacyPreferences[SMKLegacy.readReceiptManagerAreReadReceiptsEnabled] as? Bool == true)
|
||||
db[.typingIndicatorsEnabled] = (legacyPreferences[SMKLegacy.typingIndicatorsEnabledKey] as? Bool == true)
|
||||
db[.isScreenLockEnabled] = (legacyPreferences[SMKLegacy.screenLockIsScreenLockEnabledKey] as? Bool == true)
|
||||
|
@ -1461,7 +1462,6 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
value: (legacyPreferences[SMKLegacy.screenLockScreenLockTimeoutSecondsKey] as? Double)
|
||||
.defaulting(to: (15 * 60))
|
||||
)
|
||||
db[.appSwitcherPreviewEnabled] = (legacyPreferences[SMKLegacy.preferencesKeyScreenSecurityDisabled] as? Bool == false)
|
||||
db[.areLinkPreviewsEnabled] = (legacyPreferences[SMKLegacy.preferencesKeyAreLinkPreviewsEnabled] as? Bool == true)
|
||||
db[.areCallsEnabled] = (legacyPreferences[SMKLegacy.preferencesKeyAreCallsEnabled] as? Bool == true)
|
||||
db[.hasHiddenMessageRequests] = CurrentAppContext().appUserDefaults()
|
||||
|
@ -1854,3 +1854,9 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Int64 {
|
||||
static func zeroingOverflow(_ value: UInt64) -> Int64 {
|
||||
return (value > UInt64(Int64.max) ? 0 : Int64(value))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ public extension SendReadReceiptsJob {
|
|||
.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
|
||||
|
@ -119,7 +120,10 @@ public extension SendReadReceiptsJob {
|
|||
)
|
||||
|
||||
// If there are no timestamp values then do nothing
|
||||
guard let timestampMsValues: [Int64] = maybeTimestampMsValues else { return nil }
|
||||
guard
|
||||
let timestampMsValues: [Int64] = maybeTimestampMsValues,
|
||||
!timestampMsValues.isEmpty
|
||||
else { return nil }
|
||||
|
||||
// Try to get an existing job (if there is one that's not running)
|
||||
if
|
||||
|
|
|
@ -940,6 +940,13 @@ public extension SessionThreadViewModel {
|
|||
public extension SessionThreadViewModel {
|
||||
static let searchResultsLimit: Int = 500
|
||||
|
||||
/// FTS will fail or try to process characters outside of `[A-Za-z0-9]` are included directly in a search
|
||||
/// term, in order to resolve this the term needs to be wrapped in quotation marks so the eventual SQL
|
||||
/// is `MATCH '"{term}"'` or `MATCH '"{term}"*'`
|
||||
static func searchSafeTerm(_ term: String) -> String {
|
||||
return "\"\(term)\""
|
||||
}
|
||||
|
||||
static func searchTermParts(_ searchTerm: String) -> [String] {
|
||||
/// Process the search term in order to extract the parts of the search pattern we want
|
||||
///
|
||||
|
@ -954,7 +961,7 @@ public extension SessionThreadViewModel {
|
|||
guard index % 2 == 1 else {
|
||||
return String(value)
|
||||
.split(separator: " ")
|
||||
.map { String($0) }
|
||||
.map { "\"\(String($0))\"" }
|
||||
}
|
||||
|
||||
return ["\"\(value)\""]
|
||||
|
@ -972,13 +979,14 @@ public extension SessionThreadViewModel {
|
|||
let rawPattern: String = searchTermParts(searchTerm)
|
||||
.joined(separator: " OR ")
|
||||
.appending("*")
|
||||
let fallbackTerm: String = "\(searchSafeTerm(searchTerm))*"
|
||||
|
||||
/// There are cases where creating a pattern can fail, we want to try and recover from those cases
|
||||
/// by failling back to simpler patterns if needed
|
||||
let maybePattern: FTS5Pattern? = (try? db.makeFTS5Pattern(rawPattern: rawPattern, forTable: table))
|
||||
.defaulting(
|
||||
to: (try? db.makeFTS5Pattern(rawPattern: searchTerm, forTable: table))
|
||||
.defaulting(to: FTS5Pattern(matchingAnyTokenIn: searchTerm))
|
||||
to: (try? db.makeFTS5Pattern(rawPattern: fallbackTerm, forTable: table))
|
||||
.defaulting(to: FTS5Pattern(matchingAnyTokenIn: fallbackTerm))
|
||||
)
|
||||
|
||||
guard let pattern: FTS5Pattern = maybePattern else { throw StorageError.invalidSearchPattern }
|
||||
|
|
|
@ -15,12 +15,6 @@ public extension Setting.EnumKey {
|
|||
}
|
||||
|
||||
public extension Setting.BoolKey {
|
||||
/// Controls whether the preview screen in the app switcher should be enabled
|
||||
///
|
||||
/// **Note:** In the legacy setting this flag controlled whether the preview was "disabled" (and defaulted to
|
||||
/// true), by inverting this flag we can default it to false as is standard for Bool values
|
||||
static let appSwitcherPreviewEnabled: Setting.BoolKey = "appSwitcherPreviewEnabled"
|
||||
|
||||
/// Controls whether typing indicators are enabled
|
||||
///
|
||||
/// **Note:** Only works if both participants in a "contact" thread have this setting enabled
|
||||
|
|
|
@ -116,8 +116,8 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate {
|
|||
}
|
||||
|
||||
public static func createButton(title: String, titleColor: ThemeValue) -> UIButton {
|
||||
let result: UIButton = UIButton() // TODO: NEED to fix the font (looks bad)
|
||||
result.titleLabel?.font = .systemFont(ofSize: Values.mediumFontSize, weight: UIFont.Weight(600))
|
||||
let result: UIButton = UIButton()
|
||||
result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.setTitle(title, for: .normal)
|
||||
result.setThemeTitleColor(titleColor, for: .normal)
|
||||
result.setThemeBackgroundColor(.alert_buttonBackground, for: .normal)
|
||||
|
|
|
@ -360,14 +360,26 @@ public final class Storage {
|
|||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return }
|
||||
guard let observer: TransactionObserver = observer else { return }
|
||||
|
||||
dbWriter.add(transactionObserver: observer)
|
||||
// Note: This actually triggers a write to the database so can be blocked by other
|
||||
// writes, since it's usually called on the main thread when creating a view controller
|
||||
// this can result in the UI hanging - to avoid this we dispatch (and hope there isn't
|
||||
// negative impact)
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
dbWriter.add(transactionObserver: observer)
|
||||
}
|
||||
}
|
||||
|
||||
public func removeObserver(_ observer: TransactionObserver?) {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return }
|
||||
guard let observer: TransactionObserver = observer else { return }
|
||||
|
||||
dbWriter.remove(transactionObserver: observer)
|
||||
// Note: This actually triggers a write to the database so can be blocked by other
|
||||
// writes, since it's usually called on the main thread when creating a view controller
|
||||
// this can result in the UI hanging - to avoid this we dispatch (and hope there isn't
|
||||
// negative impact)
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
dbWriter.remove(transactionObserver: observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
import CoreServices
|
||||
import PromiseKit
|
||||
import SessionUIKit
|
||||
import CoreServices
|
||||
import SessionMessagingKit
|
||||
import SignalCoreKit
|
||||
|
||||
public protocol AttachmentApprovalViewControllerDelegate: AnyObject {
|
||||
func attachmentApproval(
|
||||
|
@ -538,7 +539,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
|
||||
private func setCurrentItem(_ item: SignalAttachmentItem?, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) {
|
||||
guard let item: SignalAttachmentItem = item, let page = self.buildPage(item: item) else {
|
||||
owsFailDebug("unexpectedly unable to build new page")
|
||||
Logger.error("unexpectedly unable to build new page")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -550,7 +551,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
|
||||
func updateMediaRail() {
|
||||
guard let currentItem = self.currentItem else {
|
||||
owsFailDebug("currentItem was unexpectedly nil")
|
||||
Logger.error("currentItem was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -565,7 +566,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
return cell
|
||||
|
||||
default:
|
||||
owsFailDebug("unexpted rail item type: \(railItem)")
|
||||
Logger.error("unexpted rail item type: \(railItem)")
|
||||
return GalleryRailCellView()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ public class ScreenLock {
|
|||
defaultErrorDescription: defaultErrorDescription)
|
||||
switch outcome {
|
||||
case .success:
|
||||
owsFailDebug("local authentication unexpected success")
|
||||
Logger.error("local authentication unexpected success")
|
||||
completion(.failure(error: defaultErrorDescription))
|
||||
|
||||
case .cancel, .failure, .unexpectedFailure:
|
||||
|
@ -129,8 +129,8 @@ public class ScreenLock {
|
|||
|
||||
switch outcome {
|
||||
case .success:
|
||||
owsFailDebug("local authentication unexpected success")
|
||||
completion(.failure(error:defaultErrorDescription))
|
||||
Logger.error("local authentication unexpected success")
|
||||
completion(.failure(error: defaultErrorDescription))
|
||||
|
||||
case .cancel, .failure, .unexpectedFailure:
|
||||
completion(outcome)
|
||||
|
@ -190,11 +190,11 @@ public class ScreenLock {
|
|||
return .failure(error: "SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT".localized())
|
||||
|
||||
case .invalidContext:
|
||||
owsFailDebug("context not valid.")
|
||||
Logger.error("context not valid.")
|
||||
return .unexpectedFailure(error: defaultErrorDescription)
|
||||
|
||||
case .notInteractive:
|
||||
owsFailDebug("context not interactive.")
|
||||
Logger.error("context not interactive.")
|
||||
return .unexpectedFailure(error: defaultErrorDescription)
|
||||
|
||||
@unknown default:
|
||||
|
|
Loading…
Reference in New Issue