Merge branch 'dev' into custom-server

pull/57/head
Niels Andriesse
commit 86550c8877

@ -1 +1 @@
Subproject commit 68a1e49959447a8ef4b4de77edec53375c598268 Subproject commit 56980ceea7cc0964b92f98831e79fea03e76e801

@ -567,6 +567,7 @@
B8258493230FA5E9001B41CB /* ScanQRCodeVC.m in Sources */ = {isa = PBXBuildFile; fileRef = B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */; }; B8258493230FA5E9001B41CB /* ScanQRCodeVC.m in Sources */ = {isa = PBXBuildFile; fileRef = B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */; };
B82584A02315024B001B41CB /* RSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* RSSFeedPoller.swift */; }; B82584A02315024B001B41CB /* RSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* RSSFeedPoller.swift */; };
B846365B22B7418B00AF1514 /* Identicon+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */; }; B846365B22B7418B00AF1514 /* Identicon+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */; };
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08023399883000F5AE3 /* QRCodeModal.swift */; }; B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08023399883000F5AE3 /* QRCodeModal.swift */; };
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; }; B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; }; B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; };
@ -578,8 +579,8 @@
B894D0712339D6F300B4D94D /* DeviceLinkingModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */; }; B894D0712339D6F300B4D94D /* DeviceLinkingModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */; };
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */; }; B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */; };
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* UserSelectionView.swift */; }; B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; };
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */; }; B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */; };
B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; }; B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; };
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; }; B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; };
BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.framework */; }; BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.framework */; };
@ -1378,6 +1379,7 @@
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScanQRCodeVC.m; sourceTree = "<group>"; }; B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScanQRCodeVC.m; sourceTree = "<group>"; };
B825849F2315024B001B41CB /* RSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSFeedPoller.swift; sourceTree = "<group>"; }; B825849F2315024B001B41CB /* RSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSFeedPoller.swift; sourceTree = "<group>"; };
B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identicon+ObjC.swift"; sourceTree = "<group>"; }; B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identicon+ObjC.swift"; sourceTree = "<group>"; };
B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = "<group>"; };
B86BD08023399883000F5AE3 /* QRCodeModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeModal.swift; sourceTree = "<group>"; }; B86BD08023399883000F5AE3 /* QRCodeModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeModal.swift; sourceTree = "<group>"; };
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; }; B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; }; B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
@ -1387,8 +1389,8 @@
B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModalDelegate.swift; sourceTree = "<group>"; }; B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModalDelegate.swift; sourceTree = "<group>"; };
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; }; B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; };
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionView.swift; sourceTree = "<group>"; }; B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.swift; sourceTree = "<group>"; };
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionViewDelegate.swift; sourceTree = "<group>"; }; B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionViewDelegate.swift; sourceTree = "<group>"; };
B90418E4183E9DD40038554A /* DateUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateUtil.h; sourceTree = "<group>"; }; B90418E4183E9DD40038554A /* DateUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateUtil.h; sourceTree = "<group>"; };
B90418E5183E9DD40038554A /* DateUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DateUtil.m; sourceTree = "<group>"; }; B90418E5183E9DD40038554A /* DateUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DateUtil.m; sourceTree = "<group>"; };
B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; }; B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
@ -2656,8 +2658,8 @@
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */, B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */, B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */, 24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */,
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */, B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */, B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
); );
path = Loki; path = Loki;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2699,6 +2701,7 @@
children = ( children = (
B86BD08323399ACF000F5AE3 /* Modal.swift */, B86BD08323399ACF000F5AE3 /* Modal.swift */,
B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */, B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */,
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
); );
path = Utilities; path = Utilities;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3845,7 +3848,7 @@
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */, 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */,
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */, B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */,
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */, 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */, B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */,
@ -3869,7 +3872,7 @@
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */, 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */, 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */, 45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */, B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */,
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */, 340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */, 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
@ -3879,6 +3882,7 @@
345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */, 345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */,
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */, 340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */, 45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */, 34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */,

