mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-21 15:50:59 +00:00
Animated emoji
This commit is contained in:
parent
924e7614bd
commit
8b2acf1ace
24 changed files with 373 additions and 96 deletions
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = "<group>"; };
|
||||
D0CEC0E025E0BB9700FEF5A6 /* NewItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewItemsView.swift; sourceTree = "<group>"; };
|
||||
D0CEC0F625E3303200FEF5A6 /* AnimatingLayoutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatingLayoutManager.swift; sourceTree = "<group>"; };
|
||||
D0CEC10025E337C900FEF5A6 /* AnimatedTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedTextAttachment.swift; sourceTree = "<group>"; };
|
||||
D0CEC10F25E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedAttachmentLabel.swift; sourceTree = "<group>"; };
|
||||
D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiInsertable.swift; sourceTree = "<group>"; };
|
||||
D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0D2AC4625BCD289003D5DF2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||
D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
// 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
|
||||
|
|
|
@ -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
|
||||
|
|
58
Views/UIKit/AnimatedAttachmentLabel.swift
Normal file
58
Views/UIKit/AnimatedAttachmentLabel.swift
Normal file
|
@ -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<AnimatedImageView>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
Views/UIKit/AnimatedTextAttachment.swift
Normal file
17
Views/UIKit/AnimatedTextAttachment.swift
Normal file
|
@ -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
|
||||
}
|
||||
}
|
53
Views/UIKit/AnimatingLayoutManager.swift
Normal file
53
Views/UIKit/AnimatingLayoutManager.swift
Normal file
|
@ -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<AnimatedImageView>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
if accessibilityAttributedLabel.string.isEmpty {
|
||||
accessibilityAttributedLabel.append(mutableDisplayName)
|
||||
} else {
|
||||
accessibilityAttributedLabel.appendWithSeparator(mutableDisplayName)
|
||||
}
|
||||
|
||||
if let bodyAccessibilityAttributedLabel = bodyView.accessibilityAttributedLabel {
|
||||
accessibilityAttributedLabel.appendWithSeparator(bodyAccessibilityAttributedLabel)
|
||||
|
|
5
Views/UIKit/EmojiInsertable.swift
Normal file
5
Views/UIKit/EmojiInsertable.swift
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol EmojiInsertable {}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue