Merge branch 'dev' into fix/safari-sharing

# Conflicts:
#	Podfile.lock
This commit is contained in:
Morgan Pretty 2022-02-03 08:32:17 +11:00
commit 4d62ddbd77
29 changed files with 852 additions and 106 deletions

View File

@ -204,6 +204,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 19ce2820c263e8f3c114817f7ca2da73a9382b6a
PODFILE CHECKSUM: 7f961dc4934dd213f5a3277af57d54caef7a4442
COCOAPODS: 1.11.2

View File

@ -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 */,

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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() {

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "heading.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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: %@";

View File

@ -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)!

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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];
}

View File

@ -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";

View File

@ -38,6 +38,8 @@ BOOL IsNoteToSelfEnabled(void);
*/
- (NSString *)name;
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction;
/**
* @returns recipientId for each recipient in the thread
*/

View File

@ -148,6 +148,11 @@ BOOL IsNoteToSelfEnabled(void)
return nil;
}
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return nil;
}
- (NSArray<NSString *> *)recipientIdentifiers
{
return @[];

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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")! }
}

View File

@ -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"

View File

@ -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"

View File

@ -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
}
}

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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
}
}