@ -1,21 +1,21 @@
// MARK: - User Selection View // MARK: - User Selection View
@objc(LKUserSelectionView) @objc(LKMentionCandidateSelectionView)
final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelegate { final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
@objc var users: [String] = [] { didSet { tableView.reloadData() } } @objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
@objc var hasGroupContext = false @objc var hasGroupContext = false
@objc var delegate: UserSelectionViewDelegate? @objc var delegate: MentionCandidateSelectionViewDelegate?
// MARK: Components // MARK: Components
private lazy var tableView: UITableView = { @objc lazy var tableView: UITableView = { // TODO: Make this private
let result = UITableView() let result = UITableView()
result.dataSource = self result.dataSource = self
result.delegate = self result.delegate = self
result.register(Cell.self, forCellReuseIdentifier: "Cell") result.register(Cell.self, forCellReuseIdentifier: "Cell")
result.separatorStyle = .none result.separatorStyle = .none
result.backgroundColor = .clear result.backgroundColor = .clear
result.contentInset = UIEdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0) result.contentInset = UIEdgeInsets(top: 6, leading: 0, bottom: 0, trailing: 0)
return result return result
}() }()
@ -37,30 +37,30 @@ final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelega
// MARK: Data // MARK: Data
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count return mentionCandidates.count
} }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
let user = users[indexPath.row] let mentionCandidate = mentionCandidates[indexPath.row]
cell.user = user cell.mentionCandidate = mentionCandidate
cell.hasGroupContext = hasGroupContext cell.hasGroupContext = hasGroupContext
return cell return cell
} }
// MARK: Interaction // MARK: Interaction
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let user = users[indexPath.row] let mentionCandidate = mentionCandidates[indexPath.row]
delegate?.handleUserSelected(user, from: self) delegate?.handleMentionCandidateSelected(mentionCandidate, from: self)
} }
} }
// MARK: - Cell // MARK: - Cell
private extension UserSelectionView { private extension MentionCandidateSelectionView {
final class Cell : UITableViewCell { final class Cell : UITableViewCell {
var user = "" { didSet { update() } } var mentionCandidate = Mention(hexEncodedPublicKey: "", displayName: "") { didSet { update() } }
var hasGroupContext = false var hasGroupContext = false
// MARK: Components // MARK: Components
@ -101,12 +101,12 @@ private extension UserSelectionView {
stackView.axis = .horizontal stackView.axis = .horizontal
stackView.alignment = .center stackView.alignment = .center
stackView.spacing = 16 stackView.spacing = 16
stackView.set(.height, to: 44) stackView.set(.height, to: 36)
contentView.addSubview(stackView) contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16) stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
stackView.pin(.top, to: .top, of: contentView, withInset: 4) stackView.pin(.top, to: .top, of: contentView, withInset: 8)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16) contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 4) contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 8)
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * 16) stackView.set(.width, to: UIScreen.main.bounds.width - 2 * 16)
// Set up the moderator icon image view // Set up the moderator icon image view
moderatorIconImageView.set(.width, to: 20) moderatorIconImageView.set(.width, to: 20)
@ -118,15 +118,10 @@ private extension UserSelectionView {
// MARK: Updating // MARK: Updating
private func update() { private func update() {
var displayName: String = "" displayNameLabel.text = mentionCandidate.displayName
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in let profilePicture = OWSContactAvatarBuilder(signalId: mentionCandidate.hexEncodedPublicKey, colorName: .blue, diameter: 36).build()
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
displayName = transaction.object(forKey: self.user, inCollection: collection) as! String
}
displayNameLabel.text = displayName
let profilePicture = OWSContactAvatarBuilder(signalId: user, colorName: .blue, diameter: 36).build()
profilePictureImageView.image = profilePicture profilePictureImageView.image = profilePicture
let isUserModerator = LokiGroupChatAPI.isUserModerator(user, for: LokiGroupChatAPI.publicChatServerID, on: LokiGroupChatAPI.publicChatServer) let isUserModerator = LokiGroupChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, for: LokiGroupChatAPI.publicChatServerID, on: LokiGroupChatAPI.publicChatServer)
moderatorIconImageView.isHidden = !isUserModerator || !hasGroupContext moderatorIconImageView.isHidden = !isUserModerator || !hasGroupContext
} }
} }

@ -0,0 +1,6 @@
@objc(LKMentionCandidateSelectionViewDelegate)
protocol MentionCandidateSelectionViewDelegate {
func handleMentionCandidateSelected(_ mentionCandidate: Mention, from mentionCandidateSelectionView: MentionCandidateSelectionView)
}

@ -6,7 +6,7 @@ final class DisplayNameVC : OnboardingBaseViewController {
result.textColor = Theme.primaryColor result.textColor = Theme.primaryColor
result.font = UIFont.ows_dynamicTypeBodyClamped result.font = UIFont.ows_dynamicTypeBodyClamped
result.textAlignment = .center result.textAlignment = .center
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Display Name (Optional)", comment: "")) let placeholder = NSMutableAttributedString(string: NSLocalizedString("Display Name", comment: ""))
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length)) placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
result.attributedPlaceholder = placeholder result.attributedPlaceholder = placeholder
result.tintColor = UIColor.lokiGreen() result.tintColor = UIColor.lokiGreen()
@ -14,11 +14,6 @@ final class DisplayNameVC : OnboardingBaseViewController {
result.keyboardAppearance = .dark result.keyboardAppearance = .dark
return result return result
}() }()
private var normalizedUserName: String? {
let result = userNameTextField.text!.ows_stripped()
return !result.isEmpty ? result : nil
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -59,16 +54,16 @@ final class DisplayNameVC : OnboardingBaseViewController {
} }
@objc private func handleNextButtonPressed() { @objc private func handleNextButtonPressed() {
if let normalizedName = normalizedUserName { let displayName = userNameTextField.text!.ows_stripped()
guard !OWSProfileManager.shared().isProfileNameTooLong(normalizedName) else { guard !displayName.isEmpty else {
return OWSAlerts.showErrorAlert(message: NSLocalizedString("PROFILE_VIEW_ERROR_PROFILE_NAME_TOO_LONG", comment: "Error message shown when user tries to update profile with a profile name that is too long")) return OWSAlerts.showErrorAlert(message: NSLocalizedString("Please pick a display name", comment: ""))
} }
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
return OWSAlerts.showErrorAlert(message: NSLocalizedString("Please pick a shorter display name", comment: ""))
} }
TSAccountManager.sharedInstance().didRegister() TSAccountManager.sharedInstance().didRegister()
UserDefaults.standard.set(true, forKey: "didUpdateForMainnet") UserDefaults.standard.set(true, forKey: "didUpdateForMainnet")
onboardingController.verificationDidComplete(fromView: self) onboardingController.verificationDidComplete(fromView: self)
if let normalizedName = normalizedUserName { OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
OWSProfileManager.shared().updateLocalProfileName(normalizedName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
}
} }
} }

