mirror of https://github.com/oxen-io/session-ios
Merge branch 'dev' of https://github.com/oxen-io/session-ios into 1.11.21
commit
ebb7507728
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "heading.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue