Merge branch 'dev' into custom-server
This commit is contained in:
commit
86550c8877
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit 68a1e49959447a8ef4b4de77edec53375c598268
|
||||
Subproject commit 56980ceea7cc0964b92f98831e79fea03e76e801
|
|
@ -567,6 +567,7 @@
|
|||
B8258493230FA5E9001B41CB /* ScanQRCodeVC.m in Sources */ = {isa = PBXBuildFile; fileRef = B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */; };
|
||||
B82584A02315024B001B41CB /* RSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* RSSFeedPoller.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 */; };
|
||||
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.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 */; };
|
||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
|
||||
B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */; };
|
||||
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* UserSelectionView.swift */; };
|
||||
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */; };
|
||||
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; };
|
||||
B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */; };
|
||||
B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; };
|
||||
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1387,8 +1389,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionViewDelegate.swift; sourceTree = "<group>"; };
|
||||
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.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>"; };
|
||||
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>"; };
|
||||
|
@ -2656,8 +2658,8 @@
|
|||
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
|
||||
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
|
||||
24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */,
|
||||
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */,
|
||||
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */,
|
||||
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
|
||||
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
|
||||
);
|
||||
path = Loki;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2699,6 +2701,7 @@
|
|||
children = (
|
||||
B86BD08323399ACF000F5AE3 /* Modal.swift */,
|
||||
B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */,
|
||||
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3845,7 +3848,7 @@
|
|||
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
|
||||
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */,
|
||||
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
|
||||
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */,
|
||||
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */,
|
||||
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
|
||||
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
|
||||
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */,
|
||||
|
@ -3869,7 +3872,7 @@
|
|||
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
||||
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
|
||||
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */,
|
||||
B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */,
|
||||
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
|
||||
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
|
||||
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
|
||||
|
@ -3879,6 +3882,7 @@
|
|||
345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */,
|
||||
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
|
||||
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
|
||||
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
|
||||
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
|
||||
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
|
||||
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */,
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
|
||||
// MARK: - User Selection View
|
||||
|
||||
@objc(LKUserSelectionView)
|
||||
final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
|
||||
@objc var users: [String] = [] { didSet { tableView.reloadData() } }
|
||||
@objc(LKMentionCandidateSelectionView)
|
||||
final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
|
||||
@objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
|
||||
@objc var hasGroupContext = false
|
||||
@objc var delegate: UserSelectionViewDelegate?
|
||||
@objc var delegate: MentionCandidateSelectionViewDelegate?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tableView: UITableView = {
|
||||
@objc lazy var tableView: UITableView = { // TODO: Make this private
|
||||
let result = UITableView()
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.register(Cell.self, forCellReuseIdentifier: "Cell")
|
||||
result.separatorStyle = .none
|
||||
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
|
||||
}()
|
||||
|
||||
|
@ -37,30 +37,30 @@ final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelega
|
|||
|
||||
// MARK: Data
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return users.count
|
||||
return mentionCandidates.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
|
||||
let user = users[indexPath.row]
|
||||
cell.user = user
|
||||
let mentionCandidate = mentionCandidates[indexPath.row]
|
||||
cell.mentionCandidate = mentionCandidate
|
||||
cell.hasGroupContext = hasGroupContext
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let user = users[indexPath.row]
|
||||
delegate?.handleUserSelected(user, from: self)
|
||||
let mentionCandidate = mentionCandidates[indexPath.row]
|
||||
delegate?.handleMentionCandidateSelected(mentionCandidate, from: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cell
|
||||
|
||||
private extension UserSelectionView {
|
||||
private extension MentionCandidateSelectionView {
|
||||
|
||||
final class Cell : UITableViewCell {
|
||||
var user = "" { didSet { update() } }
|
||||
var mentionCandidate = Mention(hexEncodedPublicKey: "", displayName: "") { didSet { update() } }
|
||||
var hasGroupContext = false
|
||||
|
||||
// MARK: Components
|
||||
|
@ -101,12 +101,12 @@ private extension UserSelectionView {
|
|||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 16
|
||||
stackView.set(.height, to: 44)
|
||||
stackView.set(.height, to: 36)
|
||||
contentView.addSubview(stackView)
|
||||
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(.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)
|
||||
// Set up the moderator icon image view
|
||||
moderatorIconImageView.set(.width, to: 20)
|
||||
|
@ -118,15 +118,10 @@ private extension UserSelectionView {
|
|||
|
||||
// MARK: Updating
|
||||
private func update() {
|
||||
var displayName: String = ""
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
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()
|
||||
displayNameLabel.text = mentionCandidate.displayName
|
||||
let profilePicture = OWSContactAvatarBuilder(signalId: mentionCandidate.hexEncodedPublicKey, colorName: .blue, diameter: 36).build()
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
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))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = UIColor.lokiGreen()
|
||||
|
@ -14,11 +14,6 @@ final class DisplayNameVC : OnboardingBaseViewController {
|
|||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
||||
private var normalizedUserName: String? {
|
||||
let result = userNameTextField.text!.ows_stripped()
|
||||
return !result.isEmpty ? result : nil
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -59,16 +54,16 @@ final class DisplayNameVC : OnboardingBaseViewController {
|
|||
}
|
||||
|
||||
@objc private func handleNextButtonPressed() {
|
||||
if let normalizedName = normalizedUserName {
|
||||
guard !OWSProfileManager.shared().isProfileNameTooLong(normalizedName) 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"))
|
||||
}
|
||||
let displayName = userNameTextField.text!.ows_stripped()
|
||||
guard !displayName.isEmpty else {
|
||||
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()
|
||||
UserDefaults.standard.set(true, forKey: "didUpdateForMainnet")
|
||||
onboardingController.verificationDidComplete(fromView: self)
|
||||
if let normalizedName = normalizedUserName {
|
||||
OWSProfileManager.shared().updateLocalProfileName(normalizedName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
|
||||
}
|
||||
OWSProfileManager.shared().updateLocalProfileName(displayName, 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];
|
||||
}
|
||||
|
||||
DisplayableText *_Nullable displayableQuotedText
|
||||
= (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
|
||||
|
||||
DisplayableText *_Nullable displayableQuotedText = [self getDisplayableQuotedText];
|
||||
|
||||
OWSQuotedMessageView *quotedMessageView =
|
||||
[OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply
|
||||
displayableQuotedText:displayableQuotedText
|
||||
|
@ -684,7 +683,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
font:self.textMessageFont
|
||||
shouldIgnoreEvents:shouldIgnoreEvents
|
||||
thread:self.viewItem.interaction.thread
|
||||
isOutgoing:[self.viewItem.interaction isKindOfClass:TSOutgoingMessage.self]];
|
||||
isOutgoingMessage:[self.viewItem.interaction isKindOfClass:TSOutgoingMessage.self]];
|
||||
}
|
||||
|
||||
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
|
||||
|
@ -694,7 +693,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
font:(UIFont *)font
|
||||
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
|
||||
thread:(TSThread *)thread
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
isOutgoingMessage:(BOOL)isOutgoingMessage
|
||||
{
|
||||
textView.hidden = NO;
|
||||
textView.textColor = textColor;
|
||||
|
@ -708,57 +707,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
NSString *text = displayableText.displayText;
|
||||
|
||||
NSError *error1;
|
||||
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];
|
||||
}
|
||||
NSMutableAttributedString *attributedText = [LKMentionUtilities highlightMentionsIn:text isOutgoingMessage:isOutgoingMessage thread:thread attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }].mutableCopy;
|
||||
|
||||
if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
|
||||
NSString *searchableText = [FullTextSearchFinder normalizeWithText:searchText];
|
||||
NSError *error2;
|
||||
NSRegularExpression *regex2 = [[NSRegularExpression alloc] initWithPattern:[NSRegularExpression escapedPatternForString:searchableText] options:NSRegularExpressionCaseInsensitive error:&error2];
|
||||
OWSAssertDebug(error2 == nil);
|
||||
for (NSTextCheckingResult *match2 in
|
||||
[regex2 matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
|
||||
OWSAssertDebug(match2.range.length >= ConversationSearchController.kMinimumSearchTextLength);
|
||||
[attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match2.range];
|
||||
[attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match2.range];
|
||||
NSError *error;
|
||||
NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:[NSRegularExpression escapedPatternForString:searchableText] options:NSRegularExpressionCaseInsensitive error:&error];
|
||||
OWSAssertDebug(error == nil);
|
||||
for (NSTextCheckingResult *match in
|
||||
[regex matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
|
||||
OWSAssertDebug(match.range.length >= ConversationSearchController.kMinimumSearchTextLength);
|
||||
[attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match.range];
|
||||
[attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match.range];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1181,9 +1141,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return nil;
|
||||
}
|
||||
|
||||
DisplayableText *_Nullable displayableQuotedText
|
||||
= (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
|
||||
|
||||
DisplayableText *_Nullable displayableQuotedText = [self getDisplayableQuotedText];
|
||||
|
||||
OWSQuotedMessageView *quotedMessageView =
|
||||
[OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply
|
||||
displayableQuotedText:displayableQuotedText
|
||||
|
@ -1194,6 +1153,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
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
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ConversationStyle;
|
||||
@class LKUserSelectionView;
|
||||
@class LKMention;
|
||||
@class LKMentionCandidateSelectionView;
|
||||
@class OWSLinkPreviewDraft;
|
||||
@class OWSQuotedReplyModel;
|
||||
@class SignalAttachment;
|
||||
|
@ -29,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
|
||||
|
||||
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView;
|
||||
- (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -84,11 +85,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (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
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ const CGFloat kMaxTextViewHeight = 98;
|
|||
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate,
|
||||
QuotedReplyPreviewDelegate,
|
||||
LinkPreviewViewDraftDelegate,
|
||||
LKUserSelectionViewDelegate>
|
||||
LKMentionCandidateSelectionViewDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
|
||||
|
||||
|
@ -86,8 +86,8 @@ const CGFloat kMaxTextViewHeight = 98;
|
|||
@property (nonatomic, nullable) InputLinkPreview *inputLinkPreview;
|
||||
@property (nonatomic) BOOL wasLinkPreviewCancelled;
|
||||
@property (nonatomic, nullable, weak) LinkPreviewView *linkPreviewView;
|
||||
@property (nonatomic) LKUserSelectionView *userSelectionView;
|
||||
@property (nonatomic) NSLayoutConstraint *userSelectionViewSizeConstraint;
|
||||
@property (nonatomic) LKMentionCandidateSelectionView *mentionCandidateSelectionView;
|
||||
@property (nonatomic) NSLayoutConstraint *mentionCandidateSelectionViewSizeConstraint;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -223,12 +223,12 @@ const CGFloat kMaxTextViewHeight = 98;
|
|||
[vStackWrapper setCompressionResistanceHorizontalLow];
|
||||
|
||||
// User Selection View
|
||||
_userSelectionView = [LKUserSelectionView new];
|
||||
[self addSubview:self.userSelectionView];
|
||||
[self.userSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
[self.userSelectionView autoPinWidthToSuperview];
|
||||
self.userSelectionViewSizeConstraint = [self.userSelectionView autoSetDimension:ALDimensionHeight toSize:0];
|
||||
self.userSelectionView.delegate = self;
|
||||
_mentionCandidateSelectionView = [LKMentionCandidateSelectionView new];
|
||||
[self addSubview:self.mentionCandidateSelectionView];
|
||||
[self.mentionCandidateSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
[self.mentionCandidateSelectionView autoPinWidthToSuperview];
|
||||
self.mentionCandidateSelectionViewSizeConstraint = [self.mentionCandidateSelectionView autoSetDimension:ALDimensionHeight toSize:0];
|
||||
self.mentionCandidateSelectionView.delegate = self;
|
||||
|
||||
// H Stack
|
||||
_hStack = [[UIStackView alloc]
|
||||
|
@ -240,7 +240,7 @@ const CGFloat kMaxTextViewHeight = 98;
|
|||
self.hStack.spacing = 8;
|
||||
|
||||
[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 setContentHuggingHorizontalLow];
|
||||
[self.hStack setCompressionResistanceHorizontalLow];
|
||||
|
@ -1089,27 +1089,29 @@ const CGFloat kMaxTextViewHeight = 98;
|
|||
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.userSelectionView.users = users;
|
||||
self.userSelectionViewSizeConstraint.constant = 10 + MIN(users.count, 4) * 52;
|
||||
self.mentionCandidateSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users
|
||||
self.mentionCandidateSelectionView.mentionCandidates = mentionCandidates;
|
||||
self.mentionCandidateSelectionViewSizeConstraint.constant = 6 + MIN(mentionCandidates.count, 4) * 52;
|
||||
[self setNeedsLayout];
|
||||
[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 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
|
||||
|
|
|
@ -213,7 +213,10 @@ typedef enum : NSUInteger {
|
|||
@property (nonatomic) CGFloat extraContentInsetPadding;
|
||||
@property (nonatomic) CGFloat contentInsetBottom;
|
||||
|
||||
@property (nonatomic) NSInteger mentionStartIndex;
|
||||
// Mentions
|
||||
@property (nonatomic) NSInteger currentMentionStartIndex;
|
||||
@property (nonatomic) NSMutableArray<LKMention *> *mentions;
|
||||
@property (nonatomic) NSString *oldText;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -259,7 +262,9 @@ typedef enum : NSUInteger {
|
|||
|
||||
self.scrollContinuity = kScrollContinuityBottom;
|
||||
|
||||
_mentionStartIndex = -1;
|
||||
_currentMentionStartIndex = -1;
|
||||
_mentions = [NSMutableArray new];
|
||||
_oldText = @"";
|
||||
}
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
@ -528,7 +533,7 @@ typedef enum : NSUInteger {
|
|||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
[LKAPI populateUserIDCacheIfNeededFor:thread.uniqueId in:nil];
|
||||
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:thread.uniqueId in:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
|
@ -3019,6 +3024,7 @@ typedef enum : NSUInteger {
|
|||
{
|
||||
[self tryToSendAttachments:attachments messageText:messageText];
|
||||
[self.inputToolbar clearTextMessageAnimated:NO];
|
||||
[self resetMentions];
|
||||
|
||||
// we want to already be at the bottom when the user returns, rather than have to watch
|
||||
// the new message scroll into view.
|
||||
|
@ -3770,35 +3776,70 @@ typedef enum : NSUInteger {
|
|||
|
||||
- (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];
|
||||
}
|
||||
NSUInteger currentEndIndex = (textView.text.length != 0) ? textView.text.length - 1 : 0;
|
||||
unichar lastCharacter = [textView.text characterAtIndex:currentEndIndex];
|
||||
NSMutableCharacterSet *allowedCharacters = NSMutableCharacterSet.lowercaseLetterCharacterSet;
|
||||
[allowedCharacters formUnionWithCharacterSet:NSCharacterSet.uppercaseLetterCharacterSet];
|
||||
if (lastCharacter == '@') {
|
||||
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:@"" in:self.thread.uniqueId];
|
||||
self.mentionStartIndex = (NSInteger)currentEndIndex + 1;
|
||||
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread];
|
||||
} else if (![allowedCharacters characterIsMember:lastCharacter]) {
|
||||
self.mentionStartIndex = -1;
|
||||
[self.inputToolbar hideUserSelectionView];
|
||||
} else {
|
||||
if (self.mentionStartIndex != -1) {
|
||||
NSString *query = [textView.text substringFromIndex:(NSUInteger)self.mentionStartIndex];
|
||||
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:query in:self.thread.uniqueId];
|
||||
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread];
|
||||
// Mentions
|
||||
BOOL isBackspace = newText.length < self.oldText.length;
|
||||
if (isBackspace) {
|
||||
self.currentMentionStartIndex = -1;
|
||||
[self.inputToolbar hideMentionCandidateSelectionView];
|
||||
for (LKMention *mention in self.mentions) {
|
||||
if (![mention isContainedIn:newText]) {
|
||||
[self.mentions removeObject:mention];
|
||||
}
|
||||
}
|
||||
} else if (newText.length > 0) {
|
||||
NSUInteger lastCharacterIndex = newText.length - 1;
|
||||
unichar lastCharacter = [newText characterAtIndex:lastCharacterIndex];
|
||||
if (lastCharacter == '@') {
|
||||
NSArray<LKMention *> *mentionCandidates = [LKAPI getMentionCandidatesFor:@"" in:self.thread.uniqueId];
|
||||
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;
|
||||
NSUInteger mentionStartIndex = (NSUInteger)self.mentionStartIndex;
|
||||
NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:user];
|
||||
NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:[NSString stringWithFormat:@"@%@", mentionCandidate.displayName]];
|
||||
[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
|
||||
|
@ -4049,6 +4090,7 @@ typedef enum : NSUInteger {
|
|||
{
|
||||
[self tryToSendAttachments:attachments messageText:messageText];
|
||||
[self.inputToolbar clearTextMessageAnimated:NO];
|
||||
[self resetMentions];
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
|
||||
// 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"
|
||||
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
|
||||
|
@ -4477,6 +4520,7 @@ typedef enum : NSUInteger {
|
|||
[BenchManager benchWithTitle:@"clearTextMessageAnimated"
|
||||
block:^{
|
||||
[self.inputToolbar clearTextMessageAnimated:YES];
|
||||
[self resetMentions];
|
||||
}];
|
||||
[BenchManager completeEventWithEventId:@"fromSendUntil_clearTextMessageAnimated"];
|
||||
|
||||
|
|
|
@ -395,6 +395,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
NSString *displayableText = thread.lastMessageText;
|
||||
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]
|
||||
initWithString:displayableText
|
||||
attributes:@{
|
||||
|
|
|
@ -393,12 +393,13 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
|
|||
__weak ProfileViewController *weakSelf = self;
|
||||
|
||||
NSString *normalizedProfileName = [self normalizedProfileName];
|
||||
|
||||
if (normalizedProfileName.length == 0) {
|
||||
return [OWSAlerts showErrorAlertWithMessage:NSLocalizedString(@"Please pick a display name", @"")];
|
||||
}
|
||||
|
||||
if ([OWSProfileManager.sharedManager isProfileNameTooLong:normalizedProfileName]) {
|
||||
[OWSAlerts
|
||||
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;
|
||||
return [OWSAlerts showErrorAlertWithMessage:NSLocalizedString(@"Please pick a shorter display name", @"")];
|
||||
}
|
||||
|
||||
[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)";
|
||||
"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";
|
||||
"Display Name (Optional)" = "Display Name (Optional)";
|
||||
"Display Name" = "Display Name";
|
||||
"Type an optional password for added security" = "Type an optional password for added security";
|
||||
"Password (Optional)" = "Password (Optional)";
|
||||
"Next" = "Next";
|
||||
|
@ -2593,7 +2593,6 @@
|
|||
"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.";
|
||||
"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";
|
||||
"Privacy Policy" = "Privacy Policy";
|
||||
"New Conversation" = "New Conversation";
|
||||
|
@ -2643,3 +2642,5 @@
|
|||
"Anonymous" = "Anonymous";
|
||||
"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 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)
|
||||
|
||||
// MARK: Settings
|
||||
private static let fallbackBatchCount = 20
|
||||
private static let fallbackBatchCount = 256
|
||||
private static let maxRetryCount: UInt = 8
|
||||
|
||||
// MARK: Public Chat
|
||||
|
|
|
@ -3,7 +3,7 @@ import PromiseKit
|
|||
@objc(LKAPI)
|
||||
public final class LokiAPI : NSObject {
|
||||
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
|
||||
internal static let storage = OWSPrimaryStorage.shared()
|
||||
|
@ -296,42 +296,43 @@ public final class LokiAPI : NSObject {
|
|||
}
|
||||
|
||||
// MARK: User ID Caching
|
||||
@objc public static func cache(_ userHexEncodedPublicKey: String, for threadID: String) {
|
||||
if let cache = userIDCache[threadID] {
|
||||
var mutableCache = cache
|
||||
mutableCache.insert(userHexEncodedPublicKey)
|
||||
userIDCache[threadID] = mutableCache
|
||||
@objc public static func cache(_ hexEncodedPublicKey: String, for threadID: String) {
|
||||
if let cache = userHexEncodedPublicKeyCache[threadID] {
|
||||
userHexEncodedPublicKeyCache[threadID] = cache.union([ hexEncodedPublicKey ])
|
||||
} 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
|
||||
guard let cache = userIDCache[threadID] else { return [] }
|
||||
var candidates: [(id: String, displayName: String)] = []
|
||||
guard let cache = userHexEncodedPublicKeyCache[threadID] else { return [] }
|
||||
var candidates: [Mention] = []
|
||||
// Gather candidates
|
||||
storage.dbReadConnection.read { transaction in
|
||||
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
|
||||
candidates = cache.flatMap { id in
|
||||
guard let displayName = transaction.object(forKey: id, inCollection: collection) as! String? else { return nil }
|
||||
return (id: id, displayName: displayName)
|
||||
candidates = cache.flatMap { hexEncodedPublicKey in
|
||||
guard let displayName = transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String? else { return nil }
|
||||
guard !displayName.hasPrefix("Anonymous") else { return nil }
|
||||
return Mention(hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName)
|
||||
}
|
||||
}
|
||||
// Sort alphabetically first
|
||||
candidates.sort { $0.displayName < $1.displayName }
|
||||
if query.count >= 2 {
|
||||
// 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
|
||||
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 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) {
|
||||
guard userIDCache[threadID] == nil else { return }
|
||||
@objc public static func populateUserHexEncodedPublicKeyCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
|
||||
guard userHexEncodedPublicKeyCache[threadID] == nil else { return }
|
||||
var result: Set<String> = []
|
||||
func populate(in transaction: YapDatabaseReadWriteTransaction) {
|
||||
guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
|
||||
|
@ -349,7 +350,7 @@ public final class LokiAPI : NSObject {
|
|||
}
|
||||
}
|
||||
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)
|
||||
[LKAPI populateUserIDCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
|
||||
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
|
||||
[LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId];
|
||||
|
||||
[self finalizeIncomingMessage:incomingMessage
|
||||
|
|
Loading…
Reference in New Issue