Got paging working on the conversation screen
Fixed a couple of issues where attachment messages would flicker due to thread changing Fixed a couple of issues with page loading Connected the global search result select back up
This commit is contained in:
parent
19cd9d13c5
commit
62c886e764
|
@ -685,6 +685,7 @@
|
|||
FD848B87283B844B000E298B /* MessageCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B86283B844B000E298B /* MessageCellViewModel.swift */; };
|
||||
FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */; };
|
||||
FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8C283E0B26000E298B /* MessageInputTypes.swift */; };
|
||||
FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8E283EF2A8000E298B /* UIScrollView+Utilities.swift */; };
|
||||
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; };
|
||||
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
|
||||
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
|
||||
|
@ -1661,6 +1662,7 @@
|
|||
FD848B86283B844B000E298B /* MessageCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellViewModel.swift; sourceTree = "<group>"; };
|
||||
FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedDatabaseObserver.swift; sourceTree = "<group>"; };
|
||||
FD848B8C283E0B26000E298B /* MessageInputTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputTypes.swift; sourceTree = "<group>"; };
|
||||
FD848B8E283EF2A8000E298B /* UIScrollView+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
|
||||
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
|
||||
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
|
||||
|
@ -1935,6 +1937,7 @@
|
|||
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */,
|
||||
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
|
||||
FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */,
|
||||
FD848B8E283EF2A8000E298B /* UIScrollView+Utilities.swift */,
|
||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
|
||||
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
|
||||
FD859EFF27C4691300510D0C /* MockDataGenerator.swift */,
|
||||
|
@ -4705,6 +4708,7 @@
|
|||
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */,
|
||||
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
|
||||
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */,
|
||||
FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */,
|
||||
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
|
||||
454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */,
|
||||
34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */,
|
||||
|
|
|
@ -632,8 +632,12 @@ extension ConversationVC:
|
|||
// Show the context menu if applicable
|
||||
guard
|
||||
let keyWindow: UIWindow = UIApplication.shared.keyWindow,
|
||||
let index = viewModel.interactionData.firstIndex(of: cellViewModel),
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell,
|
||||
let sectionIndex: Int = self.viewModel.interactionData
|
||||
.firstIndex(where: { $0.model == .messages }),
|
||||
let index = self.viewModel.interactionData[sectionIndex]
|
||||
.elements
|
||||
.firstIndex(of: cellViewModel),
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? VisibleMessageCell,
|
||||
let snapshot = cell.bubbleView.snapshotView(afterScreenUpdates: false),
|
||||
contextMenuWindow == nil,
|
||||
let actions: [ContextMenuVC.Action] = ContextMenuVC.actions(
|
||||
|
@ -693,8 +697,12 @@ extension ConversationVC:
|
|||
|
||||
case .mediaMessage:
|
||||
guard
|
||||
let index = self.viewModel.interactionData.firstIndex(where: { $0.id == cellViewModel.id }),
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell,
|
||||
let sectionIndex: Int = self.viewModel.interactionData
|
||||
.firstIndex(where: { $0.model == .messages }),
|
||||
let messageIndex: Int = self.viewModel.interactionData[sectionIndex]
|
||||
.elements
|
||||
.firstIndex(where: { $0.id == cellViewModel.id }),
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: messageIndex, section: sectionIndex)) as? VisibleMessageCell,
|
||||
let albumView: MediaAlbumView = cell.albumView
|
||||
else { return }
|
||||
|
||||
|
|
|
@ -937,12 +937,20 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
}
|
||||
|
||||
func scrollToBottom(isAnimated: Bool) {
|
||||
guard !isUserScrolling && !viewModel.interactionData.isEmpty else { return }
|
||||
guard
|
||||
!isUserScrolling,
|
||||
let messagesSectionIndex: Int = self.viewModel.interactionData
|
||||
.firstIndex(where: { $0.model == .messages }),
|
||||
!self.viewModel.interactionData[messagesSectionIndex]
|
||||
.elements
|
||||
.isEmpty
|
||||
else { return }
|
||||
|
||||
tableView.scrollToRow(
|
||||
at: IndexPath(
|
||||
row: viewModel.interactionData.count - 1,
|
||||
section: 0),
|
||||
row: viewModel.interactionData[messagesSectionIndex].elements.count - 1,
|
||||
section: messagesSectionIndex
|
||||
),
|
||||
at: .bottom,
|
||||
animated: isAnimated
|
||||
)
|
||||
|
@ -959,7 +967,6 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
scrollButton.alpha = getScrollButtonOpacity()
|
||||
unreadCountView.alpha = scrollButton.alpha
|
||||
autoLoadMoreIfNeeded()
|
||||
}
|
||||
|
||||
func updateUnreadCountView(unreadCount: UInt?) {
|
||||
|
@ -970,14 +977,6 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
unreadCountView.isHidden = (unreadCount == 0)
|
||||
}
|
||||
|
||||
func autoLoadMoreIfNeeded() {
|
||||
let isMainAppAndActive = CurrentAppContext().isMainAppAndActive
|
||||
guard isMainAppAndActive && didFinishInitialLayout && viewModel.canLoadMoreItems() && !isLoadingMore
|
||||
&& messagesTableView.contentOffset.y < ConversationVC.loadMoreThreshold else { return }
|
||||
isLoadingMore = true
|
||||
viewModel.loadAnotherPageOfMessages()
|
||||
}
|
||||
|
||||
func getScrollButtonOpacity() -> CGFloat {
|
||||
let contentOffsetY = tableView.contentOffset.y
|
||||
let x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude)
|
||||
|
@ -1078,5 +1077,28 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
isAnimated: Bool = true,
|
||||
highlighted: Bool = false
|
||||
) {
|
||||
// Ensure the interaction is loaded
|
||||
self.viewModel.pagedDataObserver?.load(.untilInclusive(id: interactionId, padding: 0))
|
||||
|
||||
guard
|
||||
let messageSectionIndex: Int = self.viewModel.interactionData
|
||||
.firstIndex(where: { $0.model == .messages }),
|
||||
let targetMessageIndex = self.viewModel.interactionData[messageSectionIndex]
|
||||
.elements
|
||||
.firstIndex(where: { $0.id == interactionId })
|
||||
else { return }
|
||||
|
||||
tableView.scrollToRow(
|
||||
at: IndexPath(
|
||||
row: targetMessageIndex,
|
||||
section: messageSectionIndex
|
||||
),
|
||||
at: position,
|
||||
animated: isAnimated
|
||||
)
|
||||
|
||||
if highlighted {
|
||||
focusedMessageId = interactionId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@ import SessionMessagingKit
|
|||
import SessionUtilitiesKit
|
||||
|
||||
public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||
public typealias SectionModel = ArraySection<Section, MessageCell.ViewModel>
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
public enum Action {
|
||||
case none
|
||||
case compose
|
||||
|
@ -15,6 +19,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
}
|
||||
|
||||
public static let pageSize: Int = 50
|
||||
// MARK: - Section
|
||||
|
||||
public enum Section: Differentiable, Equatable, Comparable, Hashable {
|
||||
case loadOlder
|
||||
case messages
|
||||
case loadNewer
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
|
@ -34,58 +48,52 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
|
||||
self.threadId = threadId
|
||||
self.threadData = threadData
|
||||
self.focusedInteractionId = focusedInteractionId // TODO: This
|
||||
self.focusedInteractionId = focusedInteractionId
|
||||
self.pagedDataObserver = nil
|
||||
var hasSavedIntialUpdate: Bool = false
|
||||
self.pagedDataObserver = PagedDatabaseObserver(
|
||||
pagedTable: Interaction.self,
|
||||
pageSize: ConversationViewModel.pageSize,
|
||||
idColumn: .id,
|
||||
initialFocusedId: nil,
|
||||
observedChanges: [
|
||||
PagedData.ObservedChanges(
|
||||
table: Interaction.self,
|
||||
columns: Interaction.Columns
|
||||
.allCases
|
||||
.filter { $0 != .wasRead }
|
||||
)
|
||||
],
|
||||
filterSQL: MessageCell.ViewModel.filterSQL(threadId: threadId),
|
||||
orderSQL: MessageCell.ViewModel.orderSQL,
|
||||
dataQuery: MessageCell.ViewModel.baseQuery(
|
||||
|
||||
DispatchQueue.global(qos: .default).async { [weak self] in
|
||||
self?.pagedDataObserver = PagedDatabaseObserver(
|
||||
pagedTable: Interaction.self,
|
||||
pageSize: ConversationViewModel.pageSize,
|
||||
idColumn: .id,
|
||||
initialFocusedId: focusedInteractionId,
|
||||
observedChanges: [
|
||||
PagedData.ObservedChanges(
|
||||
table: Interaction.self,
|
||||
columns: Interaction.Columns
|
||||
.allCases
|
||||
.filter { $0 != .wasRead }
|
||||
)
|
||||
],
|
||||
filterSQL: MessageCell.ViewModel.filterSQL(threadId: threadId),
|
||||
orderSQL: MessageCell.ViewModel.orderSQL,
|
||||
baseFilterSQL: MessageCell.ViewModel.filterSQL(threadId: threadId)
|
||||
),
|
||||
associatedRecords: [
|
||||
AssociatedRecord<MessageCell.AttachmentInteractionInfo, MessageCell.ViewModel>(
|
||||
trackedAgainst: Attachment.self,
|
||||
observedChanges: [
|
||||
PagedData.ObservedChanges(
|
||||
table: Attachment.self,
|
||||
columns: [.state]
|
||||
)
|
||||
],
|
||||
dataQuery: MessageCell.AttachmentInteractionInfo.baseQuery,
|
||||
joinToPagedType: MessageCell.AttachmentInteractionInfo.joinToViewModelQuerySQL,
|
||||
associateData: MessageCell.AttachmentInteractionInfo.createAssociateDataClosure()
|
||||
)
|
||||
],
|
||||
onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
|
||||
guard let updatedInteractionData: [MessageCell.ViewModel] = self?.process(data: updatedData, for: updatedPageInfo) else {
|
||||
return
|
||||
dataQuery: MessageCell.ViewModel.baseQuery(
|
||||
orderSQL: MessageCell.ViewModel.orderSQL,
|
||||
baseFilterSQL: MessageCell.ViewModel.filterSQL(threadId: threadId)
|
||||
),
|
||||
associatedRecords: [
|
||||
AssociatedRecord<MessageCell.AttachmentInteractionInfo, MessageCell.ViewModel>(
|
||||
trackedAgainst: Attachment.self,
|
||||
observedChanges: [
|
||||
PagedData.ObservedChanges(
|
||||
table: Attachment.self,
|
||||
columns: [.state]
|
||||
)
|
||||
],
|
||||
dataQuery: MessageCell.AttachmentInteractionInfo.baseQuery,
|
||||
joinToPagedType: MessageCell.AttachmentInteractionInfo.joinToViewModelQuerySQL,
|
||||
associateData: MessageCell.AttachmentInteractionInfo.createAssociateDataClosure()
|
||||
)
|
||||
],
|
||||
onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
|
||||
guard let updatedInteractionData: [SectionModel] = self?.process(data: updatedData, for: updatedPageInfo) else {
|
||||
return
|
||||
}
|
||||
|
||||
self?.onInteractionChange?(updatedInteractionData)
|
||||
}
|
||||
|
||||
// If we haven't stored the data for the initial fetch then do so now (no need
|
||||
// to call 'onInteractionsChange' in this case as it will always be null)
|
||||
guard hasSavedIntialUpdate else {
|
||||
self?.updateInteractionData(updatedInteractionData)
|
||||
hasSavedIntialUpdate = true
|
||||
return
|
||||
}
|
||||
|
||||
self?.onInteractionChange?(updatedInteractionData)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
@ -130,29 +138,44 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
|
||||
// MARK: - Interaction Data
|
||||
|
||||
public private(set) var interactionData: [MessageCell.ViewModel] = []
|
||||
public private(set) var interactionData: [SectionModel] = []
|
||||
public private(set) var pagedDataObserver: PagedDatabaseObserver<Interaction, MessageCell.ViewModel>?
|
||||
public var onInteractionChange: (([MessageCell.ViewModel]) -> ())?
|
||||
public var onInteractionChange: (([SectionModel]) -> ())?
|
||||
|
||||
private func process(data: [MessageCell.ViewModel], for pageInfo: PagedData.PageInfo) -> [MessageCell.ViewModel] {
|
||||
private func process(data: [MessageCell.ViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] {
|
||||
let sortedData: [MessageCell.ViewModel] = data
|
||||
.sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs }
|
||||
|
||||
return sortedData
|
||||
.enumerated()
|
||||
.map { index, cellViewModel -> MessageCell.ViewModel in
|
||||
cellViewModel.withClusteringChanges(
|
||||
prevModel: (index > 0 ? sortedData[index - 1] : nil),
|
||||
nextModel: (index < (sortedData.count - 2) ? sortedData[index + 1] : nil),
|
||||
isLast: (
|
||||
index == (sortedData.count - 1) &&
|
||||
pageInfo.currentCount == pageInfo.totalCount
|
||||
)
|
||||
return [
|
||||
(!data.isEmpty && (pageInfo.pageOffset + pageInfo.currentCount) < pageInfo.totalCount ?
|
||||
[SectionModel(section: .loadOlder)] :
|
||||
[]
|
||||
),
|
||||
[
|
||||
SectionModel(
|
||||
section: .messages,
|
||||
elements: sortedData
|
||||
.enumerated()
|
||||
.map { index, cellViewModel -> MessageCell.ViewModel in
|
||||
cellViewModel.withClusteringChanges(
|
||||
prevModel: (index > 0 ? sortedData[index - 1] : nil),
|
||||
nextModel: (index < (sortedData.count - 2) ? sortedData[index + 1] : nil),
|
||||
isLast: (
|
||||
index == (sortedData.count - 1) &&
|
||||
pageInfo.currentCount == pageInfo.totalCount
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
],
|
||||
(data.isEmpty && pageInfo.pageOffset > 0 ?
|
||||
[SectionModel(section: .loadNewer)] :
|
||||
[]
|
||||
)
|
||||
].flatMap { $0 }
|
||||
}
|
||||
|
||||
public func updateInteractionData(_ updatedData: [MessageCell.ViewModel]) {
|
||||
public func updateInteractionData(_ updatedData: [SectionModel]) {
|
||||
self.interactionData = updatedData
|
||||
}
|
||||
|
||||
|
@ -288,7 +311,13 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
}
|
||||
|
||||
public func markAllAsRead() {
|
||||
guard let lastInteractionId: Int64 = self.interactionData.last?.id else { return }
|
||||
guard
|
||||
let lastInteractionId: Int64 = self.interactionData
|
||||
.first(where: { $0.model == .messages })?
|
||||
.elements
|
||||
.last?
|
||||
.id
|
||||
else { return }
|
||||
|
||||
GRDBStorage.shared.write { db in
|
||||
try Interaction.markAsRead(
|
||||
|
@ -487,12 +516,15 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
|
||||
// If the next interaction is another voice message then autoplay it
|
||||
guard
|
||||
let currentIndex: Int = self.interactionData.firstIndex(where: { $0.id == interactionId }),
|
||||
currentIndex < (self.interactionData.count - 1),
|
||||
self.interactionData[currentIndex + 1].cellType == .audio
|
||||
let messageSection: SectionModel = self.interactionData
|
||||
.first(where: { $0.model == .messages }),
|
||||
let currentIndex: Int = messageSection.elements
|
||||
.firstIndex(where: { $0.id == interactionId }),
|
||||
currentIndex < (messageSection.elements.count - 1),
|
||||
messageSection.elements[currentIndex + 1].cellType == .audio
|
||||
else { return }
|
||||
|
||||
let nextItem: MessageCell.ViewModel = self.interactionData[currentIndex + 1]
|
||||
let nextItem: MessageCell.ViewModel = messageSection.elements[currentIndex + 1]
|
||||
playOrPauseAudio(for: nextItem)
|
||||
}
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ public class MediaView: UIView {
|
|||
owsFailDebug("Media has unexpected type: \(type(of: media))")
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: Animated images flicker when reloading the cells (even though they are in the cache)
|
||||
animatedImageView.image = image
|
||||
},
|
||||
cacheKey: attachment.id
|
||||
|
@ -365,9 +365,9 @@ public class MediaView: UIView {
|
|||
if let media: AnyObject = self.mediaCache.object(forKey: cacheKey as NSString) {
|
||||
Logger.verbose("media cache hit")
|
||||
|
||||
guard !Thread.isMainThread else {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async {
|
||||
loadMediaBlock(loadCompletion)
|
||||
loadCompletion(media)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -136,10 +136,16 @@ final class QuoteView: UIView {
|
|||
attachment.thumbnail(
|
||||
size: .small,
|
||||
success: { image, _ in
|
||||
DispatchQueue.main.async {
|
||||
imageView.image = image
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async {
|
||||
imageView.image = image
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
imageView.image = image
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
},
|
||||
failure: {}
|
||||
)
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
/// This custom UITableView allows us to lock the contentOffset to a specific value - it's current used to prevent
|
||||
/// the ConversationVC first responder resignation from making the MediaGalleryDetailViewController transition
|
||||
/// from looking buggy (ie. the table scrolls down with the resignation during the transition)
|
||||
/// This custom UITableView gives us two convenience behaviours:
|
||||
///
|
||||
/// 1. It allows us to lock the contentOffset to a specific value - it's currently used to prevent the ConversationVC first
|
||||
/// responder resignation from making the MediaGalleryDetailViewController transition from looking buggy (ie. the table
|
||||
/// scrolls down with the resignation during the transition)
|
||||
///
|
||||
/// 2. It allows us to provode a callback which gets triggered if a condition closure returns true - it's currently used to prevent
|
||||
/// the table view from jumping when inserting new pages at the top of a conversation screen
|
||||
public class InsetLockableTableView: UITableView {
|
||||
public var lockContentOffset: Bool = false {
|
||||
didSet {
|
||||
|
@ -15,6 +20,8 @@ public class InsetLockableTableView: UITableView {
|
|||
}
|
||||
public var oldOffset: CGPoint = .zero
|
||||
public var newOffset: CGPoint = .zero
|
||||
private var afterNextLayoutCondition: ((Int, [Int]) -> Bool)?
|
||||
private var afterNextLayoutCallback: (() -> ())?
|
||||
|
||||
public override func layoutSubviews() {
|
||||
newOffset = self.contentOffset
|
||||
|
@ -24,12 +31,35 @@ public class InsetLockableTableView: UITableView {
|
|||
x: newOffset.x,
|
||||
y: oldOffset.y
|
||||
)
|
||||
|
||||
super.layoutSubviews()
|
||||
|
||||
self.performNextLayoutCallbackIfPossible()
|
||||
return
|
||||
}
|
||||
|
||||
super.layoutSubviews()
|
||||
|
||||
oldOffset = self.contentOffset
|
||||
self.performNextLayoutCallbackIfPossible()
|
||||
self.oldOffset = self.contentOffset
|
||||
}
|
||||
|
||||
// MARK: - Function
|
||||
|
||||
public func afterNextLayout(when condition: @escaping (Int, [Int]) -> Bool, then callback: @escaping () -> ()) {
|
||||
self.afterNextLayoutCondition = condition
|
||||
self.afterNextLayoutCallback = callback
|
||||
}
|
||||
|
||||
private func performNextLayoutCallbackIfPossible() {
|
||||
let numSections: Int = self.numberOfSections
|
||||
let numRowInSections: [Int] = (0..<numSections)
|
||||
.map { self.numberOfRows(inSection: $0) }
|
||||
|
||||
guard self.afterNextLayoutCondition?(numSections, numRowInSections) == true else { return }
|
||||
|
||||
self.afterNextLayoutCallback?()
|
||||
self.afterNextLayoutCondition = nil
|
||||
self.afterNextLayoutCallback = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ class EmptySearchResultCell: UITableViewCell {
|
|||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
|
||||
contentView.addSubview(messageLabel)
|
||||
messageLabel.autoSetDimension(.height, toSize: 150)
|
||||
|
|
|
@ -9,13 +9,35 @@ import SessionUtilitiesKit
|
|||
import SignalUtilitiesKit
|
||||
|
||||
class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
||||
private struct SearchResultSet {
|
||||
let contactsAndGroups: [ConversationCell.ViewModel]
|
||||
let messages: [ConversationCell.ViewModel]
|
||||
fileprivate typealias SectionModel = ArraySection<SearchSection, ConversationCell.ViewModel>
|
||||
|
||||
// MARK: - SearchSection
|
||||
|
||||
enum SearchSection: Int, Differentiable {
|
||||
case noResults
|
||||
case contactsAndGroups
|
||||
case messages
|
||||
}
|
||||
|
||||
let isRecentSearchResultsEnabled = false
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
private lazy var defaultSearchResults: [SectionModel] = {
|
||||
let result: ConversationCell.ViewModel? = GRDBStorage.shared.read { db -> ConversationCell.ViewModel? in
|
||||
try ConversationCell.ViewModel
|
||||
.noteToSelfOnlyQuery(userPublicKey: getUserHexEncodedPublicKey(db))
|
||||
.fetchOne(db)
|
||||
}
|
||||
|
||||
return [ result.map { ArraySection(model: .contactsAndGroups, elements: [$0]) } ]
|
||||
.compactMap { $0 }
|
||||
}()
|
||||
private lazy var searchResultSet: [SectionModel] = self.defaultSearchResults
|
||||
private var termForCurrentSearchResultSet: String = ""
|
||||
private var lastSearchText: String?
|
||||
private var refreshTimer: Timer?
|
||||
|
||||
var isLoading = false
|
||||
|
||||
@objc public var searchText = "" {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
@ -23,23 +45,6 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
refreshSearchResults()
|
||||
}
|
||||
}
|
||||
var defaultSearchResults: HomeScreenSearchResultSet = HomeScreenSearchResultSet.noteToSelfOnly
|
||||
|
||||
var searchResultSet: [ArraySection<SearchSection, ConversationCell.ViewModel>] = []
|
||||
private var termForCurrentSearchResultSet: String = ""
|
||||
|
||||
|
||||
private var lastSearchText: String?
|
||||
var searcher: FullTextSearcher {
|
||||
return FullTextSearcher.shared
|
||||
}
|
||||
var isLoading = false
|
||||
|
||||
enum SearchSection: Int, Differentiable {
|
||||
case noResults
|
||||
case contactsAndGroups
|
||||
case messages
|
||||
}
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
|
@ -114,8 +119,6 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
|
||||
// MARK: - Update Search Results
|
||||
|
||||
var refreshTimer: Timer?
|
||||
|
||||
private func refreshSearchResults() {
|
||||
refreshTimer?.invalidate()
|
||||
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
|
||||
|
@ -136,49 +139,55 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
|
||||
lastSearchText = searchText
|
||||
|
||||
GRDBStorage.shared
|
||||
.read { db -> Result<SearchResultSet, Error> in
|
||||
do {
|
||||
let contactsAndGroupsResults: [ConversationCell.ViewModel] = try ConversationCell.ViewModel
|
||||
.contactsAndGroupsQuery(
|
||||
userPublicKey: getUserHexEncodedPublicKey(db),
|
||||
pattern: try ConversationCell.ViewModel.pattern(db, searchTerm: searchText),
|
||||
searchTerm: searchText
|
||||
)
|
||||
.fetchAll(db)
|
||||
|
||||
let messageResults: [ConversationCell.ViewModel] = try ConversationCell.ViewModel
|
||||
.messagesQuery(
|
||||
userPublicKey: getUserHexEncodedPublicKey(db),
|
||||
pattern: try ConversationCell.ViewModel.pattern(db, searchTerm: searchText)
|
||||
)
|
||||
.fetchAll(db)
|
||||
|
||||
return .success(SearchResultSet(
|
||||
contactsAndGroups: contactsAndGroupsResults,
|
||||
messages: messageResults
|
||||
))
|
||||
}
|
||||
catch {
|
||||
return .failure(error)
|
||||
}
|
||||
let result: Result<[SectionModel], Error>? = GRDBStorage.shared.read { db -> Result<[SectionModel], Error> in
|
||||
do {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
let contactsAndGroupsResults: [ConversationCell.ViewModel] = try ConversationCell.ViewModel
|
||||
.contactsAndGroupsQuery(
|
||||
userPublicKey: userPublicKey,
|
||||
pattern: try ConversationCell.ViewModel.pattern(db, searchTerm: searchText),
|
||||
searchTerm: searchText
|
||||
)
|
||||
.fetchAll(db)
|
||||
|
||||
let messageResults: [ConversationCell.ViewModel] = try ConversationCell.ViewModel
|
||||
.messagesQuery(
|
||||
userPublicKey: userPublicKey,
|
||||
pattern: try ConversationCell.ViewModel.pattern(db, searchTerm: searchText)
|
||||
)
|
||||
.fetchAll(db)
|
||||
|
||||
return .success([
|
||||
ArraySection(model: .contactsAndGroups, elements: contactsAndGroupsResults),
|
||||
ArraySection(model: .messages, elements: messageResults)
|
||||
])
|
||||
}
|
||||
.map { [weak self] result in
|
||||
switch result {
|
||||
case .success(let resultSet):
|
||||
self?.termForCurrentSearchResultSet = searchText
|
||||
self?.searchResultSet = [
|
||||
ArraySection(model: .contactsAndGroups, elements: resultSet.contactsAndGroups),
|
||||
ArraySection(model: .messages, elements: resultSet.messages)
|
||||
]
|
||||
self?.isLoading = false
|
||||
self?.reloadTableData()
|
||||
self?.refreshTimer = nil
|
||||
|
||||
|
||||
case .failure: break
|
||||
}
|
||||
catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let sections):
|
||||
let hasResults: Bool = (
|
||||
!searchText.isEmpty &&
|
||||
(sections.map { $0.elements.count }.reduce(0, +) > 0)
|
||||
)
|
||||
|
||||
self.termForCurrentSearchResultSet = searchText
|
||||
self.searchResultSet = [
|
||||
(hasResults ? nil : [ArraySection(model: .noResults, elements: [ConversationCell.ViewModel(unreadCount: 0)])]),
|
||||
(hasResults ? sections : nil)
|
||||
]
|
||||
.compactMap { $0 }
|
||||
.flatMap { $0 }
|
||||
self.isLoading = false
|
||||
self.reloadTableData()
|
||||
self.refreshTimer = nil
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,30 +227,40 @@ extension GlobalSearchViewController {
|
|||
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
|
||||
guard let searchSection = SearchSection(rawValue: indexPath.section) else { return }
|
||||
let section: SectionModel = self.searchResultSet[indexPath.section]
|
||||
|
||||
switch searchSection {
|
||||
case .noResults:
|
||||
SNLog("shouldn't be able to tap 'no results' section")
|
||||
|
||||
case .contactsAndGroups:
|
||||
break
|
||||
|
||||
case .messages:
|
||||
break
|
||||
switch section.model {
|
||||
case .noResults: break
|
||||
case .contactsAndGroups, .messages:
|
||||
show(
|
||||
threadId: section.elements[indexPath.row].threadId,
|
||||
focusedInteractionId: section.elements[indexPath.row].interactionId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func show(_ thread: TSThread, highlightedMessageID: String?, animated: Bool, isFromRecent: Bool = false) {
|
||||
if let presentedVC = self.presentedViewController {
|
||||
presentedVC.dismiss(animated: false, completion: nil)
|
||||
private func show(threadId: String, focusedInteractionId: Int64? = nil, animated: Bool = true) {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.show(threadId: threadId, focusedInteractionId: focusedInteractionId, animated: animated)
|
||||
}
|
||||
let conversationVC = ConversationVC(thread: thread, focusedMessageID: highlightedMessageID)
|
||||
var viewControllers = self.navigationController?.viewControllers
|
||||
if isFromRecent, let index = viewControllers?.firstIndex(of: self) { viewControllers?.remove(at: index) }
|
||||
viewControllers?.append(conversationVC)
|
||||
self.navigationController?.setViewControllers(viewControllers!, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
guard let conversationVC: ConversationVC = ConversationVC(threadId: threadId, focusedInteractionId: focusedInteractionId) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let presentedVC = self.presentedViewController {
|
||||
presentedVC.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
let viewControllers: [UIViewController] = (self.navigationController?
|
||||
.viewControllers)
|
||||
.defaulting(to: [])
|
||||
.appending(conversationVC)
|
||||
|
||||
self.navigationController?.setViewControllers(viewControllers, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
@ -249,6 +268,10 @@ extension GlobalSearchViewController {
|
|||
public func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return self.searchResultSet.count
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return self.searchResultSet[section].elements.count
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
UIView()
|
||||
|
@ -286,7 +309,8 @@ extension GlobalSearchViewController {
|
|||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let section: ArraySection<SearchSection, ConversationCell.ViewModel> = self.searchResultSet[section]
|
||||
let section: SectionModel = self.searchResultSet[section]
|
||||
|
||||
switch section.model {
|
||||
case .noResults: return nil
|
||||
case .contactsAndGroups: return (section.elements.isEmpty ? nil : "SEARCH_SECTION_CONTACTS".localized())
|
||||
|
@ -294,16 +318,12 @@ extension GlobalSearchViewController {
|
|||
}
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return self.searchResultSet[section].elements.count
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let section: ArraySection<SearchSection, ConversationCell.ViewModel> = self.searchResultSet[indexPath.section]
|
||||
let section: SectionModel = self.searchResultSet[indexPath.section]
|
||||
|
||||
switch section.model {
|
||||
case .noResults:
|
||||
|
|
|
@ -18,6 +18,8 @@ public class MediaGalleryViewModel {
|
|||
case loadNewer
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
public let threadId: String
|
||||
public let threadVariant: SessionThread.Variant
|
||||
private var focusedAttachmentId: String?
|
||||
|
@ -30,7 +32,7 @@ public class MediaGalleryViewModel {
|
|||
public var interactionIdBefore: [Int64: Int64] { cachedInteractionIdBefore.wrappedValue }
|
||||
public var interactionIdAfter: [Int64: Int64] { cachedInteractionIdAfter.wrappedValue }
|
||||
public private(set) var albumData: [Int64: [Item]] = [:]
|
||||
public private(set) var pagedDatabaseObserver: PagedDatabaseObserver<Attachment, Item>?
|
||||
public private(set) var pagedDataObserver: PagedDatabaseObserver<Attachment, Item>?
|
||||
|
||||
/// This value is the current state of a gallery view
|
||||
public private(set) var galleryData: [SectionModel] = []
|
||||
|
@ -48,13 +50,13 @@ public class MediaGalleryViewModel {
|
|||
self.threadId = threadId
|
||||
self.threadVariant = threadVariant
|
||||
self.focusedAttachmentId = focusedAttachmentId
|
||||
self.pagedDatabaseObserver = nil
|
||||
self.pagedDataObserver = nil
|
||||
|
||||
guard isPagedData else { return }
|
||||
|
||||
var hasSavedIntialUpdate: Bool = false
|
||||
let filterSQL: SQL = Item.filterSQL(threadId: threadId)
|
||||
self.pagedDatabaseObserver = PagedDatabaseObserver(
|
||||
self.pagedDataObserver = PagedDatabaseObserver(
|
||||
pagedTable: Attachment.self,
|
||||
pageSize: pageSize,
|
||||
idColumn: .id,
|
||||
|
@ -433,14 +435,6 @@ public class MediaGalleryViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
public func loadNewerGalleryItems() {
|
||||
self.pagedDatabaseObserver?.load(.pageBefore)
|
||||
}
|
||||
|
||||
public func loadOlderGalleryItems() {
|
||||
self.pagedDatabaseObserver?.load(.pageAfter)
|
||||
}
|
||||
|
||||
public func updateFocusedItem(attachmentId: String, indexPath: IndexPath) {
|
||||
// Note: We need to set both of these as the 'focusedIndexPath' is usually
|
||||
// derived and if the data changes it will be regenerated using the
|
||||
|
|
|
@ -20,6 +20,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
|
||||
private let viewModel: MediaGalleryViewModel
|
||||
private var hasLoadedInitialData: Bool = false
|
||||
private var isAutoLoadingNextPage: Bool = false
|
||||
private var currentTargetOffset: CGPoint?
|
||||
|
||||
var isInBatchSelectMode = false {
|
||||
|
@ -34,7 +35,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
|
||||
init(viewModel: MediaGalleryViewModel) {
|
||||
self.viewModel = viewModel
|
||||
GRDBStorage.shared.addObserver(viewModel.pagedDatabaseObserver)
|
||||
GRDBStorage.shared.addObserver(viewModel.pagedDataObserver)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
@ -212,20 +213,30 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
}
|
||||
|
||||
private func autoLoadNextPageIfNeeded() {
|
||||
guard !self.isAutoLoadingNextPage else { return }
|
||||
|
||||
self.isAutoLoadingNextPage = true
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + MediaTileViewController.autoLoadNextPageDelay) { [weak self] in
|
||||
self?.isAutoLoadingNextPage = false
|
||||
|
||||
// Note: We sort the headers as we want to prioritise loading newer pages over older ones
|
||||
let sortedVisibleIndexPaths: [IndexPath] = (self?.collectionView
|
||||
.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader))
|
||||
.defaulting(to: [])
|
||||
.sorted()
|
||||
|
||||
for headerIndexPath in sortedVisibleIndexPaths {
|
||||
switch self?.viewModel.galleryData[safe: headerIndexPath.section]?.model {
|
||||
case .loadNewer:
|
||||
self?.viewModel.loadNewerGalleryItems()
|
||||
return
|
||||
|
||||
case .loadOlder:
|
||||
self?.viewModel.loadOlderGalleryItems()
|
||||
let section: MediaGalleryViewModel.SectionModel? = self?.viewModel.galleryData[safe: headerIndexPath.section]
|
||||
|
||||
switch section?.model {
|
||||
case .loadNewer, .loadOlder:
|
||||
// Attachments are loaded in descending order so 'loadOlder' actually corresponds with
|
||||
// 'pageAfter' in this case
|
||||
self?.viewModel.pagedDataObserver?.load(section?.model == .loadOlder ?
|
||||
.pageAfter :
|
||||
.pageBefore
|
||||
)
|
||||
return
|
||||
|
||||
default: continue
|
||||
|
@ -242,7 +253,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
}
|
||||
|
||||
private func stopObservingChanges() {
|
||||
// Note: The 'PagedDatabaseObserver' will continue to get changes but
|
||||
// Note: The 'pagedDataObserver' will continue to get changes but
|
||||
// we don't want to trigger any UI updates
|
||||
self.viewModel.onGalleryChange = nil
|
||||
}
|
||||
|
@ -261,8 +272,8 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
// Determine if we are inserting content at the top of the collectionView
|
||||
let isInsertingAtTop: Bool = {
|
||||
let oldFirstSectionIsLoadMore: Bool = (
|
||||
self.viewModel.galleryData[safe: 0]?.model == .loadNewer ||
|
||||
self.viewModel.galleryData[safe: 0]?.model == .loadOlder
|
||||
self.viewModel.galleryData.first?.model == .loadNewer ||
|
||||
self.viewModel.galleryData.first?.model == .loadOlder
|
||||
)
|
||||
let oldTargetSectionIndex: Int = (oldFirstSectionIsLoadMore ? 1 : 0)
|
||||
|
||||
|
@ -399,41 +410,17 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
guard self.hasLoadedInitialData else { return }
|
||||
|
||||
let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[indexPath.section]
|
||||
let fastEndScrollingThen: ((@escaping () -> ()) -> ()) = { callback in
|
||||
let endOffset: CGPoint
|
||||
|
||||
if let currentTargetOffset: CGPoint = self.currentTargetOffset {
|
||||
endOffset = currentTargetOffset
|
||||
}
|
||||
else {
|
||||
let currentVelocity: CGPoint = collectionView.panGestureRecognizer.velocity(in: collectionView)
|
||||
|
||||
endOffset = CGPoint(
|
||||
x: collectionView.contentOffset.x,
|
||||
y: collectionView.contentOffset.y - (currentVelocity.y / 100)
|
||||
)
|
||||
}
|
||||
|
||||
guard endOffset != collectionView.contentOffset else {
|
||||
return callback()
|
||||
}
|
||||
|
||||
UIView.animate(
|
||||
withDuration: 0.1,
|
||||
delay: 0,
|
||||
options: .curveEaseOut,
|
||||
animations: {
|
||||
collectionView.setContentOffset(endOffset, animated: false)
|
||||
},
|
||||
completion: { _ in
|
||||
callback()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
switch section.model {
|
||||
case .loadOlder: fastEndScrollingThen { self.viewModel.loadOlderGalleryItems() }
|
||||
case .loadNewer: fastEndScrollingThen { self.viewModel.loadNewerGalleryItems() }
|
||||
case .loadOlder, .loadNewer:
|
||||
UIScrollView.fastEndScrollingThen(collectionView, self.currentTargetOffset) { [weak self] in
|
||||
// Attachments are loaded in descending order so 'loadOlder' actually corresponds with
|
||||
// 'pageAfter' in this case
|
||||
self?.viewModel.pagedDataObserver?.load(section.model == .loadOlder ?
|
||||
.pageAfter :
|
||||
.pageBefore
|
||||
)
|
||||
}
|
||||
|
||||
case .emptyGallery, .galleryMonth: break
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UIScrollView {
|
||||
static let fastEndScrollingThen: ((UIScrollView, CGPoint?, @escaping () -> ()) -> ()) = { scrollView, currentTargetOffset, callback in
|
||||
let endOffset: CGPoint
|
||||
|
||||
if let currentTargetOffset: CGPoint = currentTargetOffset {
|
||||
endOffset = currentTargetOffset
|
||||
}
|
||||
else {
|
||||
let currentVelocity: CGPoint = scrollView.panGestureRecognizer.velocity(in: scrollView)
|
||||
|
||||
endOffset = CGPoint(
|
||||
x: scrollView.contentOffset.x,
|
||||
y: scrollView.contentOffset.y - (currentVelocity.y / 100)
|
||||
)
|
||||
}
|
||||
|
||||
guard endOffset != scrollView.contentOffset else {
|
||||
return callback()
|
||||
}
|
||||
|
||||
UIView.animate(
|
||||
withDuration: 0.1,
|
||||
delay: 0,
|
||||
options: .curveEaseOut,
|
||||
animations: {
|
||||
scrollView.setContentOffset(endOffset, animated: false)
|
||||
},
|
||||
completion: { _ in
|
||||
callback()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -198,9 +198,9 @@ extension ConversationCell {
|
|||
// MARK: - Convenience Initialization
|
||||
|
||||
public extension ConversationCell.ViewModel {
|
||||
// Note: This init method is only used for the message requests cell on the home screen so we can avoid having
|
||||
init(unreadCount: UInt) {
|
||||
self.threadId = "UNREAD_MESSAGE_REQUEST_THREADS"
|
||||
// Note: This init method is only used for the message requests cell or empty states
|
||||
init(unreadCount: UInt = 0) {
|
||||
self.threadId = "INVALID_THREAD_ID"
|
||||
self.threadVariant = .contact
|
||||
self.threadCreationDateTimestamp = 0
|
||||
self.threadMemberNames = nil
|
||||
|
@ -1175,6 +1175,51 @@ public extension ConversationCell.ViewModel {
|
|||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// This method returns only the 'Note to Self' thread in the structure of a search result conversation
|
||||
static func noteToSelfOnlyQuery(userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<ConversationCell.ViewModel>> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
||||
|
||||
/// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before
|
||||
/// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to
|
||||
/// parse and might throw
|
||||
let numColumnsBeforeProfiles: Int = 7
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
100 AS \(Column.rank),
|
||||
|
||||
\(thread[.id]) AS \(ViewModel.threadIdKey),
|
||||
\(thread[.variant]) AS \(ViewModel.threadVariantKey),
|
||||
\(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey),
|
||||
'' AS \(ViewModel.threadMemberNamesKey),
|
||||
|
||||
true AS \(ViewModel.threadIsNoteToSelfKey),
|
||||
\(thread[.isPinned]) AS \(ViewModel.threadIsPinnedKey),
|
||||
|
||||
\(ViewModel.contactProfileKey).*,
|
||||
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
|
||||
|
||||
FROM \(SessionThread.self)
|
||||
JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
|
||||
WHERE \(SQL("\(thread[.id]) = \(userPublicKey)"))
|
||||
"""
|
||||
|
||||
// Add adapters which will group the various 'Profile' columns so they can be decoded
|
||||
// as instances of 'Profile' types
|
||||
return request.adapted { db in
|
||||
let adapters = try splittingRowAdapters(columnCounts: [
|
||||
numColumnsBeforeProfiles,
|
||||
Profile.numberOfSelectedColumns(db)
|
||||
])
|
||||
|
||||
return ScopeAdapter([
|
||||
ViewModel.contactProfileString: adapters[1]
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Share Extension
|
||||
|
|
|
@ -450,6 +450,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
|
|||
if let updatedPageInfo: PagedData.PageInfo = loadedPage?.pageInfo {
|
||||
self.pageInfo.mutate { $0 = updatedPageInfo }
|
||||
}
|
||||
self.isLoadingMoreData.mutate { $0 = false }
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -137,7 +137,7 @@ public extension UIViewController {
|
|||
}
|
||||
|
||||
func presentAlert(_ alert: UIAlertController, animated: Bool) {
|
||||
if !Thread.isMainThread {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.presentAlert(alert, animated: animated)
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ public extension UIViewController {
|
|||
}
|
||||
|
||||
func presentAlert(_ alert: UIAlertController, completion: @escaping (() -> Void)) {
|
||||
if !Thread.isMainThread {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.presentAlert(alert, completion: completion)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue