mirror of https://github.com/oxen-io/session-ios
Compose View: collation index and group search
- Include table index for contacts - Fix extra spacing in OWS table view - Separate search results into contact/invite sections - Include groups in search results when composing new message - Compose Screen search matches on group member names // FREEBIEpull/1/head
parent
796be18c56
commit
3080cb512b
@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// ObjC compatible searcher
|
||||
@objc class AnySearcher: NSObject {
|
||||
private let searcher: Searcher<AnyObject>
|
||||
|
||||
public init(indexer: @escaping (AnyObject) -> String ) {
|
||||
searcher = Searcher(indexer: indexer)
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc(item:doesMatchQuery:)
|
||||
public func matches(item: AnyObject, query: String) -> Bool {
|
||||
return searcher.matches(item: item, query: query)
|
||||
}
|
||||
}
|
||||
|
||||
class Searcher<T> {
|
||||
|
||||
private let indexer: (T) -> String
|
||||
|
||||
public init(indexer: @escaping (T) -> String) {
|
||||
self.indexer = indexer
|
||||
}
|
||||
|
||||
public func matches(item: T, query: String) -> Bool {
|
||||
let itemString = normalize(string: indexer(item))
|
||||
|
||||
return stem(string: query).map { queryStem in
|
||||
return itemString.contains(queryStem)
|
||||
}.reduce(true) { $0 && $1 }
|
||||
}
|
||||
|
||||
private func stem(string: String) -> [String] {
|
||||
return normalize(string: string).components(separatedBy: .whitespaces)
|
||||
}
|
||||
|
||||
private func normalize(string: String) -> String {
|
||||
return string.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc class GroupTableViewCell: UITableViewCell {
|
||||
|
||||
let TAG = "[GroupTableViewCell]"
|
||||
|
||||
private let avatarView = AvatarImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
|
||||
init() {
|
||||
super.init(style: .default, reuseIdentifier: TAG)
|
||||
|
||||
self.contentView.addSubview(avatarView)
|
||||
|
||||
let textContainer = UIView.container()
|
||||
textContainer.addSubview(nameLabel)
|
||||
textContainer.addSubview(subtitleLabel)
|
||||
self.contentView.addSubview(textContainer)
|
||||
|
||||
// Font config
|
||||
nameLabel.font = UIFont.ows_dynamicTypeBody()
|
||||
subtitleLabel.font = UIFont.ows_footnote()
|
||||
subtitleLabel.textColor = UIColor.ows_darkGray()
|
||||
|
||||
// Listen to notifications...
|
||||
// TODO avatar, group name change, group membership change, group member name change
|
||||
|
||||
// Layout
|
||||
|
||||
nameLabel.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .bottom)
|
||||
subtitleLabel.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .top)
|
||||
subtitleLabel.autoPinEdge(.top, to: .bottom, of: nameLabel)
|
||||
|
||||
avatarView.autoPinLeadingToSuperview()
|
||||
avatarView.autoVCenterInSuperview()
|
||||
avatarView.autoSetDimension(.width, toSize: CGFloat(kContactTableViewCellAvatarSize))
|
||||
avatarView.autoPinToSquareAspectRatio()
|
||||
|
||||
textContainer.autoPinEdge(.leading, to: .trailing, of: avatarView, withOffset: kContactTableViewCellAvatarTextMargin)
|
||||
textContainer.autoPinEdge(toSuperviewEdge: .trailing)
|
||||
textContainer.autoVCenterInSuperview()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func configure(thread: TSGroupThread, contactsManager: OWSContactsManager) {
|
||||
if let groupName = thread.groupModel.groupName, !groupName.isEmpty {
|
||||
self.nameLabel.text = groupName
|
||||
} else {
|
||||
self.nameLabel.text = MessageStrings.newGroupDefaultTitle
|
||||
}
|
||||
|
||||
let groupMemberIds: [String] = thread.groupModel.groupMemberIds
|
||||
let groupMemberNames = groupMemberIds.map { (recipientId: String) in
|
||||
contactsManager.displayName(forPhoneIdentifier: recipientId)
|
||||
}.joined(separator: ", ")
|
||||
self.subtitleLabel.text = groupMemberNames
|
||||
|
||||
self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kContactTableViewCellAvatarSize, contactsManager: contactsManager)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class SearcherTest: XCTestCase {
|
||||
|
||||
struct TestCharacter {
|
||||
let name: String
|
||||
let description: String
|
||||
}
|
||||
|
||||
let smerdyakov = TestCharacter(name: "Pavel Fyodorovich Smerdyakov", description: "A rusty hue in the sky")
|
||||
let stinkingLizaveta = TestCharacter(name: "Stinking Lizaveta", description: "object of pity")
|
||||
let regularLizaveta = TestCharacter(name: "Lizaveta", description: "")
|
||||
|
||||
let indexer = { (character: TestCharacter) in
|
||||
return "\(character.name) \(character.description)"
|
||||
}
|
||||
|
||||
var searcher: Searcher<TestCharacter> {
|
||||
return Searcher(indexer: indexer)
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSimple() {
|
||||
XCTAssert(searcher.matches(item: smerdyakov, query: "Pavel"))
|
||||
XCTAssert(searcher.matches(item: smerdyakov, query: "pavel"))
|
||||
XCTAssertFalse(searcher.matches(item: smerdyakov, query: "asdf"))
|
||||
XCTAssertFalse(searcher.matches(item: smerdyakov, query: ""))
|
||||
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Pity"))
|
||||
}
|
||||
|
||||
func testRepeats() {
|
||||
XCTAssert(searcher.matches(item: smerdyakov, query: "pavel pavel"))
|
||||
XCTAssertFalse(searcher.matches(item: smerdyakov, query: "pavelpavel"))
|
||||
}
|
||||
|
||||
func testSplitWords() {
|
||||
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta"))
|
||||
XCTAssert(searcher.matches(item: regularLizaveta, query: "Lizaveta"))
|
||||
|
||||
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Stinking Lizaveta"))
|
||||
XCTAssertFalse(searcher.matches(item: regularLizaveta, query: "Stinking Lizaveta"))
|
||||
|
||||
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta Stinking"))
|
||||
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta St"))
|
||||
XCTAssert(searcher.matches(item: stinkingLizaveta, query: " Lizaveta St "))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue