diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 51262850..15211380 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -227,6 +227,7 @@ D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; }; D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; }; D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; }; + D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; }; D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; }; D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; }; D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -547,6 +548,7 @@ D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = ""; }; D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = ""; }; D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = ""; }; + D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = ""; }; D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = ""; }; D6BC874421961F73006163F1 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = ""; }; @@ -1251,6 +1253,7 @@ D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */, D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */, D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */, + D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */, D67C57A721E2649B00C3118B /* Account Detail */, D67C57B021E28F9400C3118B /* Compose Status Reply */, D626494023C122C800612E6E /* Asset Picker */, @@ -1794,6 +1797,7 @@ D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */, + D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */, D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */, D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */, D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */, diff --git a/Tusker/Screens/Compose/ComposeCurrentAccount.swift b/Tusker/Screens/Compose/ComposeCurrentAccount.swift index 1bc0b8db..49121d12 100644 --- a/Tusker/Screens/Compose/ComposeCurrentAccount.swift +++ b/Tusker/Screens/Compose/ComposeCurrentAccount.swift @@ -20,8 +20,7 @@ struct ComposeCurrentAccount: View { HStack(alignment: .top) { ComposeAvatarImageView(url: account.avatar) VStack(alignment: .leading) { - Text(verbatim: account.displayName) - .font(.system(size: 20, weight: .semibold)) + AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20) .lineLimit(1) Text(verbatim: "@\(account.acct)") diff --git a/Tusker/Screens/Compose/ComposeReplyView.swift b/Tusker/Screens/Compose/ComposeReplyView.swift index 726b5745..6c5d6193 100644 --- a/Tusker/Screens/Compose/ComposeReplyView.swift +++ b/Tusker/Screens/Compose/ComposeReplyView.swift @@ -23,8 +23,7 @@ struct ComposeReplyView: View { VStack(alignment: .leading, spacing: 0) { HStack { - Text(verbatim: status.account.displayName) - .font(.system(size: 17, weight: .semibold)) + AccountDisplayNameLabel(account: status.account, fontSize: 17) .lineLimit(1) .layoutPriority(1) diff --git a/Tusker/Views/AccountDisplayNameLabel.swift b/Tusker/Views/AccountDisplayNameLabel.swift new file mode 100644 index 00000000..05c4161b --- /dev/null +++ b/Tusker/Views/AccountDisplayNameLabel.swift @@ -0,0 +1,109 @@ +// +// AccountDisplayNameLabel.swift +// Tusker +// +// Created by Shadowfacts on 9/7/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import SwiftUI + +private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) + +struct AccountDisplayNameLabel: View { + let account: AccountMO + let fontSize: Int + @State var text: Text + @State var emojiRequests = [ImageCache.Request]() + + init(account: AccountMO, fontSize: Int) { + self.account = account + self.fontSize = fontSize + self._text = State(initialValue: Text(verbatim: account.displayName)) + } + + var body: some View { + if #available(iOS 14.0, *) { + text + .font(.system(size: CGFloat(fontSize), weight: .semibold)) + .onAppear(perform: self.loadEmojis) + } else { + text + .font(.system(size: CGFloat(fontSize), weight: .semibold)) + } + } + + // embedding Image inside Text is only available on iOS 14 + @available(iOS 14.0, *) + private func loadEmojis() { + let fullRange = NSRange(account.displayName.startIndex..., in: account.displayName) + let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange) + guard !matches.isEmpty else { return } + + let emojiImages = CachedDictionary(name: "AcccountDisplayNameLabel Emoji Images") + + let group = DispatchGroup() + + for emoji in account.emojis { + guard matches.contains(where: { (match) in + let matchShortcode = (account.displayName as NSString).substring(with: match.range(at: 1)) + return emoji.shortcode == matchShortcode + }) else { + continue + } + + group.enter() + let request = ImageCache.emojis.get(emoji.url) { (data) in + defer { group.leave() } + guard let data = data, let image = UIImage(data: data) else { return } + + let size = CGSize(width: fontSize, height: fontSize) + let renderer = UIGraphicsImageRenderer(size: size) + let resized = renderer.image { (ctx) in + image.draw(in: CGRect(origin: .zero, size: size)) + } + + emojiImages[emoji.shortcode] = Image(uiImage: resized) + } + if let request = request { + emojiRequests.append(request) + } + } + + group.notify(queue: .main) { + var text: Text? + + var endIndex = account.displayName.utf16.count + + // iterate backwards as to not alter the indices of earlier matches + for match in matches.reversed() { + let shortcode = (account.displayName as NSString).substring(with: match.range(at: 1)) + guard let image = emojiImages[shortcode] else { continue } + + let afterCurrentMatch = (account.displayName as NSString).substring(with: NSRange(location: match.range.upperBound, length: endIndex - match.range.upperBound)) + + if let subsequent = text { + text = Text(image) + Text(verbatim: afterCurrentMatch) + subsequent + } else { + text = Text(image) + Text(verbatim: afterCurrentMatch) + } + + endIndex = match.range.lowerBound + } + + let beforeLastMatch = (account.displayName as NSString).substring(to: endIndex) + + if let text = text { + self.text = Text(verbatim: beforeLastMatch) + text + } else { + self.text = Text(verbatim: beforeLastMatch) + } + } + } +} + +//struct AccountDisplayNameLabel_Previews: PreviewProvider { +// static var previews: some View { +// AccountDisplayNameLabel() +// } +//}