Merge branch 'dev' into fix/safari-sharing
# Conflicts: # Podfile.lock
This commit is contained in:
commit
4d62ddbd77
|
@ -204,6 +204,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 19ce2820c263e8f3c114817f7ca2da73a9382b6a
|
||||
PODFILE CHECKSUM: 7f961dc4934dd213f5a3277af57d54caef7a4442
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -137,6 +137,9 @@
|
|||
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
|
||||
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
|
||||
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; };
|
||||
7BA7F4BB279F9F5800B3A466 /* EmptySearchResultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7F4BA279F9F5800B3A466 /* EmptySearchResultCell.swift */; };
|
||||
7BA7F4BD27A216B600B3A466 /* Storage+RecentSearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7F4BC27A216B600B3A466 /* Storage+RecentSearchResults.swift */; };
|
||||
7BA9057E27911C5800998B3C /* GlobalSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA9057D27911C5800998B3C /* GlobalSearchViewController.swift */; };
|
||||
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; };
|
||||
7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; };
|
||||
|
@ -1114,6 +1117,9 @@
|
|||
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
|
||||
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
|
||||
7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = "<group>"; };
|
||||
7BA7F4BA279F9F5800B3A466 /* EmptySearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptySearchResultCell.swift; sourceTree = "<group>"; };
|
||||
7BA7F4BC27A216B600B3A466 /* Storage+RecentSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+RecentSearchResults.swift"; sourceTree = "<group>"; };
|
||||
7BA9057D27911C5800998B3C /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = "<group>"; };
|
||||
7BA6F47DAD18D44D75B7110F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
|
||||
|
@ -2047,6 +2053,16 @@
|
|||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7BA9057D27911C5800998B3C /* GlobalSearchViewController.swift */,
|
||||
7BA7F4BA279F9F5800B3A466 /* EmptySearchResultCell.swift */,
|
||||
7BA7F4BC27A216B600B3A466 /* Storage+RecentSearchResults.swift */,
|
||||
);
|
||||
path = GlobalSearch;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7BC01A3C241F40AB00BC7C55 /* SessionNotificationServiceExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2808,6 +2824,7 @@
|
|||
children = (
|
||||
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
|
||||
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
|
||||
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */,
|
||||
);
|
||||
path = Home;
|
||||
sourceTree = "<group>";
|
||||
|
@ -4867,6 +4884,7 @@
|
|||
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
|
||||
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
|
||||
B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */,
|
||||
7BA9057E27911C5800998B3C /* GlobalSearchViewController.swift in Sources */,
|
||||
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
|
||||
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,
|
||||
|
@ -4924,6 +4942,7 @@
|
|||
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
|
||||
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */,
|
||||
C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */,
|
||||
7BA7F4BB279F9F5800B3A466 /* EmptySearchResultCell.swift in Sources */,
|
||||
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
|
||||
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */,
|
||||
|
@ -4951,6 +4970,7 @@
|
|||
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */,
|
||||
B90418E6183E9DD40038554A /* DateUtil.m in Sources */,
|
||||
C33100092558FF6D00070591 /* UserCell.swift in Sources */,
|
||||
7BA7F4BD27A216B600B3A466 /* Storage+RecentSearchResults.swift in Sources */,
|
||||
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */,
|
||||
C374EEE225DA26740073A857 /* LinkPreviewModal.swift in Sources */,
|
||||
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */,
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
|
||||
let isUnsendRequestsEnabled = true // Set to true once unsend requests are done on all platforms
|
||||
let thread: TSThread
|
||||
let focusedMessageID: String? // This isn't actually used ATM
|
||||
let focusedMessageID: String? // This is used for global search
|
||||
var focusedMessageIndexPath: IndexPath?
|
||||
var unreadViewItems: [ConversationViewItem] = []
|
||||
var scrollButtonConstraint: NSLayoutConstraint?
|
||||
// Search
|
||||
|
@ -236,13 +237,17 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
// unreadIndicatorIndex is calculated during loading of the viewItems, so it's
|
||||
// supposed to be accurate.
|
||||
DispatchQueue.main.async {
|
||||
let firstUnreadMessageIndex = self.viewModel.viewState.unreadIndicatorIndex?.intValue
|
||||
?? (self.viewItems.count - self.unreadViewItems.count)
|
||||
if unreadCount > 0, let viewItem = self.viewItems[ifValid: firstUnreadMessageIndex], let interactionID = viewItem.interaction.uniqueId {
|
||||
self.scrollToInteraction(with: interactionID, position: .top, isAnimated: false)
|
||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||
if let focusedMessageID = self.focusedMessageID {
|
||||
self.scrollToInteraction(with: focusedMessageID, isAnimated: false, highlighted: true)
|
||||
} else {
|
||||
self.scrollToBottom(isAnimated: false)
|
||||
let firstUnreadMessageIndex = self.viewModel.viewState.unreadIndicatorIndex?.intValue
|
||||
?? (self.viewItems.count - self.unreadViewItems.count)
|
||||
if unreadCount > 0, let viewItem = self.viewItems[ifValid: firstUnreadMessageIndex], let interactionID = viewItem.interaction.uniqueId {
|
||||
self.scrollToInteraction(with: interactionID, position: .top, isAnimated: false)
|
||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||
} else {
|
||||
self.scrollToBottom(isAnimated: false)
|
||||
}
|
||||
}
|
||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
||||
}
|
||||
|
@ -251,6 +256,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
highlightFocusedMessageIfNeeded()
|
||||
didFinishInitialLayout = true
|
||||
markAllAsRead()
|
||||
}
|
||||
|
@ -313,6 +319,13 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
}
|
||||
}
|
||||
|
||||
private func highlightFocusedMessageIfNeeded() {
|
||||
if let indexPath = focusedMessageIndexPath, let cell = messagesTableView.cellForRow(at: indexPath) as? VisibleMessageCell {
|
||||
cell.highlight()
|
||||
focusedMessageIndexPath = nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
||||
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
||||
if (newHeight > 0 && baselineKeyboardHeight == 0) {
|
||||
|
@ -538,6 +551,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
func showSearchUI() {
|
||||
isShowingSearchUI = true
|
||||
// Search bar
|
||||
// FIXME: This code is duplicated with SearchBar
|
||||
let searchBar = searchController.uiSearchController.searchBar
|
||||
searchBar.searchBarStyle = .minimal
|
||||
searchBar.barStyle = .black
|
||||
|
@ -616,8 +630,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
|||
scrollToInteraction(with: interactionID)
|
||||
}
|
||||
|
||||
func scrollToInteraction(with interactionID: String, position: UITableView.ScrollPosition = .middle, isAnimated: Bool = true) {
|
||||
func scrollToInteraction(with interactionID: String, position: UITableView.ScrollPosition = .middle, isAnimated: Bool = true, highlighted: Bool = false) {
|
||||
guard let indexPath = viewModel.ensureLoadWindowContainsInteractionId(interactionID) else { return }
|
||||
messagesTableView.scrollToRow(at: indexPath, at: position, animated: isAnimated)
|
||||
if highlighted {
|
||||
focusedMessageIndexPath = indexPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
|
||||
lazy var bubbleView: UIView = {
|
||||
let result = UIView()
|
||||
result.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
|
||||
result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -431,10 +431,12 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
}
|
||||
|
||||
private func updateBubbleViewCorners() {
|
||||
let maskPath = UIBezierPath(roundedRect: bubbleView.bounds, byRoundingCorners: getCornersToRound(),
|
||||
let cornersToRound = getCornersToRound()
|
||||
let maskPath = UIBezierPath(roundedRect: bubbleView.bounds, byRoundingCorners: cornersToRound,
|
||||
cornerRadii: CGSize(width: VisibleMessageCell.largeCornerRadius, height: VisibleMessageCell.largeCornerRadius))
|
||||
bubbleViewMaskLayer.path = maskPath.cgPath
|
||||
bubbleView.layer.mask = bubbleViewMaskLayer
|
||||
bubbleView.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
|
||||
bubbleView.layer.maskedCorners = getCornerMask(from: cornersToRound)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
|
@ -470,6 +472,18 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
return abs(v.x) > abs(v.y) // It has to be more horizontal than vertical
|
||||
} else {
|
||||
return true
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func highlight() {
|
||||
let shawdowColour = isLightMode ? UIColor.black.cgColor : Colors.accent.cgColor
|
||||
let opacity : Float = isLightMode ? 0.5 : 1
|
||||
bubbleView.setShadow(radius: 10, opacity: opacity, offset: .zero, color: shawdowColour)
|
||||
DispatchQueue.main.async {
|
||||
UIView.animate(withDuration: 1.6) {
|
||||
self.bubbleView.setShadow(radius: 0, opacity: 0, offset: .zero, color: UIColor.clear.cgColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -571,6 +585,19 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
return result
|
||||
}
|
||||
|
||||
private func getCornerMask(from rectCorner: UIRectCorner) -> CACornerMask {
|
||||
var cornerMask = CACornerMask()
|
||||
if rectCorner.contains(.allCorners) {
|
||||
cornerMask = [ .layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
|
||||
} else {
|
||||
if rectCorner.contains(.topRight) { cornerMask.insert(.layerMaxXMinYCorner) }
|
||||
if rectCorner.contains(.topLeft) { cornerMask.insert(.layerMinXMinYCorner) }
|
||||
if rectCorner.contains(.bottomRight) { cornerMask.insert(.layerMaxXMaxYCorner) }
|
||||
if rectCorner.contains(.bottomLeft) { cornerMask.insert(.layerMinXMaxYCorner) }
|
||||
}
|
||||
return cornerMask
|
||||
}
|
||||
|
||||
private static func getFontSize(for viewItem: ConversationViewItem) -> CGFloat {
|
||||
let baselineFontSize = Values.mediumFontSize
|
||||
switch viewItem.displayableBodyText?.jumbomojiCount {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import NVActivityIndicatorView
|
||||
|
||||
class EmptySearchResultCell: UITableViewCell {
|
||||
static let reuseIdentifier = "EmptySearchResultCell"
|
||||
|
||||
private lazy var messageLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textAlignment = .center
|
||||
result.numberOfLines = 3
|
||||
result.textColor = Colors.text
|
||||
result.text = NSLocalizedString("CONVERSATION_SEARCH_NO_RESULTS", comment: "")
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var spinner: NVActivityIndicatorView = {
|
||||
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
|
||||
result.set(.width, to: 40)
|
||||
result.set(.height, to: 40)
|
||||
return result
|
||||
}()
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
backgroundColor = .clear
|
||||
|
||||
contentView.addSubview(messageLabel)
|
||||
messageLabel.autoSetDimension(.height, toSize: 150)
|
||||
messageLabel.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual)
|
||||
messageLabel.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual)
|
||||
messageLabel.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual)
|
||||
messageLabel.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual)
|
||||
messageLabel.autoVCenterInSuperview()
|
||||
messageLabel.autoHCenterInSuperview()
|
||||
messageLabel.setContentHuggingHigh()
|
||||
messageLabel.setCompressionResistanceHigh()
|
||||
|
||||
contentView.addSubview(spinner)
|
||||
spinner.autoCenterInSuperview()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
public func configure(isLoading: Bool) {
|
||||
if isLoading {
|
||||
// Calling stopAnimating() here is a workaround for
|
||||
// the spinner won't change its colour as the theme changed.
|
||||
spinner.stopAnimating()
|
||||
spinner.startAnimating()
|
||||
messageLabel.isHidden = true
|
||||
} else {
|
||||
spinner.stopAnimating()
|
||||
messageLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
@objc
|
||||
class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
let isRecentSearchResultsEnabled = false
|
||||
|
||||
@objc public var searchText = "" {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
// Use a slight delay to debounce updates.
|
||||
refreshSearchResults()
|
||||
}
|
||||
}
|
||||
var recentSearchResults: [String] = Array(Storage.shared.getRecentSearchResults().reversed())
|
||||
var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty
|
||||
private var lastSearchText: String?
|
||||
var searcher: FullTextSearcher {
|
||||
return FullTextSearcher.shared
|
||||
}
|
||||
var isLoading = false
|
||||
|
||||
enum SearchSection: Int {
|
||||
case noResults
|
||||
case contacts
|
||||
case messages
|
||||
case recent
|
||||
}
|
||||
|
||||
// MARK: UI Components
|
||||
|
||||
internal lazy var searchBar: SearchBar = {
|
||||
let result = SearchBar()
|
||||
result.tintColor = Colors.text
|
||||
result.delegate = self
|
||||
result.showsCancelButton = true
|
||||
return result
|
||||
}()
|
||||
|
||||
internal lazy var tableView: UITableView = {
|
||||
let result = UITableView(frame: .zero, style: .grouped)
|
||||
result.rowHeight = UITableView.automaticDimension
|
||||
result.estimatedRowHeight = 60
|
||||
result.separatorStyle = .none
|
||||
result.keyboardDismissMode = .onDrag
|
||||
result.register(EmptySearchResultCell.self, forCellReuseIdentifier: EmptySearchResultCell.reuseIdentifier)
|
||||
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
|
||||
result.showsVerticalScrollIndicator = false
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
var dbReadConnection: YapDatabaseConnection {
|
||||
return OWSPrimaryStorage.shared().dbReadConnection
|
||||
}
|
||||
|
||||
// MARK: View Lifecycle
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setUpGradientBackground()
|
||||
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(.leading, to: .leading, of: view)
|
||||
tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
||||
tableView.pin(.trailing, to: .trailing, of: view)
|
||||
tableView.pin(.bottom, to: .bottom, of: view)
|
||||
|
||||
navigationItem.hidesBackButton = true
|
||||
setupNavigationBar()
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
searchBar.becomeFirstResponder()
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
private func setupNavigationBar() {
|
||||
// This is a workaround for a UI issue that the navigation bar can be a bit higher if
|
||||
// the search bar is put directly to be the titleView. And this can cause the tableView
|
||||
// in home screen doing a weird scrolling when going back to home screen.
|
||||
let searchBarContainer = UIView()
|
||||
searchBarContainer.layoutMargins = UIEdgeInsets.zero
|
||||
searchBar.sizeToFit()
|
||||
searchBar.layoutMargins = UIEdgeInsets.zero
|
||||
searchBarContainer.set(.height, to: 44)
|
||||
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
|
||||
searchBarContainer.addSubview(searchBar)
|
||||
searchBar.autoPinEdgesToSuperviewMargins()
|
||||
navigationItem.titleView = searchBarContainer
|
||||
}
|
||||
|
||||
private func reloadTableData() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: Update Search Results
|
||||
|
||||
var refreshTimer: Timer?
|
||||
|
||||
private func refreshSearchResults() {
|
||||
|
||||
guard !searchResultSet.isEmpty else {
|
||||
// To avoid incorrectly showing the "no results" state,
|
||||
// always search immediately if the current result set is empty.
|
||||
refreshTimer?.invalidate()
|
||||
refreshTimer = nil
|
||||
|
||||
updateSearchResults(searchText: searchText)
|
||||
return
|
||||
}
|
||||
|
||||
if refreshTimer != nil {
|
||||
// Don't start a new refresh timer if there's already one active.
|
||||
return
|
||||
}
|
||||
|
||||
refreshTimer?.invalidate()
|
||||
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.updateSearchResults(searchText: self.searchText)
|
||||
self.refreshTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSearchResults(searchText rawSearchText: String) {
|
||||
|
||||
let searchText = rawSearchText.stripped
|
||||
guard searchText.count > 0 else {
|
||||
searchResultSet = HomeScreenSearchResultSet.noteToSelfOnly
|
||||
lastSearchText = nil
|
||||
reloadTableData()
|
||||
return
|
||||
}
|
||||
guard lastSearchText != searchText else { return }
|
||||
|
||||
lastSearchText = searchText
|
||||
|
||||
var searchResults: HomeScreenSearchResultSet?
|
||||
self.dbReadConnection.asyncRead({[weak self] transaction in
|
||||
guard let self = self else { return }
|
||||
self.isLoading = true
|
||||
// The max search result count is set according to the keyword length. This is just a workaround for performance issue.
|
||||
// The longer and more accurate the keyword is, the less search results should there be.
|
||||
searchResults = self.searcher.searchForHomeScreen(searchText: searchText, maxSearchResults: min(searchText.count * 50, 500), transaction: transaction)
|
||||
}, completionBlock: { [weak self] in
|
||||
AssertIsOnMainThread()
|
||||
guard let self = self, let results = searchResults, self.lastSearchText == searchText else { return }
|
||||
self.searchResultSet = results
|
||||
self.isLoading = false
|
||||
self.reloadTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc func clearRecentSearchResults() {
|
||||
recentSearchResults = []
|
||||
tableView.reloadSections([ SearchSection.recent.rawValue ], with: .top)
|
||||
Storage.shared.clearRecentSearchResults()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
extension GlobalSearchViewController: UISearchBarDelegate {
|
||||
public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
self.updateSearchText()
|
||||
}
|
||||
|
||||
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||
self.updateSearchText()
|
||||
}
|
||||
|
||||
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
self.updateSearchText()
|
||||
}
|
||||
|
||||
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.text = nil
|
||||
searchBar.resignFirstResponder()
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func updateSearchText() {
|
||||
guard let searchText = searchBar.text?.ows_stripped() else { return }
|
||||
self.searchText = searchText
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate & UITableViewDataSource
|
||||
extension GlobalSearchViewController {
|
||||
|
||||
// MARK: UITableViewDelegate
|
||||
|
||||
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
guard let searchSection = SearchSection(rawValue: indexPath.section) else { return }
|
||||
switch searchSection {
|
||||
case .noResults:
|
||||
SNLog("shouldn't be able to tap 'no results' section")
|
||||
case .contacts:
|
||||
let sectionResults = searchResultSet.conversations
|
||||
guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
|
||||
show(thread, highlightedMessageID: nil, animated: true)
|
||||
case .messages:
|
||||
let sectionResults = searchResultSet.messages
|
||||
guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
|
||||
show(thread, highlightedMessageID: searchResult.messageId, animated: true)
|
||||
case .recent:
|
||||
guard let threadId = recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId) else { return }
|
||||
show(thread, highlightedMessageID: nil, animated: true, isFromRecent: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func show(_ thread: TSThread, highlightedMessageID: String?, animated: Bool, isFromRecent: Bool = false) {
|
||||
if let threadId = thread.uniqueId {
|
||||
recentSearchResults = Array(Storage.shared.addSearchResults(threadID: threadId).reversed())
|
||||
}
|
||||
|
||||
DispatchMainThreadSafe {
|
||||
if let presentedVC = self.presentedViewController {
|
||||
presentedVC.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
public func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 4
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
UIView()
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||
.leastNonzeroMagnitude
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
guard nil != self.tableView(tableView, titleForHeaderInSection: section) else {
|
||||
return .leastNonzeroMagnitude
|
||||
}
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
guard let searchSection = SearchSection(rawValue: section) else { return nil }
|
||||
|
||||
guard let title = self.tableView(tableView, titleForHeaderInSection: section) else {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
|
||||
let container = UIView()
|
||||
container.backgroundColor = Colors.cellBackground
|
||||
container.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, left: Values.mediumSpacing, bottom: Values.smallSpacing, right: Values.mediumSpacing)
|
||||
container.addSubview(titleLabel)
|
||||
titleLabel.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
if searchSection == .recent {
|
||||
let clearButton = UIButton()
|
||||
clearButton.setTitle("Clear", for: .normal)
|
||||
clearButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
clearButton.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
clearButton.addTarget(self, action: #selector(clearRecentSearchResults), for: .touchUpInside)
|
||||
container.addSubview(clearButton)
|
||||
clearButton.autoPinTrailingToSuperviewMargin()
|
||||
clearButton.autoVCenterInSuperview()
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
guard let searchSection = SearchSection(rawValue: section) else { return nil }
|
||||
|
||||
switch searchSection {
|
||||
case .noResults:
|
||||
return nil
|
||||
case .contacts:
|
||||
if searchResultSet.conversations.count > 0 {
|
||||
return NSLocalizedString("SEARCH_SECTION_CONTACTS", comment: "")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case .messages:
|
||||
if searchResultSet.messages.count > 0 {
|
||||
return NSLocalizedString("SEARCH_SECTION_MESSAGES", comment: "")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case .recent:
|
||||
if recentSearchResults.count > 0 && searchText.isEmpty && isRecentSearchResultsEnabled {
|
||||
return NSLocalizedString("SEARCH_SECTION_RECENT", comment: "")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
guard let searchSection = SearchSection(rawValue: section) else { return 0 }
|
||||
switch searchSection {
|
||||
case .noResults:
|
||||
return (searchText.count > 0 && searchResultSet.isEmpty) ? 1 : 0
|
||||
case .contacts:
|
||||
return searchResultSet.conversations.count
|
||||
case .messages:
|
||||
return searchResultSet.messages.count
|
||||
case .recent:
|
||||
return searchText.isEmpty && isRecentSearchResultsEnabled ? recentSearchResults.count : 0
|
||||
}
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
switch searchSection {
|
||||
case .noResults:
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptySearchResultCell.reuseIdentifier) as? EmptySearchResultCell, indexPath.row == 0 else { return UITableViewCell() }
|
||||
cell.configure(isLoading: isLoading)
|
||||
return cell
|
||||
case .contacts:
|
||||
let sectionResults = searchResultSet.conversations
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.isShowingGlobalSearchResult = true
|
||||
let searchResult = sectionResults[safe: indexPath.row]
|
||||
cell.threadViewModel = searchResult?.thread
|
||||
cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText)
|
||||
return cell
|
||||
case .messages:
|
||||
let sectionResults = searchResultSet.messages
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.isShowingGlobalSearchResult = true
|
||||
let searchResult = sectionResults[safe: indexPath.row]
|
||||
cell.threadViewModel = searchResult?.thread
|
||||
var message: TSMessage? = nil
|
||||
if let messageId = searchResult?.messageId { message = TSMessage.fetch(uniqueId: messageId) }
|
||||
cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText, message: message)
|
||||
return cell
|
||||
case .recent:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.isShowingGlobalSearchResult = true
|
||||
dbReadConnection.read { transaction in
|
||||
guard let threadId = self.recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId, transaction: transaction) else { return }
|
||||
cell.threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
}
|
||||
cell.configureForRecent()
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
extension Storage{
|
||||
|
||||
private static let recentSearchResultDatabaseCollection = "RecentSearchResultDatabaseCollection"
|
||||
private static let recentSearchResultKey = "RecentSearchResult"
|
||||
|
||||
public func getRecentSearchResults() -> [String] {
|
||||
var result: [String]?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: Storage.recentSearchResultKey, inCollection: Storage.recentSearchResultDatabaseCollection) as? [String]
|
||||
}
|
||||
return result ?? []
|
||||
}
|
||||
|
||||
public func clearRecentSearchResults() {
|
||||
Storage.write { transaction in
|
||||
transaction.removeObject(forKey: Storage.recentSearchResultKey, inCollection: Storage.recentSearchResultDatabaseCollection)
|
||||
}
|
||||
}
|
||||
|
||||
public func addSearchResults(threadID: String) -> [String] {
|
||||
var recentSearchResults = getRecentSearchResults()
|
||||
if recentSearchResults.count > 20 { recentSearchResults.remove(at: 0) } // Limit the size of the collection to 20
|
||||
if let index = recentSearchResults.firstIndex(of: threadID) { recentSearchResults.remove(at: index) }
|
||||
recentSearchResults.append(threadID)
|
||||
Storage.write { transaction in
|
||||
transaction.setObject(recentSearchResults, forKey: Storage.recentSearchResultKey, inCollection: Storage.recentSearchResultDatabaseCollection)
|
||||
}
|
||||
return recentSearchResults
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
// See https://github.com/yapstudios/YapDatabase/wiki/LongLivedReadTransactions and
|
||||
// https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for
|
||||
// more information on database handling.
|
||||
|
||||
final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate {
|
||||
private var threads: YapDatabaseViewMappings!
|
||||
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
||||
|
@ -89,7 +88,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
setUpNavBarStyle()
|
||||
}
|
||||
updateNavBarButtons()
|
||||
setNavBarTitle(NSLocalizedString("vc_home_title", comment: ""))
|
||||
setUpNavBarSessionHeading()
|
||||
// Recovery phrase reminder
|
||||
let hasViewedSeed = UserDefaults.standard[.hasViewedSeed]
|
||||
if !hasViewedSeed {
|
||||
|
@ -266,6 +265,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
}
|
||||
|
||||
private func updateNavBarButtons() {
|
||||
// Profile picture view
|
||||
let profilePictureSize = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.accessibilityLabel = "Settings button"
|
||||
|
@ -276,32 +276,27 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
profilePictureView.set(.height, to: profilePictureSize)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
|
||||
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
||||
// Path status indicator
|
||||
let pathStatusView = PathStatusView()
|
||||
pathStatusView.accessibilityLabel = "Current onion routing path indicator"
|
||||
pathStatusView.set(.width, to: PathStatusView.size)
|
||||
pathStatusView.set(.height, to: PathStatusView.size)
|
||||
// Container view
|
||||
let profilePictureViewContainer = UIView()
|
||||
profilePictureViewContainer.accessibilityLabel = "Settings button"
|
||||
profilePictureViewContainer.addSubview(profilePictureView)
|
||||
profilePictureView.pin(.leading, to: .leading, of: profilePictureViewContainer, withInset: 4)
|
||||
profilePictureView.pin(.top, to: .top, of: profilePictureViewContainer)
|
||||
profilePictureView.pin(.trailing, to: .trailing, of: profilePictureViewContainer)
|
||||
profilePictureView.pin(.bottom, to: .bottom, of: profilePictureViewContainer)
|
||||
profilePictureView.autoPinEdgesToSuperviewEdges()
|
||||
profilePictureViewContainer.addSubview(pathStatusView)
|
||||
pathStatusView.pin(.trailing, to: .trailing, of: profilePictureViewContainer)
|
||||
pathStatusView.pin(.bottom, to: .bottom, of: profilePictureViewContainer)
|
||||
// Left bar button item
|
||||
let leftBarButtonItem = UIBarButtonItem(customView: profilePictureViewContainer)
|
||||
leftBarButtonItem.accessibilityLabel = "Settings button"
|
||||
leftBarButtonItem.isAccessibilityElement = true
|
||||
navigationItem.leftBarButtonItem = leftBarButtonItem
|
||||
let pathStatusViewContainer = UIView()
|
||||
pathStatusViewContainer.accessibilityLabel = "Current onion routing path button"
|
||||
let pathStatusViewContainerSize = Values.verySmallProfilePictureSize // Match the profile picture view
|
||||
pathStatusViewContainer.set(.width, to: pathStatusViewContainerSize)
|
||||
pathStatusViewContainer.set(.height, to: pathStatusViewContainerSize)
|
||||
let pathStatusView = PathStatusView()
|
||||
pathStatusView.accessibilityLabel = "Current onion routing path button"
|
||||
pathStatusView.set(.width, to: PathStatusView.size)
|
||||
pathStatusView.set(.height, to: PathStatusView.size)
|
||||
pathStatusViewContainer.addSubview(pathStatusView)
|
||||
pathStatusView.center(.horizontal, in: pathStatusViewContainer)
|
||||
pathStatusView.center(.vertical, in: pathStatusViewContainer)
|
||||
pathStatusViewContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showPath)))
|
||||
let rightBarButtonItem = UIBarButtonItem(customView: pathStatusViewContainer)
|
||||
rightBarButtonItem.accessibilityLabel = "Current onion routing path button"
|
||||
// Right bar button item - search button
|
||||
let rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(showSearchUI))
|
||||
rightBarButtonItem.accessibilityLabel = "Search button"
|
||||
rightBarButtonItem.isAccessibilityElement = true
|
||||
navigationItem.rightBarButtonItem = rightBarButtonItem
|
||||
}
|
||||
|
@ -418,10 +413,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showPath() {
|
||||
let pathVC = PathVC()
|
||||
let navigationController = OWSNavigationController(rootViewController: pathVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
@objc private func showSearchUI() {
|
||||
if let presentedVC = self.presentedViewController {
|
||||
presentedVC.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
let searchController = GlobalSearchViewController()
|
||||
self.navigationController?.setViewControllers([ self, searchController ], animated: true)
|
||||
}
|
||||
|
||||
@objc func joinOpenGroup() {
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "heading.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -601,3 +601,7 @@
|
|||
"light_mode_theme" = "Light";
|
||||
"PIN_BUTTON_TEXT" = "Pin";
|
||||
"UNPIN_BUTTON_TEXT" = "Unpin";
|
||||
"SEARCH_SECTION_CONTACTS" = "Contacts and Groups";
|
||||
"SEARCH_SECTION_MESSAGES" = "Messages";
|
||||
"SEARCH_SECTION_RECENT" = "Recent";
|
||||
"RECENT_SEARCH_LAST_MESSAGE_DATETIME" = "last message: %@";
|
||||
|
|
|
@ -40,10 +40,6 @@ final class PathVC : BaseVC {
|
|||
private func setUpNavBar() {
|
||||
setUpNavBarStyle()
|
||||
setNavBarTitle(NSLocalizedString("vc_path_title", comment: ""))
|
||||
// Set up close button
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.tintColor = Colors.text
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
|
@ -167,10 +163,6 @@ final class PathVC : BaseVC {
|
|||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func learnMore() {
|
||||
let urlAsString = "https://getsession.org/faq/#onion-routing"
|
||||
let url = URL(string: urlAsString)!
|
||||
|
|
|
@ -244,7 +244,21 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
button.set(.height, to: SettingsVC.buttonHeight)
|
||||
return button
|
||||
}
|
||||
|
||||
let pathButton = getSettingButton(withTitle: NSLocalizedString("vc_path_title", comment: ""), color: Colors.text, action: #selector(showPath))
|
||||
let pathStatusView = PathStatusView()
|
||||
pathStatusView.set(.width, to: PathStatusView.size)
|
||||
pathStatusView.set(.height, to: PathStatusView.size)
|
||||
|
||||
pathButton.addSubview(pathStatusView)
|
||||
pathStatusView.pin(.leading, to: .trailing, of: pathButton.titleLabel!, withInset: Values.smallSpacing)
|
||||
pathStatusView.autoVCenterInSuperview()
|
||||
|
||||
pathButton.titleEdgeInsets = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: Values.smallSpacing)
|
||||
|
||||
return [
|
||||
getSeparator(),
|
||||
pathButton,
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_privacy_button_title", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
|
||||
getSeparator(),
|
||||
|
@ -480,6 +494,11 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showPath() {
|
||||
let pathVC = PathVC()
|
||||
navigationController!.pushViewController(pathVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showPrivacySettings() {
|
||||
let privacySettingsVC = PrivacySettingsTableViewController()
|
||||
navigationController!.pushViewController(privacySettingsVC, animated: true)
|
||||
|
|
|
@ -75,6 +75,16 @@ class BaseVC : UIViewController {
|
|||
crossfadeLabel.pin(to: container)
|
||||
navigationItem.titleView = container
|
||||
}
|
||||
|
||||
internal func setUpNavBarSessionHeading() {
|
||||
let headingImageView = UIImageView()
|
||||
headingImageView.tintColor = Colors.sessionHeading
|
||||
headingImageView.image = UIImage(named: "SessionHeading")?.withRenderingMode(.alwaysTemplate)
|
||||
headingImageView.contentMode = .scaleAspectFit
|
||||
headingImageView.set(.width, to: 150)
|
||||
headingImageView.set(.height, to: Values.mediumFontSize)
|
||||
navigationItem.titleView = headingImageView
|
||||
}
|
||||
|
||||
internal func setUpNavBarSessionIcon() {
|
||||
let logoImageView = UIImageView()
|
||||
|
|
|
@ -2,7 +2,12 @@ import UIKit
|
|||
import SessionUIKit
|
||||
|
||||
final class ConversationCell : UITableViewCell {
|
||||
var threadViewModel: ThreadViewModel! { didSet { update() } }
|
||||
var isShowingGlobalSearchResult = false
|
||||
var threadViewModel: ThreadViewModel! {
|
||||
didSet {
|
||||
isShowingGlobalSearchResult ? updateForSearchResult() : update()
|
||||
}
|
||||
}
|
||||
|
||||
static let reuseIdentifier = "ConversationCell"
|
||||
|
||||
|
@ -96,6 +101,22 @@ final class ConversationCell : UITableViewCell {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var topLabelStackView: UIStackView = {
|
||||
let result = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.alignment = .center
|
||||
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var bottomLabelStackView: UIStackView = {
|
||||
let result = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.alignment = .center
|
||||
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
private static let unreadCountViewSize: CGFloat = 20
|
||||
private static let statusIndicatorSize: CGFloat = 14
|
||||
|
@ -135,21 +156,20 @@ final class ConversationCell : UITableViewCell {
|
|||
hasMentionLabel.pin(to: hasMentionView)
|
||||
// Label stack view
|
||||
let topLabelSpacer = UIView.hStretchingSpacer()
|
||||
let topLabelStackView = UIStackView(arrangedSubviews: [ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ])
|
||||
topLabelStackView.axis = .horizontal
|
||||
topLabelStackView.alignment = .center
|
||||
topLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
[ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ].forEach{ view in
|
||||
topLabelStackView.addArrangedSubview(view)
|
||||
}
|
||||
let snippetLabelContainer = UIView()
|
||||
snippetLabelContainer.addSubview(snippetLabel)
|
||||
snippetLabelContainer.addSubview(typingIndicatorView)
|
||||
let bottomLabelSpacer = UIView.hStretchingSpacer()
|
||||
let bottomLabelStackView = UIStackView(arrangedSubviews: [ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ])
|
||||
bottomLabelStackView.axis = .horizontal
|
||||
bottomLabelStackView.alignment = .center
|
||||
bottomLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
let labelContainerView = UIView()
|
||||
labelContainerView.addSubview(topLabelStackView)
|
||||
labelContainerView.addSubview(bottomLabelStackView)
|
||||
[ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ].forEach{ view in
|
||||
bottomLabelStackView.addArrangedSubview(view)
|
||||
}
|
||||
let labelContainerView = UIStackView(arrangedSubviews: [ topLabelStackView, bottomLabelStackView ])
|
||||
labelContainerView.axis = .vertical
|
||||
labelContainerView.alignment = .leading
|
||||
labelContainerView.spacing = 6
|
||||
// Main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
|
||||
stackView.axis = .horizontal
|
||||
|
@ -172,16 +192,6 @@ final class ConversationCell : UITableViewCell {
|
|||
snippetLabel.pin(to: snippetLabelContainer)
|
||||
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
|
||||
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
|
||||
// HACK: Not using a stack view for this is part of a workaround for a weird layout bug
|
||||
topLabelStackView.pin(.leading, to: .leading, of: labelContainerView)
|
||||
topLabelStackView.pin(.top, to: .top, of: labelContainerView, withInset: 12)
|
||||
topLabelStackView.pin(.trailing, to: .trailing, of: labelContainerView)
|
||||
bottomLabelStackView.pin(.leading, to: .leading, of: labelContainerView)
|
||||
bottomLabelStackView.pin(.top, to: .bottom, of: topLabelStackView, withInset: 6)
|
||||
labelContainerView.pin(.bottom, to: .bottom, of: bottomLabelStackView, withInset: 12)
|
||||
// HACK: The two lines below are part of a workaround for a weird layout bug
|
||||
labelContainerView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - Values.mediumSpacing - profilePictureViewSize - Values.mediumSpacing - Values.mediumSpacing)
|
||||
labelContainerView.set(.height, to: cellHeight)
|
||||
stackView.pin(.leading, to: .leading, of: contentView)
|
||||
stackView.pin(.top, to: .top, of: contentView)
|
||||
// HACK: The two lines below are part of a workaround for a weird layout bug
|
||||
|
@ -189,6 +199,79 @@ final class ConversationCell : UITableViewCell {
|
|||
stackView.set(.height, to: cellHeight)
|
||||
}
|
||||
|
||||
// MARK: Updating for search results
|
||||
private func updateForSearchResult() {
|
||||
AssertIsOnMainThread()
|
||||
guard let thread = threadViewModel?.threadRecord else { return }
|
||||
profilePictureView.update(for: thread)
|
||||
isPinnedIcon.isHidden = true
|
||||
unreadCountView.isHidden = true
|
||||
hasMentionView.isHidden = true
|
||||
}
|
||||
|
||||
public func configureForRecent() {
|
||||
displayNameLabel.attributedText = NSMutableAttributedString(string: getDisplayName(), attributes: [.foregroundColor:Colors.text])
|
||||
bottomLabelStackView.isHidden = false
|
||||
let snippet = String(format: NSLocalizedString("RECENT_SEARCH_LAST_MESSAGE_DATETIME", comment: ""), DateUtil.formatDate(forDisplay: threadViewModel.lastMessageDate))
|
||||
snippetLabel.attributedText = NSMutableAttributedString(string: snippet, attributes: [.foregroundColor:Colors.text.withAlphaComponent(Values.lowOpacity)])
|
||||
timestampLabel.isHidden = true
|
||||
}
|
||||
|
||||
public func configure(messageDate: Date?, snippet: String?, searchText: String, message: TSMessage? = nil) {
|
||||
let normalizedSearchText = searchText.lowercased()
|
||||
if let messageDate = messageDate, let snippet = snippet {
|
||||
// Message
|
||||
displayNameLabel.attributedText = NSMutableAttributedString(string: getDisplayName(), attributes: [.foregroundColor:Colors.text])
|
||||
timestampLabel.isHidden = false
|
||||
timestampLabel.text = DateUtil.formatDate(forDisplay: messageDate)
|
||||
bottomLabelStackView.isHidden = false
|
||||
var rawSnippet = snippet
|
||||
if let message = message, let name = getMessageAuthorName(message: message) {
|
||||
rawSnippet = "\(name): \(snippet)"
|
||||
}
|
||||
snippetLabel.attributedText = getHighlightedSnippet(snippet: rawSnippet, searchText: normalizedSearchText, fontSize: Values.smallFontSize)
|
||||
} else {
|
||||
// Contact
|
||||
if threadViewModel.isGroupThread, let thread = threadViewModel.threadRecord as? TSGroupThread {
|
||||
displayNameLabel.attributedText = getHighlightedSnippet(snippet: getDisplayName(), searchText: normalizedSearchText, fontSize: Values.mediumFontSize)
|
||||
bottomLabelStackView.isHidden = false
|
||||
let context: Contact.Context = thread.isOpenGroup ? .openGroup : .regular
|
||||
var rawSnippet: String = ""
|
||||
thread.groupModel.groupMemberIds.forEach{ id in
|
||||
if let displayName = Storage.shared.getContact(with: id)?.displayName(for: context) {
|
||||
if !rawSnippet.isEmpty {
|
||||
rawSnippet += ", \(displayName)"
|
||||
}
|
||||
if displayName.lowercased().contains(normalizedSearchText) {
|
||||
rawSnippet = displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
snippetLabel.attributedText = getHighlightedSnippet(snippet: rawSnippet, searchText: normalizedSearchText, fontSize: Values.smallFontSize)
|
||||
} else {
|
||||
displayNameLabel.attributedText = getHighlightedSnippet(snippet: getDisplayNameForSearch(threadViewModel.contactSessionID!), searchText: normalizedSearchText, fontSize: Values.mediumFontSize)
|
||||
bottomLabelStackView.isHidden = true
|
||||
}
|
||||
timestampLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func getHighlightedSnippet(snippet: String, searchText: String, fontSize: CGFloat) -> NSMutableAttributedString {
|
||||
guard snippet != NSLocalizedString("NOTE_TO_SELF", comment: "") else {
|
||||
return NSMutableAttributedString(string: snippet, attributes: [.foregroundColor:Colors.text])
|
||||
}
|
||||
|
||||
let result = NSMutableAttributedString(string: snippet, attributes: [.foregroundColor:Colors.text.withAlphaComponent(Values.lowOpacity)])
|
||||
let normalizedSnippet = snippet.lowercased() as NSString
|
||||
|
||||
guard normalizedSnippet.contains(searchText) else { return result }
|
||||
|
||||
let range = normalizedSnippet.range(of: searchText)
|
||||
result.addAttribute(.foregroundColor, value: Colors.text, range: range)
|
||||
result.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: range)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func update() {
|
||||
AssertIsOnMainThread()
|
||||
|
@ -246,6 +329,27 @@ final class ConversationCell : UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
private func getMessageAuthorName(message: TSMessage) -> String? {
|
||||
guard threadViewModel.isGroupThread else { return nil }
|
||||
if let incomingMessage = message as? TSIncomingMessage {
|
||||
return Storage.shared.getContact(with: incomingMessage.authorId)?.displayName(for: .regular) ?? "Anonymous"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getDisplayNameForSearch(_ sessionID: String) -> String {
|
||||
if threadViewModel.threadRecord.isNoteToSelf() {
|
||||
return NSLocalizedString("NOTE_TO_SELF", comment: "")
|
||||
} else {
|
||||
var result = sessionID
|
||||
if let contact = Storage.shared.getContact(with: sessionID), let name = contact.name {
|
||||
result = name
|
||||
if let nickname = contact.nickname { result += "(\(nickname))"}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private func getDisplayName() -> String {
|
||||
if threadViewModel.isGroupThread {
|
||||
if threadViewModel.name.isEmpty {
|
||||
|
@ -275,9 +379,12 @@ final class ConversationCell : UITableViewCell {
|
|||
result.append(imageString)
|
||||
result.append(NSAttributedString(string: " ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ]))
|
||||
}
|
||||
let font = threadViewModel.hasUnreadMessages ? UIFont.boldSystemFont(ofSize: Values.smallFontSize) : UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
if threadViewModel.isGroupThread, let message = threadViewModel.lastMessageForInbox as? TSMessage, let name = getMessageAuthorName(message: message) {
|
||||
result.append(NSAttributedString(string: "\(name): ", attributes: [ .font : font, .foregroundColor : Colors.text ]))
|
||||
}
|
||||
if let rawSnippet = threadViewModel.lastMessageText {
|
||||
let snippet = MentionUtilities.highlightMentions(in: rawSnippet, threadID: threadViewModel.threadRecord.uniqueId!)
|
||||
let font = threadViewModel.hasUnreadMessages ? UIFont.boldSystemFont(ofSize: Values.smallFontSize) : UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
result.append(NSAttributedString(string: snippet, attributes: [ .font : font, .foregroundColor : Colors.text ]))
|
||||
}
|
||||
return result
|
||||
|
|
|
@ -69,6 +69,13 @@ NSString *const TSContactThreadPrefix = @"c";
|
|||
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
|
||||
}
|
||||
|
||||
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
NSString *sessionID = self.contactSessionID;
|
||||
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
|
||||
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
|
||||
}
|
||||
|
||||
+ (NSString *)threadIDFromContactSessionID:(NSString *)contactSessionID {
|
||||
return [TSContactThreadPrefix stringByAppendingString:contactSessionID];
|
||||
}
|
||||
|
|
|
@ -192,6 +192,11 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
return self.groupModel.groupName ?: self.class.defaultGroupName;
|
||||
}
|
||||
|
||||
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
return [self name];
|
||||
}
|
||||
|
||||
+ (NSString *)defaultGroupName
|
||||
{
|
||||
return @"Group";
|
||||
|
|
|
@ -38,6 +38,8 @@ BOOL IsNoteToSelfEnabled(void);
|
|||
*/
|
||||
- (NSString *)name;
|
||||
|
||||
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction;
|
||||
|
||||
/**
|
||||
* @returns recipientId for each recipient in the thread
|
||||
*/
|
||||
|
|
|
@ -148,6 +148,11 @@ BOOL IsNoteToSelfEnabled(void)
|
|||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)recipientIdentifiers
|
||||
{
|
||||
return @[];
|
||||
|
|
|
@ -85,18 +85,19 @@ public class FullTextSearchFinder: NSObject {
|
|||
return query
|
||||
}
|
||||
|
||||
public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) {
|
||||
public func enumerateObjects(searchText: String, maxSearchResults: Int? = nil, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) {
|
||||
guard let ext: YapDatabaseFullTextSearchTransaction = ext(transaction: transaction) else {
|
||||
return
|
||||
}
|
||||
|
||||
let query = FullTextSearchFinder.query(searchText: searchText)
|
||||
|
||||
let maxSearchResults = 500
|
||||
let maxSearchResults = maxSearchResults ?? 500
|
||||
var searchResultCount = 0
|
||||
let snippetOptions = YapDatabaseFullTextSearchSnippetOptions()
|
||||
snippetOptions.startMatchText = ""
|
||||
snippetOptions.endMatchText = ""
|
||||
snippetOptions.numberOfTokens = 5
|
||||
ext.enumerateKeysAndObjects(matching: query, with: snippetOptions) { (snippet: String, _: String, _: String, object: Any, stop: UnsafeMutablePointer<ObjCBool>) in
|
||||
guard searchResultCount < maxSearchResults else {
|
||||
stop.pointee = true
|
||||
|
@ -177,8 +178,12 @@ public class FullTextSearchFinder: NSObject {
|
|||
}
|
||||
|
||||
private static let recipientIndexer: SearchIndexer<String> = SearchIndexer { (recipientId: String, transaction: YapDatabaseReadTransaction) in
|
||||
let displayName = Storage.shared.getContact(with: recipientId)?.displayName(for: Contact.Context.regular) ?? recipientId
|
||||
return "\(recipientId) \(displayName)"
|
||||
var result = "\(recipientId)"
|
||||
if let contact = Storage.shared.getContact(with: recipientId) {
|
||||
if let name = contact.name { result += " \(name)" }
|
||||
if let nickname = contact.nickname { result += " \(nickname)" }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static let messageIndexer: SearchIndexer<TSMessage> = SearchIndexer { (message: TSMessage, transaction: YapDatabaseReadTransaction) in
|
||||
|
@ -241,6 +246,6 @@ public class FullTextSearchFinder: NSObject {
|
|||
options: nil,
|
||||
handler: handler,
|
||||
ftsVersion: YapDatabaseFullTextSearchFTS5Version,
|
||||
versionTag: "1")
|
||||
versionTag: "2")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,16 +29,8 @@ public final class SearchBar : UISearchBar {
|
|||
searchTextField.backgroundColor = Colors.searchBarBackground // The search bar background color
|
||||
searchTextField.textColor = Colors.text
|
||||
searchTextField.attributedPlaceholder = NSAttributedString(string: NSLocalizedString("Search", comment: ""), attributes: [ .foregroundColor : Colors.searchBarPlaceholder ])
|
||||
searchTextField.keyboardAppearance = .dark
|
||||
setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: UISearchBar.Icon.search)
|
||||
searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
|
||||
setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: UISearchBar.Icon.clear)
|
||||
searchTextField.removeConstraints(searchTextField.constraints)
|
||||
searchTextField.pin(.leading, to: .leading, of: searchTextField.superview!, withInset: Values.mediumSpacing + 3)
|
||||
searchTextField.pin(.top, to: .top, of: searchTextField.superview!, withInset: 10)
|
||||
searchTextField.superview!.pin(.trailing, to: .trailing, of: searchTextField, withInset: Values.mediumSpacing + 3)
|
||||
searchTextField.superview!.pin(.bottom, to: .bottom, of: searchTextField, withInset: 10)
|
||||
searchTextField.set(.height, to: Values.searchBarHeight)
|
||||
searchTextField.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,4 +42,5 @@ public final class Colors : NSObject {
|
|||
@objc public static var pnOptionBorder: UIColor { UIColor(named: "session_pn_option_border")! }
|
||||
@objc public static var pathsBuilding: UIColor { UIColor(named: "session_paths_building")! }
|
||||
@objc public static var pinIcon: UIColor { UIColor(named: "session_pin_icon")! }
|
||||
@objc public static var sessionHeading: UIColor { UIColor(named: "session_heading")! }
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFC",
|
||||
"green" : "0xFC",
|
||||
"red" : "0xFC"
|
||||
"blue" : "252",
|
||||
"green" : "252",
|
||||
"red" : "252"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x1B",
|
||||
"green" : "0x1B",
|
||||
"red" : "0x1B"
|
||||
"blue" : "22",
|
||||
"green" : "22",
|
||||
"red" : "22"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF0",
|
||||
"green" : "0xF0",
|
||||
"red" : "0xF0"
|
||||
"blue" : "247",
|
||||
"green" : "247",
|
||||
"red" : "247"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x40",
|
||||
"green" : "0x40",
|
||||
"red" : "0x40"
|
||||
"blue" : "28",
|
||||
"green" : "28",
|
||||
"red" : "28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x82",
|
||||
"green" : "0xF7",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFC",
|
||||
"green" : "0xFC",
|
||||
"red" : "0xFC"
|
||||
"blue" : "252",
|
||||
"green" : "252",
|
||||
"red" : "252"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x16",
|
||||
"green" : "0x16",
|
||||
"red" : "0x16"
|
||||
"blue" : "22",
|
||||
"green" : "22",
|
||||
"red" : "22"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -65,6 +65,20 @@ public class HomeScreenSearchResultSet: NSObject {
|
|||
public class var empty: HomeScreenSearchResultSet {
|
||||
return HomeScreenSearchResultSet(searchText: "", conversations: [], messages: [])
|
||||
}
|
||||
|
||||
public class var noteToSelfOnly: HomeScreenSearchResultSet {
|
||||
var conversations: [ConversationSearchResult<ConversationSortKey>] = []
|
||||
Storage.read { transaction in
|
||||
if let thread = TSContactThread.getWithContactSessionID(getUserHexEncodedPublicKey(), transaction: transaction) {
|
||||
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
let sortKey = ConversationSortKey(creationDate: thread.creationDate,
|
||||
lastMessageReceivedAtDate: thread.lastInteractionForInbox(transaction: transaction)?.receivedAtDate())
|
||||
let searchResult = ConversationSearchResult(thread: threadViewModel, sortKey: sortKey)
|
||||
conversations.append(searchResult)
|
||||
}
|
||||
}
|
||||
return HomeScreenSearchResultSet(searchText: "", conversations: conversations, messages: [])
|
||||
}
|
||||
|
||||
public var isEmpty: Bool {
|
||||
return conversations.isEmpty && messages.isEmpty
|
||||
|
@ -227,6 +241,7 @@ public class FullTextSearcher: NSObject {
|
|||
}
|
||||
|
||||
public func searchForHomeScreen(searchText: String,
|
||||
maxSearchResults: Int? = nil,
|
||||
transaction: YapDatabaseReadTransaction) -> HomeScreenSearchResultSet {
|
||||
|
||||
var conversations: [ConversationSearchResult<ConversationSortKey>] = []
|
||||
|
@ -234,7 +249,7 @@ public class FullTextSearcher: NSObject {
|
|||
|
||||
var existingConversationRecipientIds: Set<String> = Set()
|
||||
|
||||
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in
|
||||
self.finder.enumerateObjects(searchText: searchText, maxSearchResults: maxSearchResults, transaction: transaction) { (match: Any, snippet: String?) in
|
||||
|
||||
if let thread = match as? TSThread {
|
||||
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
|
|
|
@ -30,7 +30,7 @@ public class ThreadViewModel: NSObject {
|
|||
self.threadRecord = thread
|
||||
|
||||
self.isGroupThread = thread.isGroupThread()
|
||||
self.name = thread.name()
|
||||
self.name = thread.name(with: transaction)
|
||||
self.isMuted = thread.isMuted
|
||||
self.isPinned = thread.isPinned
|
||||
self.lastMessageText = thread.lastMessageText(transaction: transaction)
|
||||
|
|
|
@ -120,11 +120,11 @@ public extension UIView {
|
|||
return constraints
|
||||
}
|
||||
|
||||
func setShadow(radius: CGFloat = 2.0, opacity: CGFloat = 0.66, offset: CGPoint = .zero, color: CGColor = UIColor.black.cgColor) {
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowRadius = 2.0
|
||||
layer.shadowOpacity = 0.66
|
||||
layer.shadowOffset = .zero
|
||||
func setShadow(radius: CGFloat = 2.0, opacity: Float = 0.66, offset: CGSize = .zero, color: CGColor = UIColor.black.cgColor) {
|
||||
layer.shadowColor = color
|
||||
layer.shadowRadius = radius
|
||||
layer.shadowOpacity = opacity
|
||||
layer.shadowOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue