From 8b2acf1ace4f469ba8128b1869f8d57121bc76b9 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 21 Feb 2021 23:10:34 -0800 Subject: [PATCH] Animated emoji --- ...NSMutableAttributedString+Extensions.swift | 15 ++- Extensions/String+Extensions.swift | 8 +- Localizations/Localizable.strings | 1 + Metatext.xcodeproj/project.pbxproj | 24 +++++ .../Utilities/AppPreferences.swift | 6 ++ Views/SwiftUI/PreferencesView.swift | 3 + Views/UIKit/AccountFieldView.swift | 13 ++- Views/UIKit/AccountHeaderView.swift | 16 +++- Views/UIKit/AnimatedAttachmentLabel.swift | 58 ++++++++++++ Views/UIKit/AnimatedTextAttachment.swift | 17 ++++ Views/UIKit/AnimatingLayoutManager.swift | 53 +++++++++++ Views/UIKit/Content Views/AccountView.swift | 8 +- .../Content Views/AutocompleteItemView.swift | 6 +- .../Content Views/ConversationView.swift | 5 +- Views/UIKit/Content Views/IdentityView.swift | 4 +- .../Content Views/NotificationView.swift | 20 ++-- Views/UIKit/Content Views/StatusView.swift | 62 +++++++------ Views/UIKit/EmojiInsertable.swift | 5 + Views/UIKit/PollOptionButton.swift | 92 ++++++++++++++----- Views/UIKit/PollResultView.swift | 13 ++- Views/UIKit/PollView.swift | 12 ++- .../UIKit/SecondaryNavigationTitleView.swift | 6 +- Views/UIKit/StatusBodyView.swift | 10 +- Views/UIKit/TouchFallthroughTextView.swift | 12 ++- 24 files changed, 373 insertions(+), 96 deletions(-) create mode 100644 Views/UIKit/AnimatedAttachmentLabel.swift create mode 100644 Views/UIKit/AnimatedTextAttachment.swift create mode 100644 Views/UIKit/AnimatingLayoutManager.swift create mode 100644 Views/UIKit/EmojiInsertable.swift diff --git a/Extensions/NSMutableAttributedString+Extensions.swift b/Extensions/NSMutableAttributedString+Extensions.swift index 07e0b5a..bac7e8f 100644 --- a/Extensions/NSMutableAttributedString+Extensions.swift +++ b/Extensions/NSMutableAttributedString+Extensions.swift @@ -3,17 +3,26 @@ import Kingfisher import Mastodon import UIKit +import ViewModels extension NSMutableAttributedString { - func insert(emojis: [Emoji], view: UIView) { + func insert(emojis: [Emoji], view: UIView & EmojiInsertable, identityContext: IdentityContext) { for emoji in emojis { let token = ":\(emoji.shortcode):" while let tokenRange = string.range(of: token) { - let attachment = NSTextAttachment() + let attachment = AnimatedTextAttachment() + let url: URL + + if !identityContext.appPreferences.shouldReduceMotion, + identityContext.appPreferences.animateCustomEmojis { + url = emoji.url + } else { + url = emoji.staticUrl + } attachment.accessibilityLabel = emoji.shortcode - attachment.kf.setImage(with: emoji.url, attributedView: view) + attachment.kf.setImage(with: url, attributedView: view) replaceCharacters(in: NSRange(tokenRange, in: string), with: NSAttributedString(attachment: attachment)) } } diff --git a/Extensions/String+Extensions.swift b/Extensions/String+Extensions.swift index d34fa89..ea9bd5f 100644 --- a/Extensions/String+Extensions.swift +++ b/Extensions/String+Extensions.swift @@ -2,6 +2,7 @@ import Mastodon import UIKit +import ViewModels extension String { static var separator: Self { @@ -36,7 +37,10 @@ extension String { return attributed } - func localizedBolding(displayName: String, emojis: [Emoji], label: UILabel) -> NSAttributedString { + func localizedBolding(displayName: String, + emojis: [Emoji], + label: AnimatedAttachmentLabel, + identityContext: IdentityContext) -> NSAttributedString { let mutableString = NSMutableAttributedString( string: String.localizedStringWithFormat( NSLocalizedString(self, comment: ""), @@ -51,7 +55,7 @@ extension String { mutableString.setAttributes([NSAttributedString.Key.font: boldFont], range: range) } - mutableString.insert(emojis: emojis, view: label) + mutableString.insert(emojis: emojis, view: label, identityContext: identityContext) mutableString.resizeAttachments(toLineHeight: label.font.lineHeight) return mutableString diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 95a12bd..9f852ed 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -178,6 +178,7 @@ "preferences.media.avatars.animate.everywhere" = "Everywhere"; "preferences.media.avatars.animate.profiles" = "In profiles"; "preferences.media.avatars.animate.never" = "Never"; +"preferences.media.custom-emojis.animate" = "Animate custom emoji"; "preferences.media.headers" = "Headers"; "preferences.media.headers.animate" = "Animate headers"; "preferences.media.autoplay" = "Autoplay"; diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 82cd36d..b0225e9 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -161,6 +161,14 @@ D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; }; D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; }; D0CEC0E125E0BB9700FEF5A6 /* NewItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC0E025E0BB9700FEF5A6 /* NewItemsView.swift */; }; + D0CEC0F725E3303200FEF5A6 /* AnimatingLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC0F625E3303200FEF5A6 /* AnimatingLayoutManager.swift */; }; + D0CEC10125E337C900FEF5A6 /* AnimatedTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC10025E337C900FEF5A6 /* AnimatedTextAttachment.swift */; }; + D0CEC10A25E3381500FEF5A6 /* AnimatedTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC10025E337C900FEF5A6 /* AnimatedTextAttachment.swift */; }; + D0CEC11025E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC10F25E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift */; }; + D0CEC11525E3464A00FEF5A6 /* AnimatedAttachmentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC10F25E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift */; }; + D0CEC11A25E34BFE00FEF5A6 /* AnimatingLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC0F625E3303200FEF5A6 /* AnimatingLayoutManager.swift */; }; + D0CEC12025E35FE100FEF5A6 /* EmojiInsertable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */; }; + D0CEC12525E35FE300FEF5A6 /* EmojiInsertable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */; }; D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */; }; D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4625BCD289003D5DF2 /* TagView.swift */; }; D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */; }; @@ -378,6 +386,10 @@ D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = ""; }; D0CEC0E025E0BB9700FEF5A6 /* NewItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewItemsView.swift; sourceTree = ""; }; + D0CEC0F625E3303200FEF5A6 /* AnimatingLayoutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatingLayoutManager.swift; sourceTree = ""; }; + D0CEC10025E337C900FEF5A6 /* AnimatedTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedTextAttachment.swift; sourceTree = ""; }; + D0CEC10F25E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedAttachmentLabel.swift; sourceTree = ""; }; + D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiInsertable.swift; sourceTree = ""; }; D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = ""; }; D0D2AC4625BCD289003D5DF2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTableViewCell.swift; sourceTree = ""; }; @@ -461,6 +473,9 @@ children = ( D0070251255921B100F38136 /* AccountFieldView.swift */, D01EF22325182B1F00650C6B /* AccountHeaderView.swift */, + D0CEC10F25E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift */, + D0CEC10025E337C900FEF5A6 /* AnimatedTextAttachment.swift */, + D0CEC0F625E3303200FEF5A6 /* AnimatingLayoutManager.swift */, D01F41E224F8889700D55A2D /* AttachmentsView.swift */, D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */, D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */, @@ -477,6 +492,7 @@ D007023D25562A2800F38136 /* ConversationAvatarsView.swift */, D05936DD25A937EC00754FDF /* EditThumbnailView.swift */, D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */, + D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */, D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */, D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */, D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */, @@ -1046,10 +1062,12 @@ D07F4D9825D493E300F61133 /* MuteView.swift in Sources */, D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */, D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */, + D0CEC10125E337C900FEF5A6 /* AnimatedTextAttachment.swift in Sources */, D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */, D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */, D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */, D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */, + D0CEC11025E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift in Sources */, D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */, D0D93EC025D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */, D09D970E25C64539007E6394 /* InstanceContentConfiguration.swift in Sources */, @@ -1071,6 +1089,7 @@ D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */, D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */, D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */, + D0CEC0F725E3303200FEF5A6 /* AnimatingLayoutManager.swift in Sources */, D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */, D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */, D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */, @@ -1095,6 +1114,7 @@ D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */, D08B8D72254246E200B1EBEF /* PollView.swift in Sources */, D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */, + D0CEC12025E35FE100FEF5A6 /* EmojiInsertable.swift in Sources */, D0EA59402522AC8700804347 /* CardView.swift in Sources */, D0F0B10E251A868200942152 /* AccountView.swift in Sources */, D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */, @@ -1171,6 +1191,7 @@ D0D93EDE25DA014700C622ED /* SeparatorConfiguredCollectionViewListCell.swift in Sources */, D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */, D00CB23825C93047008EF267 /* String+Extensions.swift in Sources */, + D0CEC12525E35FE300FEF5A6 /* EmojiInsertable.swift in Sources */, D0D93EC525D9C75E00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */, D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */, D021A67B25C3E32A008A0C0D /* PlayerView.swift in Sources */, @@ -1180,9 +1201,11 @@ D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */, D025B14E25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */, D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */, + D0CEC10A25E3381500FEF5A6 /* AnimatedTextAttachment.swift in Sources */, D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */, D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */, D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */, + D0CEC11A25E34BFE00FEF5A6 /* AnimatingLayoutManager.swift in Sources */, D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */, D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */, D0BE981725D242EB0057E161 /* UIImage+Extensions.swift in Sources */, @@ -1191,6 +1214,7 @@ D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */, D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */, D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */, + D0CEC11525E3464A00FEF5A6 /* AnimatedAttachmentLabel.swift in Sources */, D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */, D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */, D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */, diff --git a/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift b/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift index 1e1f1f8..b387687 100644 --- a/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift +++ b/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift @@ -79,6 +79,11 @@ public extension AppPreferences { set { self[.animateHeaders] = newValue } } + var animateCustomEmojis: Bool { + get { self[.animateCustomEmojis] ?? true } + set { self[.animateCustomEmojis] = newValue } + } + var autoplayGIFs: Autoplay { get { if let rawValue = self[.autoplayGIFs] as String?, @@ -193,6 +198,7 @@ private extension AppPreferences { case useSystemReduceMotionForMedia case animateAvatars case animateHeaders + case animateCustomEmojis case autoplayGIFs case autoplayVideos case homeTimelineBehavior diff --git a/Views/SwiftUI/PreferencesView.swift b/Views/SwiftUI/PreferencesView.swift index 7963d7d..f3f1304 100644 --- a/Views/SwiftUI/PreferencesView.swift +++ b/Views/SwiftUI/PreferencesView.swift @@ -114,6 +114,9 @@ struct PreferencesView: View { Toggle("preferences.media.headers.animate", isOn: reduceMotion ? .constant(false) : $identityContext.appPreferences.animateHeaders) .disabled(reduceMotion) + Toggle("preferences.media.custom-emojis.animate", + isOn: reduceMotion ? .constant(false) : $identityContext.appPreferences.animateCustomEmojis) + .disabled(reduceMotion) } .disabled(reduceMotion) if viewModel.identityContext.identity.authenticated diff --git a/Views/UIKit/AccountFieldView.swift b/Views/UIKit/AccountFieldView.swift index 286b18b..7966b80 100644 --- a/Views/UIKit/AccountFieldView.swift +++ b/Views/UIKit/AccountFieldView.swift @@ -3,16 +3,21 @@ import Combine import Mastodon import UIKit +import ViewModels final class AccountFieldView: UIView { - let nameLabel = UILabel() + let nameLabel = AnimatedAttachmentLabel() let valueTextView = TouchFallthroughTextView() let checkButton = UIButton() private var valueTextViewTrailingConstraint: NSLayoutConstraint? private var cancellables = Set() // swiftlint:disable:next function_body_length - init(name: String, value: NSAttributedString, verifiedAt: Date?, emojis: [Emoji]) { + init(name: String, + value: NSAttributedString, + verifiedAt: Date?, + emojis: [Emoji], + identityContext: IdentityContext) { super.init(frame: .zero) NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) @@ -44,7 +49,7 @@ final class AccountFieldView: UIView { let mutableName = NSMutableAttributedString(string: name) - mutableName.insert(emojis: emojis, view: nameLabel) + mutableName.insert(emojis: emojis, view: nameLabel, identityContext: identityContext) mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight) nameLabel.attributedText = mutableName @@ -73,7 +78,7 @@ final class AccountFieldView: UIView { [.font: valueFont as Any, .foregroundColor: UIColor.label], range: valueRange) - mutableValue.insert(emojis: emojis, view: valueTextView) + mutableValue.insert(emojis: emojis, view: valueTextView, identityContext: identityContext) mutableValue.resizeAttachments(toLineHeight: valueFont.lineHeight) valueTextView.attributedText = mutableValue diff --git a/Views/UIKit/AccountHeaderView.swift b/Views/UIKit/AccountHeaderView.swift index 8714619..40e3897 100644 --- a/Views/UIKit/AccountHeaderView.swift +++ b/Views/UIKit/AccountHeaderView.swift @@ -15,7 +15,7 @@ final class AccountHeaderView: UIView { let relationshipButtonsStackView = UIStackView() let followButton = UIButton(type: .system) let unfollowButton = UIButton(type: .system) - let displayNameLabel = UILabel() + let displayNameLabel = AnimatedAttachmentLabel() let accountStackView = UIStackView() let accountLabel = UILabel() let lockedImageView = UIImageView() @@ -81,7 +81,9 @@ final class AccountHeaderView: UIView { } else { let mutableDisplayName = NSMutableAttributedString(string: accountViewModel.displayName) - mutableDisplayName.insert(emojis: accountViewModel.emojis, view: displayNameLabel) + mutableDisplayName.insert(emojis: accountViewModel.emojis, + view: displayNameLabel, + identityContext: viewModel.identityContext) mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) displayNameLabel.attributedText = mutableDisplayName } @@ -130,7 +132,8 @@ final class AccountHeaderView: UIView { string: identityProof.providerUsername, attributes: [.link: identityProof.profileUrl]), verifiedAt: identityProof.updatedAt, - emojis: []) + emojis: [], + identityContext: viewModel.identityContext) fieldView.valueTextView.delegate = self @@ -142,7 +145,8 @@ final class AccountHeaderView: UIView { name: field.name, value: field.value.attributed, verifiedAt: field.verifiedAt, - emojis: accountViewModel.emojis) + emojis: accountViewModel.emojis, + identityContext: viewModel.identityContext) fieldView.valueTextView.delegate = self @@ -159,7 +163,9 @@ final class AccountHeaderView: UIView { [.font: noteFont as Any, .foregroundColor: UIColor.label], range: noteRange) - mutableNote.insert(emojis: accountViewModel.emojis, view: noteTextView) + mutableNote.insert(emojis: accountViewModel.emojis, + view: noteTextView, + identityContext: viewModel.identityContext) mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight) noteTextView.attributedText = mutableNote noteTextView.isHidden = false diff --git a/Views/UIKit/AnimatedAttachmentLabel.swift b/Views/UIKit/AnimatedAttachmentLabel.swift new file mode 100644 index 0000000..2d89561 --- /dev/null +++ b/Views/UIKit/AnimatedAttachmentLabel.swift @@ -0,0 +1,58 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Kingfisher +import UIKit + +final class AnimatedAttachmentLabel: UILabel, EmojiInsertable { + override init(frame: CGRect) { + super.init(frame: frame) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func drawText(in rect: CGRect) { + super.drawText(in: rect) + + guard let attributedText = attributedText else { return } + + var attachmentImageViews = Set() + + attributedText.enumerateAttribute( + .attachment, + in: NSRange(location: 0, length: attributedText.length)) { attachment, _, _ in + guard let attachmentImageView = (attachment as? AnimatedTextAttachment)?.imageView else { return } + + attachmentImageViews.insert(attachmentImageView) + } + + for subview in subviews { + guard let attachmentImageView = subview as? AnimatedImageView else { continue } + + if !attachmentImageViews.contains(attachmentImageView) { + attachmentImageView.removeFromSuperview() + } + } + + attributedText.enumerateAttribute( + .attachment, + in: NSRange(location: 0, length: attributedText.length), + options: .longestEffectiveRangeNotRequired) { attachment, _, _ in + guard let animatedAttachment = attachment as? AnimatedTextAttachment, + let imageBounds = animatedAttachment.imageBounds + else { return } + + animatedAttachment.imageView.frame = imageBounds + + animatedAttachment.imageView.image = animatedAttachment.image + animatedAttachment.imageView.contentMode = .scaleAspectFit + animatedAttachment.imageView.center.y = center.y + + if animatedAttachment.imageView.superview != self { + addSubview(animatedAttachment.imageView) + } + } + } +} diff --git a/Views/UIKit/AnimatedTextAttachment.swift b/Views/UIKit/AnimatedTextAttachment.swift new file mode 100644 index 0000000..7ed5a1a --- /dev/null +++ b/Views/UIKit/AnimatedTextAttachment.swift @@ -0,0 +1,17 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Kingfisher +import UIKit + +final class AnimatedTextAttachment: NSTextAttachment { + var imageView = AnimatedImageView() + var imageBounds: CGRect? + + override func image(forBounds imageBounds: CGRect, + textContainer: NSTextContainer?, + characterIndex charIndex: Int) -> UIImage? { + self.imageBounds = imageBounds + + return nil // rendered by AnimatingLayoutManager or AnimatedAttachmentLabel + } +} diff --git a/Views/UIKit/AnimatingLayoutManager.swift b/Views/UIKit/AnimatingLayoutManager.swift new file mode 100644 index 0000000..356d7cc --- /dev/null +++ b/Views/UIKit/AnimatingLayoutManager.swift @@ -0,0 +1,53 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Kingfisher +import UIKit + +final class AnimatingLayoutManager: NSLayoutManager { + weak var view: UIView? + + override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { + guard let textStorage = textStorage else { + super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) + + return + } + + var attachmentImageViews = Set() + + textStorage.enumerateAttribute( + .attachment, + in: NSRange(location: 0, length: textStorage.length)) { attachment, _, _ in + guard let attachmentImageView = (attachment as? AnimatedTextAttachment)?.imageView else { return } + + attachmentImageViews.insert(attachmentImageView) + } + + for subview in view?.subviews ?? [] { + guard let attachmentImageView = subview as? AnimatedImageView else { continue } + + if !attachmentImageViews.contains(attachmentImageView) { + attachmentImageView.removeFromSuperview() + } + } + + textStorage.enumerateAttribute( + .attachment, + in: glyphsToShow, + options: .longestEffectiveRangeNotRequired) { attachment, range, _ in + guard let animatedAttachment = attachment as? AnimatedTextAttachment, + let textContainer = textContainer(forGlyphAt: range.location, effectiveRange: nil) + else { return } + + animatedAttachment.imageView.frame = boundingRect(forGlyphRange: range, in: textContainer) + animatedAttachment.imageView.image = animatedAttachment.image + animatedAttachment.imageView.contentMode = .scaleAspectFit + + if animatedAttachment.imageView.superview != view { + view?.addSubview(animatedAttachment.imageView) + } + } + + super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) + } +} diff --git a/Views/UIKit/Content Views/AccountView.swift b/Views/UIKit/Content Views/AccountView.swift index 401e0ce..23eb94f 100644 --- a/Views/UIKit/Content Views/AccountView.swift +++ b/Views/UIKit/Content Views/AccountView.swift @@ -7,7 +7,7 @@ import ViewModels final class AccountView: UIView { let avatarImageView = AnimatedImageView() - let displayNameLabel = UILabel() + let displayNameLabel = AnimatedAttachmentLabel() let accountLabel = UILabel() let noteTextView = TouchFallthroughTextView() let acceptFollowRequestButton = UIButton() @@ -203,7 +203,9 @@ private extension AccountView { let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName) - mutableDisplayName.insert(emojis: viewModel.emojis, view: displayNameLabel) + mutableDisplayName.insert(emojis: viewModel.emojis, + view: displayNameLabel, + identityContext: viewModel.identityContext) mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) displayNameLabel.attributedText = mutableDisplayName @@ -221,7 +223,7 @@ private extension AccountView { [.font: noteFont as Any, .foregroundColor: UIColor.label], range: noteRange) - mutableNote.insert(emojis: viewModel.emojis, view: noteTextView) + mutableNote.insert(emojis: viewModel.emojis, view: noteTextView, identityContext: viewModel.identityContext) mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight) noteTextView.attributedText = mutableNote diff --git a/Views/UIKit/Content Views/AutocompleteItemView.swift b/Views/UIKit/Content Views/AutocompleteItemView.swift index 31f5523..faa951e 100644 --- a/Views/UIKit/Content Views/AutocompleteItemView.swift +++ b/Views/UIKit/Content Views/AutocompleteItemView.swift @@ -5,7 +5,7 @@ import UIKit final class AutocompleteItemView: UIView { private let imageView = AnimatedImageView() - private let primaryLabel = UILabel() + private let primaryLabel = AnimatedAttachmentLabel() private let secondaryLabel = UILabel() private let stackView = UIStackView() private var autocompleteItemConfiguration: AutocompleteItemContentConfiguration @@ -83,7 +83,9 @@ private extension AutocompleteItemView { let mutableDisplayName = NSMutableAttributedString(string: account.displayName) - mutableDisplayName.insert(emojis: account.emojis, view: primaryLabel) + mutableDisplayName.insert(emojis: account.emojis, + view: primaryLabel, + identityContext: autocompleteItemConfiguration.identityContext) mutableDisplayName.resizeAttachments(toLineHeight: primaryLabel.font.lineHeight) primaryLabel.attributedText = mutableDisplayName primaryLabel.isHidden = account.displayName.isEmpty diff --git a/Views/UIKit/Content Views/ConversationView.swift b/Views/UIKit/Content Views/ConversationView.swift index f311fb4..d518430 100644 --- a/Views/UIKit/Content Views/ConversationView.swift +++ b/Views/UIKit/Content Views/ConversationView.swift @@ -6,7 +6,7 @@ import ViewModels final class ConversationView: UIView { let avatarsView = ConversationAvatarsView() - let displayNamesLabel = UILabel() + let displayNamesLabel = AnimatedAttachmentLabel() let unreadIndicator = UIImageView(image: UIImage( systemName: "circlebadge.fill", withConfiguration: UIImage.SymbolConfiguration(scale: .small))) @@ -130,7 +130,8 @@ private extension ConversationView { mutableDisplayNames.insert( emojis: viewModel.accountViewModels.map(\.emojis).reduce([], +), - view: displayNamesLabel) + view: displayNamesLabel, + identityContext: viewModel.identityContext) mutableDisplayNames.resizeAttachments(toLineHeight: displayNamesLabel.font.lineHeight) unreadIndicator.isHidden = !viewModel.isUnread diff --git a/Views/UIKit/Content Views/IdentityView.swift b/Views/UIKit/Content Views/IdentityView.swift index 08eaecd..068293b 100644 --- a/Views/UIKit/Content Views/IdentityView.swift +++ b/Views/UIKit/Content Views/IdentityView.swift @@ -5,7 +5,7 @@ import UIKit final class IdentityView: UIView { let imageView = AnimatedImageView() - let nameLabel = UILabel() + let nameLabel = AnimatedAttachmentLabel() let secondaryLabel = UILabel() private var identityConfiguration: IdentityContentConfiguration @@ -92,7 +92,7 @@ private extension IdentityView { let mutableName = NSMutableAttributedString(string: displayName) if let emojis = viewModel.identity.account?.emojis { - mutableName.insert(emojis: emojis, view: nameLabel) + mutableName.insert(emojis: emojis, view: nameLabel, identityContext: viewModel.identityContext) mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight) } diff --git a/Views/UIKit/Content Views/NotificationView.swift b/Views/UIKit/Content Views/NotificationView.swift index 7c8e1db..b04456b 100644 --- a/Views/UIKit/Content Views/NotificationView.swift +++ b/Views/UIKit/Content Views/NotificationView.swift @@ -9,9 +9,9 @@ final class NotificationView: UIView { private let iconImageView = UIImageView() private let avatarImageView = AnimatedImageView() private let avatarButton = UIButton() - private let typeLabel = UILabel() + private let typeLabel = AnimatedAttachmentLabel() private let timeLabel = UILabel() - private let displayNameLabel = UILabel() + private let displayNameLabel = AnimatedAttachmentLabel() private let accountLabel = UILabel() private let statusBodyView = StatusBodyView() private var notificationConfiguration: NotificationContentConfiguration @@ -173,19 +173,22 @@ private extension NotificationView { typeLabel.attributedText = "notifications.followed-you".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, - label: typeLabel) + label: typeLabel, + identityContext: viewModel.identityContext) iconImageView.tintColor = nil case .reblog: typeLabel.attributedText = "notifications.reblogged-your-status".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, - label: typeLabel) + label: typeLabel, + identityContext: viewModel.identityContext) iconImageView.tintColor = .systemGreen case .favourite: typeLabel.attributedText = "notifications.favourited-your-status".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, - label: typeLabel) + label: typeLabel, + identityContext: viewModel.identityContext) iconImageView.tintColor = .systemYellow case .poll: typeLabel.text = NSLocalizedString( @@ -198,14 +201,17 @@ private extension NotificationView { typeLabel.attributedText = "notifications.unknown".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, - label: typeLabel) + label: typeLabel, + identityContext: viewModel.identityContext) iconImageView.tintColor = nil } if viewModel.statusViewModel == nil { let mutableDisplayName = NSMutableAttributedString(string: viewModel.accountViewModel.displayName) - mutableDisplayName.insert(emojis: viewModel.accountViewModel.emojis, view: displayNameLabel) + mutableDisplayName.insert(emojis: viewModel.accountViewModel.emojis, + view: displayNameLabel, + identityContext: viewModel.identityContext) mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) displayNameLabel.attributedText = mutableDisplayName accountLabel.text = viewModel.accountViewModel.accountName diff --git a/Views/UIKit/Content Views/StatusView.swift b/Views/UIKit/Content Views/StatusView.swift index 5adfe24..bf67e78 100644 --- a/Views/UIKit/Content Views/StatusView.swift +++ b/Views/UIKit/Content Views/StatusView.swift @@ -11,9 +11,9 @@ final class StatusView: UIView { let avatarImageView = AnimatedImageView() let avatarButton = UIButton() let infoIcon = UIImageView() - let infoLabel = UILabel() + let infoLabel = AnimatedAttachmentLabel() let rebloggerButton = UIButton() - let displayNameLabel = UILabel() + let displayNameLabel = AnimatedAttachmentLabel() let accountLabel = UILabel() let nameButton = UIButton() let timeLabel = UILabel() @@ -165,19 +165,28 @@ private extension StatusView { infoLabel.font = .preferredFont(forTextStyle: .caption1) infoLabel.textColor = .secondaryLabel infoLabel.adjustsFontForContentSizeCategory = true + infoLabel.isUserInteractionEnabled = true infoLabel.setContentHuggingPriority(.required, for: .vertical) mainStackView.addArrangedSubview(infoLabel) - rebloggerButton.setTitleColor(.secondaryLabel, for: .normal) - rebloggerButton.titleLabel?.font = .preferredFont(forTextStyle: .caption1) - rebloggerButton.titleLabel?.adjustsFontForContentSizeCategory = true - rebloggerButton.contentHorizontalAlignment = .leading - rebloggerButton.setContentHuggingPriority(.required, for: .vertical) - mainStackView.addArrangedSubview(rebloggerButton) + infoLabel.addSubview(rebloggerButton) + rebloggerButton.translatesAutoresizingMaskIntoConstraints = false rebloggerButton.addAction( UIAction { [weak self] _ in self?.statusConfiguration.viewModel.rebloggerAccountSelected() }, for: .touchUpInside) + let rebloggerTouchStartAction = UIAction { [weak self] _ in self?.infoLabel.alpha = 0.75 } + + rebloggerButton.addAction(rebloggerTouchStartAction, for: .touchDown) + rebloggerButton.addAction(rebloggerTouchStartAction, for: .touchDragEnter) + + let rebloggerTouchEnd = UIAction { [weak self] _ in self?.infoLabel.alpha = 1 } + + rebloggerButton.addAction(rebloggerTouchEnd, for: .touchDragExit) + rebloggerButton.addAction(rebloggerTouchEnd, for: .touchUpInside) + rebloggerButton.addAction(rebloggerTouchEnd, for: .touchUpOutside) + rebloggerButton.addAction(rebloggerTouchEnd, for: .touchCancel) + displayNameLabel.font = .preferredFont(forTextStyle: .headline) displayNameLabel.adjustsFontForContentSizeCategory = true displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) @@ -379,13 +388,18 @@ private extension StatusView { avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor), avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), + infoIcon.centerYAnchor.constraint(equalTo: infoLabel.centerYAnchor), nameButton.leadingAnchor.constraint(equalTo: displayNameLabel.leadingAnchor), nameButton.topAnchor.constraint(equalTo: displayNameLabel.topAnchor), nameButton.trailingAnchor.constraint(equalTo: accountLabel.trailingAnchor), nameButton.bottomAnchor.constraint(equalTo: accountLabel.bottomAnchor), contextParentTimeApplicationStackView.heightAnchor.constraint( greaterThanOrEqualToConstant: .minimumButtonDimension / 2), - interactionsStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension) + interactionsStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension), + rebloggerButton.leadingAnchor.constraint(equalTo: infoLabel.leadingAnchor), + rebloggerButton.topAnchor.constraint(equalTo: infoLabel.topAnchor), + rebloggerButton.trailingAnchor.constraint(equalTo: infoLabel.trailingAnchor), + rebloggerButton.bottomAnchor.constraint(equalTo: infoLabel.bottomAnchor) ]) NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) @@ -429,29 +443,24 @@ private extension StatusView { inReplyToView.isHidden = !viewModel.configuration.isReplyInContext hasReplyFollowingView.isHidden = !viewModel.configuration.hasReplyFollowing - if viewModel.isReblog, let titleLabel = rebloggerButton.titleLabel { + if viewModel.isReblog { let attributedTitle = "status.reblogged-by".localizedBolding( displayName: viewModel.rebloggedByDisplayName, emojis: viewModel.rebloggedByDisplayNameEmojis, - label: titleLabel) + label: infoLabel, + identityContext: viewModel.identityContext) let highlightedAttributedTitle = NSMutableAttributedString(attributedString: attributedTitle) highlightedAttributedTitle.addAttribute( .foregroundColor, value: UIColor.tertiaryLabel, range: .init(location: 0, length: highlightedAttributedTitle.length)) - rebloggerButton.setAttributedTitle( - attributedTitle, - for: .normal) - rebloggerButton.setAttributedTitle( - highlightedAttributedTitle, - for: .highlighted) - infoIcon.centerYAnchor.constraint(equalTo: rebloggerButton.centerYAnchor).isActive = true + infoLabel.attributedText = attributedTitle infoIcon.image = UIImage( systemName: "arrow.2.squarepath", withConfiguration: UIImage.SymbolConfiguration(scale: .small)) - infoLabel.isHidden = true + infoLabel.isHidden = false infoIcon.isHidden = false rebloggerButton.isHidden = false } else if viewModel.configuration.isPinned { @@ -482,7 +491,9 @@ private extension StatusView { rebloggerButton.isHidden = true } - mutableDisplayName.insert(emojis: viewModel.accountViewModel.emojis, view: displayNameLabel) + mutableDisplayName.insert(emojis: viewModel.accountViewModel.emojis, + view: displayNameLabel, + identityContext: viewModel.identityContext) mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) displayNameLabel.attributedText = mutableDisplayName accountLabel.text = viewModel.accountName @@ -577,16 +588,15 @@ private extension StatusView { let accessibilityAttributedLabel = NSMutableAttributedString(string: "") - if !rebloggerButton.isHidden, - let rebloggerAttributedText = rebloggerButton.attributedTitle(for: .normal) { - accessibilityAttributedLabel.appendWithSeparator(rebloggerAttributedText) - } - if !infoLabel.isHidden, let infoText = infoLabel.attributedText { accessibilityAttributedLabel.appendWithSeparator(infoText) } - accessibilityAttributedLabel.append(mutableDisplayName) + if accessibilityAttributedLabel.string.isEmpty { + accessibilityAttributedLabel.append(mutableDisplayName) + } else { + accessibilityAttributedLabel.appendWithSeparator(mutableDisplayName) + } if let bodyAccessibilityAttributedLabel = bodyView.accessibilityAttributedLabel { accessibilityAttributedLabel.appendWithSeparator(bodyAccessibilityAttributedLabel) diff --git a/Views/UIKit/EmojiInsertable.swift b/Views/UIKit/EmojiInsertable.swift new file mode 100644 index 0000000..2411ec6 --- /dev/null +++ b/Views/UIKit/EmojiInsertable.swift @@ -0,0 +1,5 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation + +protocol EmojiInsertable {} diff --git a/Views/UIKit/PollOptionButton.swift b/Views/UIKit/PollOptionButton.swift index 8d663b6..8b14dbc 100644 --- a/Views/UIKit/PollOptionButton.swift +++ b/Views/UIKit/PollOptionButton.swift @@ -2,40 +2,82 @@ import Mastodon import UIKit +import ViewModels + +final class PollOptionButton: UIView { + let button = UIButton() + + public var isSelected = false { + didSet { + imageView.image = isSelected ? selectedImage : image + button.isSelected = isSelected + } + } + + private let label = AnimatedAttachmentLabel() + private let imageView = UIImageView() + private let image: UIImage? + private let selectedImage: UIImage? + + // swiftlint:disable:next function_body_length + init(title: String, emojis: [Emoji], multipleSelection: Bool, identityContext: IdentityContext) { + image = UIImage( + systemName: multipleSelection ? "square" : "circle", + withConfiguration: UIImage.SymbolConfiguration(scale: .medium)) + selectedImage = UIImage( + systemName: multipleSelection ? "checkmark.square" : "checkmark.circle", + withConfiguration: UIImage.SymbolConfiguration(scale: .medium)) -final class PollOptionButton: UIButton { - init(title: String, emojis: [Emoji], multipleSelection: Bool) { super.init(frame: .zero) - titleLabel?.font = .preferredFont(forTextStyle: .callout) - titleLabel?.adjustsFontForContentSizeCategory = true - titleLabel?.numberOfLines = 0 - titleLabel?.lineBreakMode = .byWordWrapping - contentHorizontalAlignment = .leading + let stackView = UIStackView() + + addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.spacing = .defaultSpacing + + stackView.addArrangedSubview(imageView) + imageView.contentMode = .scaleAspectFit + imageView.setContentHuggingPriority(.required, for: .horizontal) + + stackView.addArrangedSubview(label) + label.font = .preferredFont(forTextStyle: .callout) + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 let attributedTitle = NSMutableAttributedString(string: title) - attributedTitle.insert(emojis: emojis, view: titleLabel!) - attributedTitle.resizeAttachments(toLineHeight: titleLabel!.font.lineHeight) - setAttributedTitle(attributedTitle, for: .normal) - setImage( - UIImage( - systemName: multipleSelection ? "square" : "circle", - withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), - for: .normal) - setImage( - UIImage( - systemName: multipleSelection ? "checkmark.square" : "checkmark.circle", - withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), - for: .selected) + attributedTitle.insert(emojis: emojis, view: label, identityContext: identityContext) + attributedTitle.resizeAttachments(toLineHeight: label.font.lineHeight) - setContentCompressionResistancePriority(.required, for: .vertical) + label.attributedText = attributedTitle - imageView?.translatesAutoresizingMaskIntoConstraints = false - imageView?.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true - imageView?.contentMode = .scaleAspectFit + addSubview(button) + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityAttributedLabel = attributedTitle - heightAnchor.constraint(equalTo: titleLabel!.heightAnchor).isActive = true + let touchStartAction = UIAction { [weak self] _ in self?.alpha = 0.75 } + + button.addAction(touchStartAction, for: .touchDown) + button.addAction(touchStartAction, for: .touchDragEnter) + + let touchEndAction = UIAction { [weak self] _ in self?.alpha = 1 } + + button.addAction(touchEndAction, for: .touchDragExit) + button.addAction(touchEndAction, for: .touchUpInside) + button.addAction(touchEndAction, for: .touchUpOutside) + button.addAction(touchEndAction, for: .touchCancel) + + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor), + button.leadingAnchor.constraint(equalTo: leadingAnchor), + button.topAnchor.constraint(equalTo: topAnchor), + button.trailingAnchor.constraint(equalTo: trailingAnchor), + button.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) } @available(*, unavailable) diff --git a/Views/UIKit/PollResultView.swift b/Views/UIKit/PollResultView.swift index 5c3c2e1..152c02b 100644 --- a/Views/UIKit/PollResultView.swift +++ b/Views/UIKit/PollResultView.swift @@ -2,15 +2,21 @@ import Mastodon import UIKit +import ViewModels final class PollResultView: UIView { - let titleLabel = UILabel() + let titleLabel = AnimatedAttachmentLabel() let percentLabel = UILabel() private let verticalStackView = UIStackView() private let horizontalStackView = UIStackView() private let percentView = UIProgressView() - init(option: Poll.Option, emojis: [Emoji], selected: Bool, multipleSelection: Bool, votersCount: Int) { + init(option: Poll.Option, + emojis: [Emoji], + selected: Bool, + multipleSelection: Bool, + votersCount: Int, + identityContext: IdentityContext) { super.init(frame: .zero) addSubview(verticalStackView) @@ -29,6 +35,7 @@ final class PollResultView: UIView { systemName: multipleSelection ? "checkmark.square" : "checkmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .medium))) + imageView.contentMode = .scaleAspectFit imageView.setContentHuggingPriority(.required, for: .horizontal) horizontalStackView.addArrangedSubview(imageView) } @@ -45,7 +52,7 @@ final class PollResultView: UIView { let attributedTitle = NSMutableAttributedString(string: option.title) - attributedTitle.insert(emojis: emojis, view: titleLabel) + attributedTitle.insert(emojis: emojis, view: titleLabel, identityContext: identityContext) attributedTitle.resizeAttachments(toLineHeight: titleLabel.font.lineHeight) titleLabel.attributedText = attributedTitle diff --git a/Views/UIKit/PollView.swift b/Views/UIKit/PollView.swift index 7bd509e..058cfff 100644 --- a/Views/UIKit/PollView.swift +++ b/Views/UIKit/PollView.swift @@ -37,9 +37,10 @@ final class PollView: UIView { let button = PollOptionButton( title: option.title, emojis: viewModel.pollEmojis, - multipleSelection: viewModel.isPollMultipleSelection) + multipleSelection: viewModel.isPollMultipleSelection, + identityContext: viewModel.identityContext) - button.addAction( + button.button.addAction( UIAction { _ in if viewModel.pollOptionSelections.contains(index) { viewModel.pollOptionSelections.remove(index) @@ -60,7 +61,8 @@ final class PollView: UIView { emojis: viewModel.pollEmojis, selected: viewModel.pollOwnVotes.contains(index), multipleSelection: viewModel.isPollMultipleSelection, - votersCount: viewModel.pollVotersCount) + votersCount: viewModel.pollVotersCount, + identityContext: viewModel.identityContext) stackView.addArrangedSubview(resultView) } @@ -74,7 +76,7 @@ final class PollView: UIView { index + 1) if let optionView = view as? PollOptionButton, - let attributedTitle = optionView.attributedTitle(for: .normal) { + let attributedTitle = optionView.button.accessibilityAttributedLabel { title = attributedTitle let optionAccessibilityAttributedLabel = NSMutableAttributedString(string: indexLabel) @@ -108,7 +110,7 @@ final class PollView: UIView { guard let self = self else { return } for (index, view) in self.stackView.arrangedSubviews.enumerated() { - (view as? UIButton)?.isSelected = $0.contains(index) + (view as? PollOptionButton)?.isSelected = $0.contains(index) } self.voteButton.isEnabled = !$0.isEmpty diff --git a/Views/UIKit/SecondaryNavigationTitleView.swift b/Views/UIKit/SecondaryNavigationTitleView.swift index 511d843..df7a7b9 100644 --- a/Views/UIKit/SecondaryNavigationTitleView.swift +++ b/Views/UIKit/SecondaryNavigationTitleView.swift @@ -7,7 +7,7 @@ import ViewModels final class SecondaryNavigationTitleView: UIView { private let viewModel: NavigationViewModel private let avatarImageView = AnimatedImageView() - private let displayNameLabel = UILabel() + private let displayNameLabel = AnimatedAttachmentLabel() private let accountLabel = UILabel() private let stackView = UIStackView() @@ -77,7 +77,9 @@ private extension SecondaryNavigationTitleView { let mutableDisplayName = NSMutableAttributedString(string: displayName) if let emojis = viewModel.identityContext.identity.account?.emojis { - mutableDisplayName.insert(emojis: emojis, view: displayNameLabel) + mutableDisplayName.insert(emojis: emojis, + view: displayNameLabel, + identityContext: viewModel.identityContext) mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) } diff --git a/Views/UIKit/StatusBodyView.swift b/Views/UIKit/StatusBodyView.swift index 9fca2db..bc34dd3 100644 --- a/Views/UIKit/StatusBodyView.swift +++ b/Views/UIKit/StatusBodyView.swift @@ -5,7 +5,7 @@ import UIKit import ViewModels final class StatusBodyView: UIView { - let spoilerTextLabel = UILabel() + let spoilerTextLabel = AnimatedAttachmentLabel() let toggleShowContentButton = CapsuleButton() let contentTextView = TouchFallthroughTextView() let attachmentsView = AttachmentsView() @@ -28,12 +28,16 @@ final class StatusBodyView: UIView { mutableContent.addAttributes( [.font: contentFont, .foregroundColor: UIColor.label], range: contentRange) - mutableContent.insert(emojis: viewModel.contentEmojis, view: contentTextView) + mutableContent.insert(emojis: viewModel.contentEmojis, + view: contentTextView, + identityContext: viewModel.identityContext) mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight) contentTextView.attributedText = mutableContent contentTextView.isHidden = contentTextView.text.isEmpty - mutableSpoilerText.insert(emojis: viewModel.contentEmojis, view: spoilerTextLabel) + mutableSpoilerText.insert(emojis: viewModel.contentEmojis, + view: spoilerTextLabel, + identityContext: viewModel.identityContext) mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight) spoilerTextLabel.font = contentFont spoilerTextLabel.attributedText = mutableSpoilerText diff --git a/Views/UIKit/TouchFallthroughTextView.swift b/Views/UIKit/TouchFallthroughTextView.swift index 07b2f63..a25a4d7 100644 --- a/Views/UIKit/TouchFallthroughTextView.swift +++ b/Views/UIKit/TouchFallthroughTextView.swift @@ -2,14 +2,22 @@ import UIKit -final class TouchFallthroughTextView: UITextView { +final class TouchFallthroughTextView: UITextView, EmojiInsertable { var shouldFallthrough: Bool = true private var linkHighlightView: UIView? override init(frame: CGRect, textContainer: NSTextContainer?) { - super.init(frame: frame, textContainer: textContainer) + let textStorage = NSTextStorage() + let layoutManager = AnimatingLayoutManager() + let presentTextContainer = textContainer ?? NSTextContainer(size: .zero) + layoutManager.addTextContainer(presentTextContainer) + textStorage.addLayoutManager(layoutManager) + + super.init(frame: frame, textContainer: presentTextContainer) + + layoutManager.view = self clipsToBounds = false textDragInteraction?.isEnabled = false isEditable = false