@ -1,6 +0,0 @@
@objc(LKUserSelectionViewDelegate)
protocol UserSelectionViewDelegate {
func handleUserSelected(_ user: String, from userSelectionView: UserSelectionView)
}

@ -0,0 +1,49 @@
@objc(LKMentionUtilities)
public final class MentionUtilities : NSObject {
override private init() { }
@objc public static func highlightMentions(in string: String, thread: TSThread) -> String {
return highlightMentions(in: string, isOutgoingMessage: false, thread: thread, attributes: [:]).string // isOutgoingMessage and attributes are irrelevant
}
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, thread: TSThread, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
var string = string
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: [])
let knownUserHexEncodedPublicKeys = LokiAPI.userHexEncodedPublicKeyCache[thread.uniqueId!] ?? [] // Should always be populated at this point
var mentions: [NSRange] = []
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.count))
while let match = outerMatch, thread.isGroupThread() {
let hexEncodedPublicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
let matchEnd: Int
if knownUserHexEncodedPublicKeys.contains(hexEncodedPublicKey) {
var userDisplayName: String?
if hexEncodedPublicKey == OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey {
userDisplayName = OWSProfileManager.shared().localProfileName()
} else {
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
userDisplayName = transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String?
}
}
if let userDisplayName = userDisplayName {
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(userDisplayName)")
mentions.append(NSRange(location: match.range.location, length: userDisplayName.count + 1)) // + 1 to include the @
matchEnd = match.range.location + userDisplayName.count
} else {
matchEnd = match.range.location + match.range.length
}
} else {
matchEnd = match.range.location + match.range.length
}
outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: matchEnd, length: string.count - matchEnd))
}
let result = NSMutableAttributedString(string: string, attributes: attributes)
mentions.forEach { mention in
let color: UIColor = isOutgoingMessage ? .lokiDarkGray() : .lokiGreen()
result.addAttribute(.backgroundColor, value: color, range: mention)
}
return result
}
}

