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