@ -239,9 +239,8 @@ NS_ASSUME_NONNULL_BEGIN
[self.stackView addArrangedSubview:spacerView]; [self.stackView addArrangedSubview:spacerView];
} }
DisplayableText *_Nullable displayableQuotedText DisplayableText *_Nullable displayableQuotedText = [self getDisplayableQuotedText];
= (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
OWSQuotedMessageView *quotedMessageView = OWSQuotedMessageView *quotedMessageView =
[OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply
displayableQuotedText:displayableQuotedText displayableQuotedText:displayableQuotedText
@ -684,7 +683,7 @@ NS_ASSUME_NONNULL_BEGIN
font:self.textMessageFont font:self.textMessageFont
shouldIgnoreEvents:shouldIgnoreEvents shouldIgnoreEvents:shouldIgnoreEvents
thread:self.viewItem.interaction.thread thread:self.viewItem.interaction.thread
isOutgoing:[self.viewItem.interaction isKindOfClass:TSOutgoingMessage.self]]; isOutgoingMessage:[self.viewItem.interaction isKindOfClass:TSOutgoingMessage.self]];
} }
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView + (void)loadForTextDisplay:(OWSMessageTextView *)textView
@ -694,7 +693,7 @@ NS_ASSUME_NONNULL_BEGIN
font:(UIFont *)font font:(UIFont *)font
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
thread:(TSThread *)thread thread:(TSThread *)thread
isOutgoing:(BOOL)isOutgoing isOutgoingMessage:(BOOL)isOutgoingMessage
{ {
textView.hidden = NO; textView.hidden = NO;
textView.textColor = textColor; textView.textColor = textColor;
@ -708,57 +707,18 @@ NS_ASSUME_NONNULL_BEGIN
NSString *text = displayableText.displayText; NSString *text = displayableText.displayText;
NSError *error1; NSMutableAttributedString *attributedText = [LKMentionUtilities highlightMentionsIn:text isOutgoingMessage:isOutgoingMessage thread:thread attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }].mutableCopy;
NSRegularExpression *regex1 = [[NSRegularExpression alloc] initWithPattern:@"@\\w*" options:0 error:&error1];
OWSAssertDebug(error1 == nil);
NSSet<NSString *> *knownUserIDs = LKAPI.userIDCache[thread.uniqueId];
NSMutableArray<NSValue *> *mentions = [NSMutableArray new];
NSTextCheckingResult *match1 = [regex1 firstMatchInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)];
if (match1 != nil && thread.isGroupThread) {
while (YES) {
NSString *userID = [[text substringWithRange:match1.range] stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""];
NSUInteger matchEnd;
if ([knownUserIDs containsObject:userID]) {
__block NSString *userDisplayName;
if ([userID isEqual:OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey]) {
userDisplayName = OWSProfileManager.sharedManager.localProfileName;
} else {
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSString *collection = [NSString stringWithFormat:@"%@.%llu", LKGroupChatAPI.publicChatServer, LKGroupChatAPI.publicChatServerID];
userDisplayName = [transaction objectForKey:userID inCollection:collection];
}];
}
if (userDisplayName != nil) {
text = [text stringByReplacingCharactersInRange:match1.range withString:[NSString stringWithFormat:@"@%@", userDisplayName]];
[mentions addObject:[NSValue valueWithRange:NSMakeRange(match1.range.location, userDisplayName.length + 1)]];
matchEnd = match1.range.location + userDisplayName.length;
} else {
matchEnd = match1.range.location + match1.range.length;
}
} else {
matchEnd = match1.range.location + match1.range.length;
}
match1 = [regex1 firstMatchInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(matchEnd, text.length - matchEnd)];
if (match1 == nil) { break; }
}
}
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }];
for (NSValue *mention in mentions) {
NSRange range = mention.rangeValue;
UIColor *highlightColor = isOutgoing ? UIColor.lokiDarkGray : UIColor.lokiGreen;
[attributedText addAttribute:NSBackgroundColorAttributeName value:highlightColor range:range];
}
if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) { if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
NSString *searchableText = [FullTextSearchFinder normalizeWithText:searchText]; NSString *searchableText = [FullTextSearchFinder normalizeWithText:searchText];
NSError *error2; NSError *error;
NSRegularExpression *regex2 = [[NSRegularExpression alloc] initWithPattern:[NSRegularExpression escapedPatternForString:searchableText] options:NSRegularExpressionCaseInsensitive error:&error2]; NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:[NSRegularExpression escapedPatternForString:searchableText] options:NSRegularExpressionCaseInsensitive error:&error];
OWSAssertDebug(error2 == nil); OWSAssertDebug(error == nil);
for (NSTextCheckingResult *match2 in for (NSTextCheckingResult *match in
[regex2 matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) { [regex matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
OWSAssertDebug(match2.range.length >= ConversationSearchController.kMinimumSearchTextLength); OWSAssertDebug(match.range.length >= ConversationSearchController.kMinimumSearchTextLength);
[attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match2.range]; [attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match.range];
[attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match2.range]; [attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match.range];
} }
} }
@ -1181,9 +1141,8 @@ NS_ASSUME_NONNULL_BEGIN
return nil; return nil;
} }
DisplayableText *_Nullable displayableQuotedText DisplayableText *_Nullable displayableQuotedText = [self getDisplayableQuotedText];
= (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
OWSQuotedMessageView *quotedMessageView = OWSQuotedMessageView *quotedMessageView =
[OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply
displayableQuotedText:displayableQuotedText displayableQuotedText:displayableQuotedText
@ -1194,6 +1153,15 @@ NS_ASSUME_NONNULL_BEGIN
return [NSValue valueWithCGSize:CGSizeCeil(result)]; return [NSValue valueWithCGSize:CGSizeCeil(result)];
} }
- (DisplayableText *)getDisplayableQuotedText
{
if (!self.viewItem.hasQuotedText) { return nil; }
NSString *rawText = self.viewItem.displayableQuotedText.fullText;
TSThread *thread = self.viewItem.interaction.thread;
NSString *text = [LKMentionUtilities highlightMentionsIn:rawText thread:thread];
return [DisplayableText displayableText:text];
}
- (nullable NSValue *)senderNameSize - (nullable NSValue *)senderNameSize
{ {
OWSAssertDebug(self.conversationStyle); OWSAssertDebug(self.conversationStyle);

@ -5,7 +5,8 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle; @class ConversationStyle;
@class LKUserSelectionView; @class LKMention;
@class LKMentionCandidateSelectionView;
@class OWSLinkPreviewDraft; @class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel; @class OWSQuotedReplyModel;
@class SignalAttachment; @class SignalAttachment;
@ -29,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha; - (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView; - (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView;
@end @end
@ -84,11 +85,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)hideInputMethod; - (void)hideInputMethod;
#pragma mark - User Selection View #pragma mark - Mention Candidate Selection View
- (void)showUserSelectionViewFor:(NSArray<NSString *> *)users in:(TSThread *)thread; - (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread;
- (void)hideUserSelectionView; - (void)hideMentionCandidateSelectionView;
@end @end

@ -51,7 +51,7 @@ const CGFloat kMaxTextViewHeight = 98;
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate, @interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate,
QuotedReplyPreviewDelegate, QuotedReplyPreviewDelegate,
LinkPreviewViewDraftDelegate, LinkPreviewViewDraftDelegate,
LKUserSelectionViewDelegate> LKMentionCandidateSelectionViewDelegate>
@property (nonatomic, readonly) ConversationStyle *conversationStyle; @property (nonatomic, readonly) ConversationStyle *conversationStyle;
@ -86,8 +86,8 @@ const CGFloat kMaxTextViewHeight = 98;
@property (nonatomic, nullable) InputLinkPreview *inputLinkPreview; @property (nonatomic, nullable) InputLinkPreview *inputLinkPreview;
@property (nonatomic) BOOL wasLinkPreviewCancelled; @property (nonatomic) BOOL wasLinkPreviewCancelled;
@property (nonatomic, nullable, weak) LinkPreviewView *linkPreviewView; @property (nonatomic, nullable, weak) LinkPreviewView *linkPreviewView;
@property (nonatomic) LKUserSelectionView *userSelectionView; @property (nonatomic) LKMentionCandidateSelectionView *mentionCandidateSelectionView;
@property (nonatomic) NSLayoutConstraint *userSelectionViewSizeConstraint; @property (nonatomic) NSLayoutConstraint *mentionCandidateSelectionViewSizeConstraint;
@end @end
@ -223,12 +223,12 @@ const CGFloat kMaxTextViewHeight = 98;
[vStackWrapper setCompressionResistanceHorizontalLow]; [vStackWrapper setCompressionResistanceHorizontalLow];
// User Selection View // User Selection View
_userSelectionView = [LKUserSelectionView new]; _mentionCandidateSelectionView = [LKMentionCandidateSelectionView new];
[self addSubview:self.userSelectionView]; [self addSubview:self.mentionCandidateSelectionView];
[self.userSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop]; [self.mentionCandidateSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.userSelectionView autoPinWidthToSuperview]; [self.mentionCandidateSelectionView autoPinWidthToSuperview];
self.userSelectionViewSizeConstraint = [self.userSelectionView autoSetDimension:ALDimensionHeight toSize:0]; self.mentionCandidateSelectionViewSizeConstraint = [self.mentionCandidateSelectionView autoSetDimension:ALDimensionHeight toSize:0];
self.userSelectionView.delegate = self; self.mentionCandidateSelectionView.delegate = self;
// H Stack // H Stack
_hStack = [[UIStackView alloc] _hStack = [[UIStackView alloc]
@ -240,7 +240,7 @@ const CGFloat kMaxTextViewHeight = 98;
self.hStack.spacing = 8; self.hStack.spacing = 8;
[self addSubview:self.hStack]; [self addSubview:self.hStack];
[self.hStack autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.userSelectionView]; [self.hStack autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.mentionCandidateSelectionView];
[self.hStack autoPinEdgeToSuperviewSafeArea:ALEdgeBottom]; [self.hStack autoPinEdgeToSuperviewSafeArea:ALEdgeBottom];
[self.hStack setContentHuggingHorizontalLow]; [self.hStack setContentHuggingHorizontalLow];
[self.hStack setCompressionResistanceHorizontalLow]; [self.hStack setCompressionResistanceHorizontalLow];
@ -1089,27 +1089,29 @@ const CGFloat kMaxTextViewHeight = 98;
self.borderView.hidden = YES; self.borderView.hidden = YES;
} }
#pragma mark - User Selection View #pragma mark - Mention Candidate Selection View
- (void)showUserSelectionViewFor:(NSArray<NSString *> *)users in:(TSThread *)thread - (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread
{ {
self.userSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users self.mentionCandidateSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users
self.userSelectionView.users = users; self.mentionCandidateSelectionView.mentionCandidates = mentionCandidates;
self.userSelectionViewSizeConstraint.constant = 10 + MIN(users.count, 4) * 52; self.mentionCandidateSelectionViewSizeConstraint.constant = 6 + MIN(mentionCandidates.count, 4) * 52;
[self setNeedsLayout]; [self setNeedsLayout];
[self layoutIfNeeded]; [self layoutIfNeeded];
[self.mentionCandidateSelectionView.tableView setContentOffset:CGPointMake(0, -6)]; // TODO: Workaround for content offset bug
} }
- (void)hideUserSelectionView - (void)hideMentionCandidateSelectionView
{ {
self.userSelectionViewSizeConstraint.constant = 0; self.mentionCandidateSelectionViewSizeConstraint.constant = 0;
[self setNeedsLayout]; [self setNeedsLayout];
[self layoutIfNeeded]; [self layoutIfNeeded];
[self.mentionCandidateSelectionView.tableView setContentOffset:CGPointMake(0, 0)];
} }
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView - (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView
{ {
[self.inputToolbarDelegate handleUserSelected:user from:userSelectionView]; [self.inputToolbarDelegate handleMentionCandidateSelected:mentionCandidate from:mentionCandidateSelectionView];
} }
@end @end

@ -213,7 +213,10 @@ typedef enum : NSUInteger {
@property (nonatomic) CGFloat extraContentInsetPadding; @property (nonatomic) CGFloat extraContentInsetPadding;
@property (nonatomic) CGFloat contentInsetBottom; @property (nonatomic) CGFloat contentInsetBottom;
@property (nonatomic) NSInteger mentionStartIndex; // Mentions
@property (nonatomic) NSInteger currentMentionStartIndex;
@property (nonatomic) NSMutableArray<LKMention *> *mentions;
@property (nonatomic) NSString *oldText;
@end @end
@ -259,7 +262,9 @@ typedef enum : NSUInteger {
self.scrollContinuity = kScrollContinuityBottom; self.scrollContinuity = kScrollContinuityBottom;
_mentionStartIndex = -1; _currentMentionStartIndex = -1;
_mentions = [NSMutableArray new];
_oldText = @"";
} }
#pragma mark - Dependencies #pragma mark - Dependencies
@ -528,7 +533,7 @@ typedef enum : NSUInteger {
userInfo:nil userInfo:nil
repeats:YES]; repeats:YES];
[LKAPI populateUserIDCacheIfNeededFor:thread.uniqueId in:nil]; [LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:thread.uniqueId in:nil];
} }
- (void)dealloc - (void)dealloc
@ -3019,6 +3024,7 @@ typedef enum : NSUInteger {
{ {
[self tryToSendAttachments:attachments messageText:messageText]; [self tryToSendAttachments:attachments messageText:messageText];
[self.inputToolbar clearTextMessageAnimated:NO]; [self.inputToolbar clearTextMessageAnimated:NO];
[self resetMentions];
// we want to already be at the bottom when the user returns, rather than have to watch // we want to already be at the bottom when the user returns, rather than have to watch
// the new message scroll into view. // the new message scroll into view.
@ -3770,35 +3776,70 @@ typedef enum : NSUInteger {
- (void)textViewDidChange:(UITextView *)textView - (void)textViewDidChange:(UITextView *)textView
{ {
if (textView.text.length > 0) { // Prepare
NSString *newText = textView.text;
// Typing indicators
if (newText.length > 0) {
[self.typingIndicators didStartTypingOutgoingInputInThread:self.thread]; [self.typingIndicators didStartTypingOutgoingInputInThread:self.thread];
} }
NSUInteger currentEndIndex = (textView.text.length != 0) ? textView.text.length - 1 : 0; // Mentions
unichar lastCharacter = [textView.text characterAtIndex:currentEndIndex]; BOOL isBackspace = newText.length < self.oldText.length;
NSMutableCharacterSet *allowedCharacters = NSMutableCharacterSet.lowercaseLetterCharacterSet; if (isBackspace) {
[allowedCharacters formUnionWithCharacterSet:NSCharacterSet.uppercaseLetterCharacterSet]; self.currentMentionStartIndex = -1;
if (lastCharacter == '@') { [self.inputToolbar hideMentionCandidateSelectionView];
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:@"" in:self.thread.uniqueId]; for (LKMention *mention in self.mentions) {
self.mentionStartIndex = (NSInteger)currentEndIndex + 1; if (![mention isContainedIn:newText]) {
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread]; [self.mentions removeObject:mention];
} else if (![allowedCharacters characterIsMember:lastCharacter]) { }
self.mentionStartIndex = -1; }
[self.inputToolbar hideUserSelectionView]; } else if (newText.length > 0) {
} else { NSUInteger lastCharacterIndex = newText.length - 1;
if (self.mentionStartIndex != -1) { unichar lastCharacter = [newText characterAtIndex:lastCharacterIndex];
NSString *query = [textView.text substringFromIndex:(NSUInteger)self.mentionStartIndex]; if (lastCharacter == '@') {
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:query in:self.thread.uniqueId]; NSArray<LKMention *> *mentionCandidates = [LKAPI getMentionCandidatesFor:@"" in:self.thread.uniqueId];
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread]; self.currentMentionStartIndex = (NSInteger)lastCharacterIndex;
[self.inputToolbar showMentionCandidateSelectionViewFor:mentionCandidates in:self.thread];
} else if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:lastCharacter]) {
self.currentMentionStartIndex = -1;
[self.inputToolbar hideMentionCandidateSelectionView];
} else {
if (self.currentMentionStartIndex != -1) {
NSString *query = [newText substringFromIndex:(NSUInteger)self.currentMentionStartIndex + 1]; // + 1 to get rid of the @
NSArray<LKMention *> *mentionCandidates = [LKAPI getMentionCandidatesFor:query in:self.thread.uniqueId];
[self.inputToolbar showMentionCandidateSelectionViewFor:mentionCandidates in:self.thread];
}
} }
} }
self.oldText = newText;
} }
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView - (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView
{ {
NSUInteger mentionStartIndex = (NSUInteger)self.currentMentionStartIndex;
[self.mentions addObject:mentionCandidate];
NSString *oldText = self.inputToolbar.messageText; NSString *oldText = self.inputToolbar.messageText;
NSUInteger mentionStartIndex = (NSUInteger)self.mentionStartIndex; NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:[NSString stringWithFormat:@"@%@", mentionCandidate.displayName]];
NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:user];
[self.inputToolbar setMessageText:newText animated:NO]; [self.inputToolbar setMessageText:newText animated:NO];
self.currentMentionStartIndex = -1;
[self.inputToolbar hideMentionCandidateSelectionView];
self.oldText = newText;
}
- (NSString *)getSendText
{
NSString *result = self.inputToolbar.messageText;
for (LKMention *mention in self.mentions) {
NSRange range = [result rangeOfString:[NSString stringWithFormat:@"@%@", mention.displayName]];
result = [result stringByReplacingCharactersInRange:range withString:[[NSString alloc] initWithFormat:@"@%@", mention.hexEncodedPublicKey]];
}
return result;
}
- (void)resetMentions
{
self.oldText = @"";
self.currentMentionStartIndex = -1;
self.mentions = @[].mutableCopy;
} }
- (void)inputTextViewSendMessagePressed - (void)inputTextViewSendMessagePressed
@ -4049,6 +4090,7 @@ typedef enum : NSUInteger {
{ {
[self tryToSendAttachments:attachments messageText:messageText]; [self tryToSendAttachments:attachments messageText:messageText];
[self.inputToolbar clearTextMessageAnimated:NO]; [self.inputToolbar clearTextMessageAnimated:NO];
[self resetMentions];
[self dismissViewControllerAnimated:YES completion:nil]; [self dismissViewControllerAnimated:YES completion:nil];
// We always want to scroll to the bottom of the conversation after the local user // We always want to scroll to the bottom of the conversation after the local user
@ -4419,7 +4461,8 @@ typedef enum : NSUInteger {
[BenchManager startEventWithTitle:@"Send Message milestone: toggleDefaultKeyboard completed" [BenchManager startEventWithTitle:@"Send Message milestone: toggleDefaultKeyboard completed"
eventId:@"fromSendUntil_toggleDefaultKeyboard"]; eventId:@"fromSendUntil_toggleDefaultKeyboard"];
[self tryToSendTextMessage:self.inputToolbar.messageText updateKeyboardState:YES]; [self.inputToolbar hideMentionCandidateSelectionView];
[self tryToSendTextMessage:[self getSendText] updateKeyboardState:YES];
} }
- (void)tryToSendTextMessage:(NSString *)text updateKeyboardState:(BOOL)updateKeyboardState - (void)tryToSendTextMessage:(NSString *)text updateKeyboardState:(BOOL)updateKeyboardState
@ -4477,6 +4520,7 @@ typedef enum : NSUInteger {
[BenchManager benchWithTitle:@"clearTextMessageAnimated" [BenchManager benchWithTitle:@"clearTextMessageAnimated"
block:^{ block:^{
[self.inputToolbar clearTextMessageAnimated:YES]; [self.inputToolbar clearTextMessageAnimated:YES];
[self resetMentions];
}]; }];
[BenchManager completeEventWithEventId:@"fromSendUntil_clearTextMessageAnimated"]; [BenchManager completeEventWithEventId:@"fromSendUntil_clearTextMessageAnimated"];

@ -395,6 +395,8 @@ NS_ASSUME_NONNULL_BEGIN
} }
NSString *displayableText = thread.lastMessageText; NSString *displayableText = thread.lastMessageText;
if (displayableText) { if (displayableText) {
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:thread.threadRecord.uniqueId in:nil]; // TODO: Terrible place to do this, but okay for now
displayableText = [LKMentionUtilities highlightMentionsIn:displayableText thread:thread.threadRecord];
[snippetText appendAttributedString:[[NSAttributedString alloc] [snippetText appendAttributedString:[[NSAttributedString alloc]
initWithString:displayableText initWithString:displayableText
attributes:@{ attributes:@{

@ -393,12 +393,13 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
__weak ProfileViewController *weakSelf = self; __weak ProfileViewController *weakSelf = self;
NSString *normalizedProfileName = [self normalizedProfileName]; NSString *normalizedProfileName = [self normalizedProfileName];
if (normalizedProfileName.length == 0) {
return [OWSAlerts showErrorAlertWithMessage:NSLocalizedString(@"Please pick a display name", @"")];
}
if ([OWSProfileManager.sharedManager isProfileNameTooLong:normalizedProfileName]) { if ([OWSProfileManager.sharedManager isProfileNameTooLong:normalizedProfileName]) {
[OWSAlerts return [OWSAlerts showErrorAlertWithMessage:NSLocalizedString(@"Please pick a shorter display name", @"")];
showErrorAlertWithMessage:NSLocalizedString(@"PROFILE_VIEW_ERROR_PROFILE_NAME_TOO_LONG",
@"Error message shown when user tries to update profile with a profile name "
@"that is too long.")];
return;
} }
[LKAnalytics.shared track:@"Display Name Updated"]; [LKAnalytics.shared track:@"Display Name Updated"];

@ -2547,7 +2547,7 @@
"Loki Messenger can let you know when you get a message (and who it is from)" = "Loki Messenger can let you know when you get a message (and who it is from)"; "Loki Messenger can let you know when you get a message (and who it is from)" = "Loki Messenger can let you know when you get a message (and who it is from)";
"Create Your Loki Messenger Account" = "Create Your Loki Messenger Account"; "Create Your Loki Messenger Account" = "Create Your Loki Messenger Account";
"Enter a name to be shown to your contacts" = "Enter a name to be shown to your contacts"; "Enter a name to be shown to your contacts" = "Enter a name to be shown to your contacts";
"Display Name (Optional)" = "Display Name (Optional)"; "Display Name" = "Display Name";
"Type an optional password for added security" = "Type an optional password for added security"; "Type an optional password for added security" = "Type an optional password for added security";
"Password (Optional)" = "Password (Optional)"; "Password (Optional)" = "Password (Optional)";
"Next" = "Next"; "Next" = "Next";
@ -2593,7 +2593,6 @@
"Your Seed" = "Your Seed"; "Your Seed" = "Your Seed";
"Unlock Loki Messenger's screen using Touch ID, Face ID, or your iOS device passcode. You can still answer incoming calls and receive message notifications while Screen Lock is enabled. Loki Messenger's notification settings allow you to customize the information that is displayed." = "Unlock Loki Messenger's screen using Touch ID, Face ID, or your iOS device passcode. You can still answer incoming calls and receive message notifications while Screen Lock is enabled. Loki Messenger's notification settings allow you to customize the information that is displayed."; "Unlock Loki Messenger's screen using Touch ID, Face ID, or your iOS device passcode. You can still answer incoming calls and receive message notifications while Screen Lock is enabled. Loki Messenger's notification settings allow you to customize the information that is displayed." = "Unlock Loki Messenger's screen using Touch ID, Face ID, or your iOS device passcode. You can still answer incoming calls and receive message notifications while Screen Lock is enabled. Loki Messenger's notification settings allow you to customize the information that is displayed.";
"Prevent Loki Messenger previews from appearing in the app switcher." = "Prevent Loki Messenger previews from appearing in the app switcher."; "Prevent Loki Messenger previews from appearing in the app switcher." = "Prevent Loki Messenger previews from appearing in the app switcher.";
"Display Name" = "Display Name";
"Loki Messenger" = "Loki Messenger"; "Loki Messenger" = "Loki Messenger";
"Privacy Policy" = "Privacy Policy"; "Privacy Policy" = "Privacy Policy";
"New Conversation" = "New Conversation"; "New Conversation" = "New Conversation";
@ -2643,3 +2642,5 @@
"Anonymous" = "Anonymous"; "Anonymous" = "Anonymous";
"Invalid server URL provided" = "Invalid server URL provided"; "Invalid server URL provided" = "Invalid server URL provided";
"Please make sure you have provided the full url" = "Please make sure you have provided the full url. E.g https://public-chat-server.url/"; "Please make sure you have provided the full url" = "Please make sure you have provided the full url. E.g https://public-chat-server.url/";
"Please pick a shorter display name" = "Please pick a shorter display name";
"Please pick a display name" = "Please pick a display name";

@ -5,7 +5,7 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs) private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
// MARK: Settings // MARK: Settings
private static let fallbackBatchCount = 20 private static let fallbackBatchCount = 256
private static let maxRetryCount: UInt = 8 private static let maxRetryCount: UInt = 8
// MARK: Public Chat // MARK: Public Chat

@ -3,7 +3,7 @@ import PromiseKit
@objc(LKAPI) @objc(LKAPI)
public final class LokiAPI : NSObject { public final class LokiAPI : NSObject {
private static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date private static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date
@objc static var userIDCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys @objc public static var userHexEncodedPublicKeyCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
// MARK: Convenience // MARK: Convenience
internal static let storage = OWSPrimaryStorage.shared() internal static let storage = OWSPrimaryStorage.shared()
@ -296,42 +296,43 @@ public final class LokiAPI : NSObject {
} }
// MARK: User ID Caching // MARK: User ID Caching
@objc public static func cache(_ userHexEncodedPublicKey: String, for threadID: String) { @objc public static func cache(_ hexEncodedPublicKey: String, for threadID: String) {
if let cache = userIDCache[threadID] { if let cache = userHexEncodedPublicKeyCache[threadID] {
var mutableCache = cache userHexEncodedPublicKeyCache[threadID] = cache.union([ hexEncodedPublicKey ])
mutableCache.insert(userHexEncodedPublicKey)
userIDCache[threadID] = mutableCache
} else { } else {
userIDCache[threadID] = [ userHexEncodedPublicKey ] userHexEncodedPublicKeyCache[threadID] = [ hexEncodedPublicKey ]
} }
} }
@objc public static func getUserIDs(for query: String, in threadID: String) -> [String] { @objc public static func getMentionCandidates(for query: String, in threadID: String) -> [Mention] {
// Prepare // Prepare
guard let cache = userIDCache[threadID] else { return [] } guard let cache = userHexEncodedPublicKeyCache[threadID] else { return [] }
var candidates: [(id: String, displayName: String)] = [] var candidates: [Mention] = []
// Gather candidates // Gather candidates
storage.dbReadConnection.read { transaction in storage.dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)" let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
candidates = cache.flatMap { id in candidates = cache.flatMap { hexEncodedPublicKey in
guard let displayName = transaction.object(forKey: id, inCollection: collection) as! String? else { return nil } guard let displayName = transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String? else { return nil }
return (id: id, displayName: displayName) guard !displayName.hasPrefix("Anonymous") else { return nil }
return Mention(hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName)
} }
} }
// Sort alphabetically first // Sort alphabetically first
candidates.sort { $0.displayName < $1.displayName } candidates.sort { $0.displayName < $1.displayName }
if query.count >= 2 { if query.count >= 2 {
// Filter out any non-matching candidates // Filter out any non-matching candidates
candidates = candidates.filter { $0.displayName.contains(query) } candidates = candidates.filter { $0.displayName.lowercased().contains(query.lowercased()) }
// Sort based on where in the candidate the query occurs // Sort based on where in the candidate the query occurs
candidates.sort { $0.displayName.range(of: query)!.lowerBound < $1.displayName.range(of: query)!.lowerBound } candidates.sort {
$0.displayName.lowercased().range(of: query.lowercased())!.lowerBound < $1.displayName.lowercased().range(of: query.lowercased())!.lowerBound
}
} }
// Return // Return
return candidates.map { $0.id } // Inefficient to do this and then look up the display name again later, but easy to interface with Obj-C return candidates
} }
@objc public static func populateUserIDCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadWriteTransaction? = nil) { @objc public static func populateUserHexEncodedPublicKeyCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
guard userIDCache[threadID] == nil else { return } guard userHexEncodedPublicKeyCache[threadID] == nil else { return }
var result: Set<String> = [] var result: Set<String> = []
func populate(in transaction: YapDatabaseReadWriteTransaction) { func populate(in transaction: YapDatabaseReadWriteTransaction) {
guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return } guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
@ -349,7 +350,7 @@ public final class LokiAPI : NSObject {
} }
} }
result.insert(userHexEncodedPublicKey) result.insert(userHexEncodedPublicKey)
userIDCache[threadID] = result userHexEncodedPublicKeyCache[threadID] = result
} }
} }

@ -0,0 +1,15 @@
@objc(LKMention)
public final class Mention : NSObject {
@objc public let hexEncodedPublicKey: String
@objc public let displayName: String
@objc public init(hexEncodedPublicKey: String, displayName: String) {
self.hexEncodedPublicKey = hexEncodedPublicKey
self.displayName = displayName
}
@objc public func isContained(in string: String) -> Bool {
return string.contains(displayName)
}
}

@ -1414,7 +1414,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
// Loki: Cache the user hex encoded public key (for mentions) // Loki: Cache the user hex encoded public key (for mentions)
[LKAPI populateUserIDCacheIfNeededFor:oldGroupThread.uniqueId in:transaction]; [LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
[LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId]; [LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId];
[self finalizeIncomingMessage:incomingMessage [self finalizeIncomingMessage:incomingMessage

Loading…
Cancel
Save