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 Kingfisher
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
extension NSMutableAttributedString {
|
extension NSMutableAttributedString {
|
||||||
func insert(emojis: [Emoji], view: UIView) {
|
func insert(emojis: [Emoji], view: UIView & EmojiInsertable, identityContext: IdentityContext) {
|
||||||
for emoji in emojis {
|
for emoji in emojis {
|
||||||
let token = ":\(emoji.shortcode):"
|
let token = ":\(emoji.shortcode):"
|
||||||
|
|
||||||
while let tokenRange = string.range(of: token) {
|
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.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))
|
replaceCharacters(in: NSRange(tokenRange, in: string), with: NSAttributedString(attachment: attachment))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
static var separator: Self {
|
static var separator: Self {
|
||||||
|
@ -36,7 +37,10 @@ extension String {
|
||||||
return attributed
|
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(
|
let mutableString = NSMutableAttributedString(
|
||||||
string: String.localizedStringWithFormat(
|
string: String.localizedStringWithFormat(
|
||||||
NSLocalizedString(self, comment: ""),
|
NSLocalizedString(self, comment: ""),
|
||||||
|
@ -51,7 +55,7 @@ extension String {
|
||||||
mutableString.setAttributes([NSAttributedString.Key.font: boldFont], range: range)
|
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)
|
mutableString.resizeAttachments(toLineHeight: label.font.lineHeight)
|
||||||
|
|
||||||
return mutableString
|
return mutableString
|
||||||
|
|
|
@ -178,6 +178,7 @@
|
||||||
"preferences.media.avatars.animate.everywhere" = "Everywhere";
|
"preferences.media.avatars.animate.everywhere" = "Everywhere";
|
||||||
"preferences.media.avatars.animate.profiles" = "In profiles";
|
"preferences.media.avatars.animate.profiles" = "In profiles";
|
||||||
"preferences.media.avatars.animate.never" = "Never";
|
"preferences.media.avatars.animate.never" = "Never";
|
||||||
|
"preferences.media.custom-emojis.animate" = "Animate custom emoji";
|
||||||
"preferences.media.headers" = "Headers";
|
"preferences.media.headers" = "Headers";
|
||||||
"preferences.media.headers.animate" = "Animate headers";
|
"preferences.media.headers.animate" = "Animate headers";
|
||||||
"preferences.media.autoplay" = "Autoplay";
|
"preferences.media.autoplay" = "Autoplay";
|
||||||
|
|
|
@ -161,6 +161,14 @@
|
||||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
||||||
D0CE9F88258B076900E3A6B6 /* 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 */; };
|
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 */; };
|
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */; };
|
||||||
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4625BCD289003D5DF2 /* TagView.swift */; };
|
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4625BCD289003D5DF2 /* TagView.swift */; };
|
||||||
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -461,6 +473,9 @@
|
||||||
children = (
|
children = (
|
||||||
D0070251255921B100F38136 /* AccountFieldView.swift */,
|
D0070251255921B100F38136 /* AccountFieldView.swift */,
|
||||||
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */,
|
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */,
|
||||||
|
D0CEC10F25E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift */,
|
||||||
|
D0CEC10025E337C900FEF5A6 /* AnimatedTextAttachment.swift */,
|
||||||
|
D0CEC0F625E3303200FEF5A6 /* AnimatingLayoutManager.swift */,
|
||||||
D01F41E224F8889700D55A2D /* AttachmentsView.swift */,
|
D01F41E224F8889700D55A2D /* AttachmentsView.swift */,
|
||||||
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
|
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
|
||||||
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */,
|
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */,
|
||||||
|
@ -477,6 +492,7 @@
|
||||||
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
||||||
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
||||||
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */,
|
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */,
|
||||||
|
D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */,
|
||||||
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */,
|
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */,
|
||||||
D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */,
|
D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */,
|
||||||
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
||||||
|
@ -1046,10 +1062,12 @@
|
||||||
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */,
|
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */,
|
||||||
D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */,
|
D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */,
|
||||||
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */,
|
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */,
|
||||||
|
D0CEC10125E337C900FEF5A6 /* AnimatedTextAttachment.swift in Sources */,
|
||||||
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */,
|
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */,
|
||||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||||
|
D0CEC11025E3462B00FEF5A6 /* AnimatedAttachmentLabel.swift in Sources */,
|
||||||
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */,
|
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */,
|
||||||
D0D93EC025D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */,
|
D0D93EC025D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */,
|
||||||
D09D970E25C64539007E6394 /* InstanceContentConfiguration.swift in Sources */,
|
D09D970E25C64539007E6394 /* InstanceContentConfiguration.swift in Sources */,
|
||||||
|
@ -1071,6 +1089,7 @@
|
||||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||||
|
D0CEC0F725E3303200FEF5A6 /* AnimatingLayoutManager.swift in Sources */,
|
||||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
||||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||||
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
||||||
|
@ -1095,6 +1114,7 @@
|
||||||
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
|
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
|
||||||
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
||||||
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */,
|
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */,
|
||||||
|
D0CEC12025E35FE100FEF5A6 /* EmojiInsertable.swift in Sources */,
|
||||||
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
||||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||||
|
@ -1171,6 +1191,7 @@
|
||||||
D0D93EDE25DA014700C622ED /* SeparatorConfiguredCollectionViewListCell.swift in Sources */,
|
D0D93EDE25DA014700C622ED /* SeparatorConfiguredCollectionViewListCell.swift in Sources */,
|
||||||
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
||||||
D00CB23825C93047008EF267 /* String+Extensions.swift in Sources */,
|
D00CB23825C93047008EF267 /* String+Extensions.swift in Sources */,
|
||||||
|
D0CEC12525E35FE300FEF5A6 /* EmojiInsertable.swift in Sources */,
|
||||||
D0D93EC525D9C75E00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */,
|
D0D93EC525D9C75E00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */,
|
||||||
D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
||||||
D021A67B25C3E32A008A0C0D /* PlayerView.swift in Sources */,
|
D021A67B25C3E32A008A0C0D /* PlayerView.swift in Sources */,
|
||||||
|
@ -1180,9 +1201,11 @@
|
||||||
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
||||||
D025B14E25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */,
|
D025B14E25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */,
|
||||||
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
|
D0CEC10A25E3381500FEF5A6 /* AnimatedTextAttachment.swift in Sources */,
|
||||||
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||||
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
||||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||||
|
D0CEC11A25E34BFE00FEF5A6 /* AnimatingLayoutManager.swift in Sources */,
|
||||||
D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
||||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
||||||
D0BE981725D242EB0057E161 /* UIImage+Extensions.swift in Sources */,
|
D0BE981725D242EB0057E161 /* UIImage+Extensions.swift in Sources */,
|
||||||
|
@ -1191,6 +1214,7 @@
|
||||||
D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||||
D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
||||||
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */,
|
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */,
|
||||||
|
D0CEC11525E3464A00FEF5A6 /* AnimatedAttachmentLabel.swift in Sources */,
|
||||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
||||||
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
||||||
D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */,
|
D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */,
|
||||||
|
|
|
@ -79,6 +79,11 @@ public extension AppPreferences {
|
||||||
set { self[.animateHeaders] = newValue }
|
set { self[.animateHeaders] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var animateCustomEmojis: Bool {
|
||||||
|
get { self[.animateCustomEmojis] ?? true }
|
||||||
|
set { self[.animateCustomEmojis] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
var autoplayGIFs: Autoplay {
|
var autoplayGIFs: Autoplay {
|
||||||
get {
|
get {
|
||||||
if let rawValue = self[.autoplayGIFs] as String?,
|
if let rawValue = self[.autoplayGIFs] as String?,
|
||||||
|
@ -193,6 +198,7 @@ private extension AppPreferences {
|
||||||
case useSystemReduceMotionForMedia
|
case useSystemReduceMotionForMedia
|
||||||
case animateAvatars
|
case animateAvatars
|
||||||
case animateHeaders
|
case animateHeaders
|
||||||
|
case animateCustomEmojis
|
||||||
case autoplayGIFs
|
case autoplayGIFs
|
||||||
case autoplayVideos
|
case autoplayVideos
|
||||||
case homeTimelineBehavior
|
case homeTimelineBehavior
|
||||||
|
|
|
@ -114,6 +114,9 @@ struct PreferencesView: View {
|
||||||
Toggle("preferences.media.headers.animate",
|
Toggle("preferences.media.headers.animate",
|
||||||
isOn: reduceMotion ? .constant(false) : $identityContext.appPreferences.animateHeaders)
|
isOn: reduceMotion ? .constant(false) : $identityContext.appPreferences.animateHeaders)
|
||||||
.disabled(reduceMotion)
|
.disabled(reduceMotion)
|
||||||
|
Toggle("preferences.media.custom-emojis.animate",
|
||||||
|
isOn: reduceMotion ? .constant(false) : $identityContext.appPreferences.animateCustomEmojis)
|
||||||
|
.disabled(reduceMotion)
|
||||||
}
|
}
|
||||||
.disabled(reduceMotion)
|
.disabled(reduceMotion)
|
||||||
if viewModel.identityContext.identity.authenticated
|
if viewModel.identityContext.identity.authenticated
|
||||||
|
|
|
@ -3,16 +3,21 @@
|
||||||
import Combine
|
import Combine
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
final class AccountFieldView: UIView {
|
final class AccountFieldView: UIView {
|
||||||
let nameLabel = UILabel()
|
let nameLabel = AnimatedAttachmentLabel()
|
||||||
let valueTextView = TouchFallthroughTextView()
|
let valueTextView = TouchFallthroughTextView()
|
||||||
let checkButton = UIButton()
|
let checkButton = UIButton()
|
||||||
private var valueTextViewTrailingConstraint: NSLayoutConstraint?
|
private var valueTextViewTrailingConstraint: NSLayoutConstraint?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
// 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)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
|
NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
|
||||||
|
@ -44,7 +49,7 @@ final class AccountFieldView: UIView {
|
||||||
|
|
||||||
let mutableName = NSMutableAttributedString(string: name)
|
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)
|
mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight)
|
||||||
nameLabel.attributedText = mutableName
|
nameLabel.attributedText = mutableName
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@ final class AccountFieldView: UIView {
|
||||||
[.font: valueFont as Any,
|
[.font: valueFont as Any,
|
||||||
.foregroundColor: UIColor.label],
|
.foregroundColor: UIColor.label],
|
||||||
range: valueRange)
|
range: valueRange)
|
||||||
mutableValue.insert(emojis: emojis, view: valueTextView)
|
mutableValue.insert(emojis: emojis, view: valueTextView, identityContext: identityContext)
|
||||||
mutableValue.resizeAttachments(toLineHeight: valueFont.lineHeight)
|
mutableValue.resizeAttachments(toLineHeight: valueFont.lineHeight)
|
||||||
|
|
||||||
valueTextView.attributedText = mutableValue
|
valueTextView.attributedText = mutableValue
|
||||||
|
|
|
@ -15,7 +15,7 @@ final class AccountHeaderView: UIView {
|
||||||
let relationshipButtonsStackView = UIStackView()
|
let relationshipButtonsStackView = UIStackView()
|
||||||
let followButton = UIButton(type: .system)
|
let followButton = UIButton(type: .system)
|
||||||
let unfollowButton = UIButton(type: .system)
|
let unfollowButton = UIButton(type: .system)
|
||||||
let displayNameLabel = UILabel()
|
let displayNameLabel = AnimatedAttachmentLabel()
|
||||||
let accountStackView = UIStackView()
|
let accountStackView = UIStackView()
|
||||||
let accountLabel = UILabel()
|
let accountLabel = UILabel()
|
||||||
let lockedImageView = UIImageView()
|
let lockedImageView = UIImageView()
|
||||||
|
@ -81,7 +81,9 @@ final class AccountHeaderView: UIView {
|
||||||
} else {
|
} else {
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: accountViewModel.displayName)
|
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)
|
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||||
displayNameLabel.attributedText = mutableDisplayName
|
displayNameLabel.attributedText = mutableDisplayName
|
||||||
}
|
}
|
||||||
|
@ -130,7 +132,8 @@ final class AccountHeaderView: UIView {
|
||||||
string: identityProof.providerUsername,
|
string: identityProof.providerUsername,
|
||||||
attributes: [.link: identityProof.profileUrl]),
|
attributes: [.link: identityProof.profileUrl]),
|
||||||
verifiedAt: identityProof.updatedAt,
|
verifiedAt: identityProof.updatedAt,
|
||||||
emojis: [])
|
emojis: [],
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
|
|
||||||
fieldView.valueTextView.delegate = self
|
fieldView.valueTextView.delegate = self
|
||||||
|
|
||||||
|
@ -142,7 +145,8 @@ final class AccountHeaderView: UIView {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
value: field.value.attributed,
|
value: field.value.attributed,
|
||||||
verifiedAt: field.verifiedAt,
|
verifiedAt: field.verifiedAt,
|
||||||
emojis: accountViewModel.emojis)
|
emojis: accountViewModel.emojis,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
|
|
||||||
fieldView.valueTextView.delegate = self
|
fieldView.valueTextView.delegate = self
|
||||||
|
|
||||||
|
@ -159,7 +163,9 @@ final class AccountHeaderView: UIView {
|
||||||
[.font: noteFont as Any,
|
[.font: noteFont as Any,
|
||||||
.foregroundColor: UIColor.label],
|
.foregroundColor: UIColor.label],
|
||||||
range: noteRange)
|
range: noteRange)
|
||||||
mutableNote.insert(emojis: accountViewModel.emojis, view: noteTextView)
|
mutableNote.insert(emojis: accountViewModel.emojis,
|
||||||
|
view: noteTextView,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight)
|
mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight)
|
||||||
noteTextView.attributedText = mutableNote
|
noteTextView.attributedText = mutableNote
|
||||||
noteTextView.isHidden = false
|
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 {
|
final class AccountView: UIView {
|
||||||
let avatarImageView = AnimatedImageView()
|
let avatarImageView = AnimatedImageView()
|
||||||
let displayNameLabel = UILabel()
|
let displayNameLabel = AnimatedAttachmentLabel()
|
||||||
let accountLabel = UILabel()
|
let accountLabel = UILabel()
|
||||||
let noteTextView = TouchFallthroughTextView()
|
let noteTextView = TouchFallthroughTextView()
|
||||||
let acceptFollowRequestButton = UIButton()
|
let acceptFollowRequestButton = UIButton()
|
||||||
|
@ -203,7 +203,9 @@ private extension AccountView {
|
||||||
|
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
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)
|
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||||
displayNameLabel.attributedText = mutableDisplayName
|
displayNameLabel.attributedText = mutableDisplayName
|
||||||
|
|
||||||
|
@ -221,7 +223,7 @@ private extension AccountView {
|
||||||
[.font: noteFont as Any,
|
[.font: noteFont as Any,
|
||||||
.foregroundColor: UIColor.label],
|
.foregroundColor: UIColor.label],
|
||||||
range: noteRange)
|
range: noteRange)
|
||||||
mutableNote.insert(emojis: viewModel.emojis, view: noteTextView)
|
mutableNote.insert(emojis: viewModel.emojis, view: noteTextView, identityContext: viewModel.identityContext)
|
||||||
mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight)
|
mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight)
|
||||||
|
|
||||||
noteTextView.attributedText = mutableNote
|
noteTextView.attributedText = mutableNote
|
||||||
|
|
|
@ -5,7 +5,7 @@ import UIKit
|
||||||
|
|
||||||
final class AutocompleteItemView: UIView {
|
final class AutocompleteItemView: UIView {
|
||||||
private let imageView = AnimatedImageView()
|
private let imageView = AnimatedImageView()
|
||||||
private let primaryLabel = UILabel()
|
private let primaryLabel = AnimatedAttachmentLabel()
|
||||||
private let secondaryLabel = UILabel()
|
private let secondaryLabel = UILabel()
|
||||||
private let stackView = UIStackView()
|
private let stackView = UIStackView()
|
||||||
private var autocompleteItemConfiguration: AutocompleteItemContentConfiguration
|
private var autocompleteItemConfiguration: AutocompleteItemContentConfiguration
|
||||||
|
@ -83,7 +83,9 @@ private extension AutocompleteItemView {
|
||||||
|
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: account.displayName)
|
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)
|
mutableDisplayName.resizeAttachments(toLineHeight: primaryLabel.font.lineHeight)
|
||||||
primaryLabel.attributedText = mutableDisplayName
|
primaryLabel.attributedText = mutableDisplayName
|
||||||
primaryLabel.isHidden = account.displayName.isEmpty
|
primaryLabel.isHidden = account.displayName.isEmpty
|
||||||
|
|
|
@ -6,7 +6,7 @@ import ViewModels
|
||||||
|
|
||||||
final class ConversationView: UIView {
|
final class ConversationView: UIView {
|
||||||
let avatarsView = ConversationAvatarsView()
|
let avatarsView = ConversationAvatarsView()
|
||||||
let displayNamesLabel = UILabel()
|
let displayNamesLabel = AnimatedAttachmentLabel()
|
||||||
let unreadIndicator = UIImageView(image: UIImage(
|
let unreadIndicator = UIImageView(image: UIImage(
|
||||||
systemName: "circlebadge.fill",
|
systemName: "circlebadge.fill",
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small)))
|
withConfiguration: UIImage.SymbolConfiguration(scale: .small)))
|
||||||
|
@ -130,7 +130,8 @@ private extension ConversationView {
|
||||||
|
|
||||||
mutableDisplayNames.insert(
|
mutableDisplayNames.insert(
|
||||||
emojis: viewModel.accountViewModels.map(\.emojis).reduce([], +),
|
emojis: viewModel.accountViewModels.map(\.emojis).reduce([], +),
|
||||||
view: displayNamesLabel)
|
view: displayNamesLabel,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
mutableDisplayNames.resizeAttachments(toLineHeight: displayNamesLabel.font.lineHeight)
|
mutableDisplayNames.resizeAttachments(toLineHeight: displayNamesLabel.font.lineHeight)
|
||||||
|
|
||||||
unreadIndicator.isHidden = !viewModel.isUnread
|
unreadIndicator.isHidden = !viewModel.isUnread
|
||||||
|
|
|
@ -5,7 +5,7 @@ import UIKit
|
||||||
|
|
||||||
final class IdentityView: UIView {
|
final class IdentityView: UIView {
|
||||||
let imageView = AnimatedImageView()
|
let imageView = AnimatedImageView()
|
||||||
let nameLabel = UILabel()
|
let nameLabel = AnimatedAttachmentLabel()
|
||||||
let secondaryLabel = UILabel()
|
let secondaryLabel = UILabel()
|
||||||
|
|
||||||
private var identityConfiguration: IdentityContentConfiguration
|
private var identityConfiguration: IdentityContentConfiguration
|
||||||
|
@ -92,7 +92,7 @@ private extension IdentityView {
|
||||||
let mutableName = NSMutableAttributedString(string: displayName)
|
let mutableName = NSMutableAttributedString(string: displayName)
|
||||||
|
|
||||||
if let emojis = viewModel.identity.account?.emojis {
|
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)
|
mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,9 @@ final class NotificationView: UIView {
|
||||||
private let iconImageView = UIImageView()
|
private let iconImageView = UIImageView()
|
||||||
private let avatarImageView = AnimatedImageView()
|
private let avatarImageView = AnimatedImageView()
|
||||||
private let avatarButton = UIButton()
|
private let avatarButton = UIButton()
|
||||||
private let typeLabel = UILabel()
|
private let typeLabel = AnimatedAttachmentLabel()
|
||||||
private let timeLabel = UILabel()
|
private let timeLabel = UILabel()
|
||||||
private let displayNameLabel = UILabel()
|
private let displayNameLabel = AnimatedAttachmentLabel()
|
||||||
private let accountLabel = UILabel()
|
private let accountLabel = UILabel()
|
||||||
private let statusBodyView = StatusBodyView()
|
private let statusBodyView = StatusBodyView()
|
||||||
private var notificationConfiguration: NotificationContentConfiguration
|
private var notificationConfiguration: NotificationContentConfiguration
|
||||||
|
@ -173,19 +173,22 @@ private extension NotificationView {
|
||||||
typeLabel.attributedText = "notifications.followed-you".localizedBolding(
|
typeLabel.attributedText = "notifications.followed-you".localizedBolding(
|
||||||
displayName: viewModel.accountViewModel.displayName,
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
emojis: viewModel.accountViewModel.emojis,
|
emojis: viewModel.accountViewModel.emojis,
|
||||||
label: typeLabel)
|
label: typeLabel,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
iconImageView.tintColor = nil
|
iconImageView.tintColor = nil
|
||||||
case .reblog:
|
case .reblog:
|
||||||
typeLabel.attributedText = "notifications.reblogged-your-status".localizedBolding(
|
typeLabel.attributedText = "notifications.reblogged-your-status".localizedBolding(
|
||||||
displayName: viewModel.accountViewModel.displayName,
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
emojis: viewModel.accountViewModel.emojis,
|
emojis: viewModel.accountViewModel.emojis,
|
||||||
label: typeLabel)
|
label: typeLabel,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
iconImageView.tintColor = .systemGreen
|
iconImageView.tintColor = .systemGreen
|
||||||
case .favourite:
|
case .favourite:
|
||||||
typeLabel.attributedText = "notifications.favourited-your-status".localizedBolding(
|
typeLabel.attributedText = "notifications.favourited-your-status".localizedBolding(
|
||||||
displayName: viewModel.accountViewModel.displayName,
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
emojis: viewModel.accountViewModel.emojis,
|
emojis: viewModel.accountViewModel.emojis,
|
||||||
label: typeLabel)
|
label: typeLabel,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
iconImageView.tintColor = .systemYellow
|
iconImageView.tintColor = .systemYellow
|
||||||
case .poll:
|
case .poll:
|
||||||
typeLabel.text = NSLocalizedString(
|
typeLabel.text = NSLocalizedString(
|
||||||
|
@ -198,14 +201,17 @@ private extension NotificationView {
|
||||||
typeLabel.attributedText = "notifications.unknown".localizedBolding(
|
typeLabel.attributedText = "notifications.unknown".localizedBolding(
|
||||||
displayName: viewModel.accountViewModel.displayName,
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
emojis: viewModel.accountViewModel.emojis,
|
emojis: viewModel.accountViewModel.emojis,
|
||||||
label: typeLabel)
|
label: typeLabel,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
iconImageView.tintColor = nil
|
iconImageView.tintColor = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.statusViewModel == nil {
|
if viewModel.statusViewModel == nil {
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.accountViewModel.displayName)
|
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)
|
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||||
displayNameLabel.attributedText = mutableDisplayName
|
displayNameLabel.attributedText = mutableDisplayName
|
||||||
accountLabel.text = viewModel.accountViewModel.accountName
|
accountLabel.text = viewModel.accountViewModel.accountName
|
||||||
|
|
|
@ -11,9 +11,9 @@ final class StatusView: UIView {
|
||||||
let avatarImageView = AnimatedImageView()
|
let avatarImageView = AnimatedImageView()
|
||||||
let avatarButton = UIButton()
|
let avatarButton = UIButton()
|
||||||
let infoIcon = UIImageView()
|
let infoIcon = UIImageView()
|
||||||
let infoLabel = UILabel()
|
let infoLabel = AnimatedAttachmentLabel()
|
||||||
let rebloggerButton = UIButton()
|
let rebloggerButton = UIButton()
|
||||||
let displayNameLabel = UILabel()
|
let displayNameLabel = AnimatedAttachmentLabel()
|
||||||
let accountLabel = UILabel()
|
let accountLabel = UILabel()
|
||||||
let nameButton = UIButton()
|
let nameButton = UIButton()
|
||||||
let timeLabel = UILabel()
|
let timeLabel = UILabel()
|
||||||
|
@ -165,19 +165,28 @@ private extension StatusView {
|
||||||
infoLabel.font = .preferredFont(forTextStyle: .caption1)
|
infoLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
infoLabel.textColor = .secondaryLabel
|
infoLabel.textColor = .secondaryLabel
|
||||||
infoLabel.adjustsFontForContentSizeCategory = true
|
infoLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
infoLabel.isUserInteractionEnabled = true
|
||||||
infoLabel.setContentHuggingPriority(.required, for: .vertical)
|
infoLabel.setContentHuggingPriority(.required, for: .vertical)
|
||||||
mainStackView.addArrangedSubview(infoLabel)
|
mainStackView.addArrangedSubview(infoLabel)
|
||||||
|
|
||||||
rebloggerButton.setTitleColor(.secondaryLabel, for: .normal)
|
infoLabel.addSubview(rebloggerButton)
|
||||||
rebloggerButton.titleLabel?.font = .preferredFont(forTextStyle: .caption1)
|
rebloggerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
rebloggerButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
|
||||||
rebloggerButton.contentHorizontalAlignment = .leading
|
|
||||||
rebloggerButton.setContentHuggingPriority(.required, for: .vertical)
|
|
||||||
mainStackView.addArrangedSubview(rebloggerButton)
|
|
||||||
rebloggerButton.addAction(
|
rebloggerButton.addAction(
|
||||||
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.rebloggerAccountSelected() },
|
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.rebloggerAccountSelected() },
|
||||||
for: .touchUpInside)
|
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.font = .preferredFont(forTextStyle: .headline)
|
||||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||||
displayNameLabel.setContentHuggingPriority(.required, for: .horizontal)
|
displayNameLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
@ -379,13 +388,18 @@ private extension StatusView {
|
||||||
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
||||||
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
||||||
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
||||||
|
infoIcon.centerYAnchor.constraint(equalTo: infoLabel.centerYAnchor),
|
||||||
nameButton.leadingAnchor.constraint(equalTo: displayNameLabel.leadingAnchor),
|
nameButton.leadingAnchor.constraint(equalTo: displayNameLabel.leadingAnchor),
|
||||||
nameButton.topAnchor.constraint(equalTo: displayNameLabel.topAnchor),
|
nameButton.topAnchor.constraint(equalTo: displayNameLabel.topAnchor),
|
||||||
nameButton.trailingAnchor.constraint(equalTo: accountLabel.trailingAnchor),
|
nameButton.trailingAnchor.constraint(equalTo: accountLabel.trailingAnchor),
|
||||||
nameButton.bottomAnchor.constraint(equalTo: accountLabel.bottomAnchor),
|
nameButton.bottomAnchor.constraint(equalTo: accountLabel.bottomAnchor),
|
||||||
contextParentTimeApplicationStackView.heightAnchor.constraint(
|
contextParentTimeApplicationStackView.heightAnchor.constraint(
|
||||||
greaterThanOrEqualToConstant: .minimumButtonDimension / 2),
|
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)
|
NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
|
||||||
|
@ -429,29 +443,24 @@ private extension StatusView {
|
||||||
inReplyToView.isHidden = !viewModel.configuration.isReplyInContext
|
inReplyToView.isHidden = !viewModel.configuration.isReplyInContext
|
||||||
hasReplyFollowingView.isHidden = !viewModel.configuration.hasReplyFollowing
|
hasReplyFollowingView.isHidden = !viewModel.configuration.hasReplyFollowing
|
||||||
|
|
||||||
if viewModel.isReblog, let titleLabel = rebloggerButton.titleLabel {
|
if viewModel.isReblog {
|
||||||
let attributedTitle = "status.reblogged-by".localizedBolding(
|
let attributedTitle = "status.reblogged-by".localizedBolding(
|
||||||
displayName: viewModel.rebloggedByDisplayName,
|
displayName: viewModel.rebloggedByDisplayName,
|
||||||
emojis: viewModel.rebloggedByDisplayNameEmojis,
|
emojis: viewModel.rebloggedByDisplayNameEmojis,
|
||||||
label: titleLabel)
|
label: infoLabel,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
let highlightedAttributedTitle = NSMutableAttributedString(attributedString: attributedTitle)
|
let highlightedAttributedTitle = NSMutableAttributedString(attributedString: attributedTitle)
|
||||||
|
|
||||||
highlightedAttributedTitle.addAttribute(
|
highlightedAttributedTitle.addAttribute(
|
||||||
.foregroundColor,
|
.foregroundColor,
|
||||||
value: UIColor.tertiaryLabel,
|
value: UIColor.tertiaryLabel,
|
||||||
range: .init(location: 0, length: highlightedAttributedTitle.length))
|
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(
|
infoIcon.image = UIImage(
|
||||||
systemName: "arrow.2.squarepath",
|
systemName: "arrow.2.squarepath",
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
||||||
infoLabel.isHidden = true
|
infoLabel.isHidden = false
|
||||||
infoIcon.isHidden = false
|
infoIcon.isHidden = false
|
||||||
rebloggerButton.isHidden = false
|
rebloggerButton.isHidden = false
|
||||||
} else if viewModel.configuration.isPinned {
|
} else if viewModel.configuration.isPinned {
|
||||||
|
@ -482,7 +491,9 @@ private extension StatusView {
|
||||||
rebloggerButton.isHidden = true
|
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)
|
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||||
displayNameLabel.attributedText = mutableDisplayName
|
displayNameLabel.attributedText = mutableDisplayName
|
||||||
accountLabel.text = viewModel.accountName
|
accountLabel.text = viewModel.accountName
|
||||||
|
@ -577,16 +588,15 @@ private extension StatusView {
|
||||||
|
|
||||||
let accessibilityAttributedLabel = NSMutableAttributedString(string: "")
|
let accessibilityAttributedLabel = NSMutableAttributedString(string: "")
|
||||||
|
|
||||||
if !rebloggerButton.isHidden,
|
|
||||||
let rebloggerAttributedText = rebloggerButton.attributedTitle(for: .normal) {
|
|
||||||
accessibilityAttributedLabel.appendWithSeparator(rebloggerAttributedText)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !infoLabel.isHidden, let infoText = infoLabel.attributedText {
|
if !infoLabel.isHidden, let infoText = infoLabel.attributedText {
|
||||||
accessibilityAttributedLabel.appendWithSeparator(infoText)
|
accessibilityAttributedLabel.appendWithSeparator(infoText)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessibilityAttributedLabel.append(mutableDisplayName)
|
if accessibilityAttributedLabel.string.isEmpty {
|
||||||
|
accessibilityAttributedLabel.append(mutableDisplayName)
|
||||||
|
} else {
|
||||||
|
accessibilityAttributedLabel.appendWithSeparator(mutableDisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
if let bodyAccessibilityAttributedLabel = bodyView.accessibilityAttributedLabel {
|
if let bodyAccessibilityAttributedLabel = bodyView.accessibilityAttributedLabel {
|
||||||
accessibilityAttributedLabel.appendWithSeparator(bodyAccessibilityAttributedLabel)
|
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 Mastodon
|
||||||
import UIKit
|
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)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
titleLabel?.font = .preferredFont(forTextStyle: .callout)
|
let stackView = UIStackView()
|
||||||
titleLabel?.adjustsFontForContentSizeCategory = true
|
|
||||||
titleLabel?.numberOfLines = 0
|
addSubview(stackView)
|
||||||
titleLabel?.lineBreakMode = .byWordWrapping
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentHorizontalAlignment = .leading
|
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)
|
let attributedTitle = NSMutableAttributedString(string: title)
|
||||||
|
|
||||||
attributedTitle.insert(emojis: emojis, view: titleLabel!)
|
attributedTitle.insert(emojis: emojis, view: label, identityContext: identityContext)
|
||||||
attributedTitle.resizeAttachments(toLineHeight: titleLabel!.font.lineHeight)
|
attributedTitle.resizeAttachments(toLineHeight: label.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)
|
|
||||||
|
|
||||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
label.attributedText = attributedTitle
|
||||||
|
|
||||||
imageView?.translatesAutoresizingMaskIntoConstraints = false
|
addSubview(button)
|
||||||
imageView?.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView?.contentMode = .scaleAspectFit
|
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)
|
@available(*, unavailable)
|
||||||
|
|
|
@ -2,15 +2,21 @@
|
||||||
|
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
final class PollResultView: UIView {
|
final class PollResultView: UIView {
|
||||||
let titleLabel = UILabel()
|
let titleLabel = AnimatedAttachmentLabel()
|
||||||
let percentLabel = UILabel()
|
let percentLabel = UILabel()
|
||||||
private let verticalStackView = UIStackView()
|
private let verticalStackView = UIStackView()
|
||||||
private let horizontalStackView = UIStackView()
|
private let horizontalStackView = UIStackView()
|
||||||
private let percentView = UIProgressView()
|
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)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
addSubview(verticalStackView)
|
addSubview(verticalStackView)
|
||||||
|
@ -29,6 +35,7 @@ final class PollResultView: UIView {
|
||||||
systemName: multipleSelection ? "checkmark.square" : "checkmark.circle",
|
systemName: multipleSelection ? "checkmark.square" : "checkmark.circle",
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)))
|
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)))
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
imageView.setContentHuggingPriority(.required, for: .horizontal)
|
imageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
horizontalStackView.addArrangedSubview(imageView)
|
horizontalStackView.addArrangedSubview(imageView)
|
||||||
}
|
}
|
||||||
|
@ -45,7 +52,7 @@ final class PollResultView: UIView {
|
||||||
|
|
||||||
let attributedTitle = NSMutableAttributedString(string: option.title)
|
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)
|
attributedTitle.resizeAttachments(toLineHeight: titleLabel.font.lineHeight)
|
||||||
titleLabel.attributedText = attributedTitle
|
titleLabel.attributedText = attributedTitle
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,10 @@ final class PollView: UIView {
|
||||||
let button = PollOptionButton(
|
let button = PollOptionButton(
|
||||||
title: option.title,
|
title: option.title,
|
||||||
emojis: viewModel.pollEmojis,
|
emojis: viewModel.pollEmojis,
|
||||||
multipleSelection: viewModel.isPollMultipleSelection)
|
multipleSelection: viewModel.isPollMultipleSelection,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
|
|
||||||
button.addAction(
|
button.button.addAction(
|
||||||
UIAction { _ in
|
UIAction { _ in
|
||||||
if viewModel.pollOptionSelections.contains(index) {
|
if viewModel.pollOptionSelections.contains(index) {
|
||||||
viewModel.pollOptionSelections.remove(index)
|
viewModel.pollOptionSelections.remove(index)
|
||||||
|
@ -60,7 +61,8 @@ final class PollView: UIView {
|
||||||
emojis: viewModel.pollEmojis,
|
emojis: viewModel.pollEmojis,
|
||||||
selected: viewModel.pollOwnVotes.contains(index),
|
selected: viewModel.pollOwnVotes.contains(index),
|
||||||
multipleSelection: viewModel.isPollMultipleSelection,
|
multipleSelection: viewModel.isPollMultipleSelection,
|
||||||
votersCount: viewModel.pollVotersCount)
|
votersCount: viewModel.pollVotersCount,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
|
|
||||||
stackView.addArrangedSubview(resultView)
|
stackView.addArrangedSubview(resultView)
|
||||||
}
|
}
|
||||||
|
@ -74,7 +76,7 @@ final class PollView: UIView {
|
||||||
index + 1)
|
index + 1)
|
||||||
|
|
||||||
if let optionView = view as? PollOptionButton,
|
if let optionView = view as? PollOptionButton,
|
||||||
let attributedTitle = optionView.attributedTitle(for: .normal) {
|
let attributedTitle = optionView.button.accessibilityAttributedLabel {
|
||||||
title = attributedTitle
|
title = attributedTitle
|
||||||
|
|
||||||
let optionAccessibilityAttributedLabel = NSMutableAttributedString(string: indexLabel)
|
let optionAccessibilityAttributedLabel = NSMutableAttributedString(string: indexLabel)
|
||||||
|
@ -108,7 +110,7 @@ final class PollView: UIView {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
for (index, view) in self.stackView.arrangedSubviews.enumerated() {
|
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
|
self.voteButton.isEnabled = !$0.isEmpty
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ViewModels
|
||||||
final class SecondaryNavigationTitleView: UIView {
|
final class SecondaryNavigationTitleView: UIView {
|
||||||
private let viewModel: NavigationViewModel
|
private let viewModel: NavigationViewModel
|
||||||
private let avatarImageView = AnimatedImageView()
|
private let avatarImageView = AnimatedImageView()
|
||||||
private let displayNameLabel = UILabel()
|
private let displayNameLabel = AnimatedAttachmentLabel()
|
||||||
private let accountLabel = UILabel()
|
private let accountLabel = UILabel()
|
||||||
private let stackView = UIStackView()
|
private let stackView = UIStackView()
|
||||||
|
|
||||||
|
@ -77,7 +77,9 @@ private extension SecondaryNavigationTitleView {
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: displayName)
|
let mutableDisplayName = NSMutableAttributedString(string: displayName)
|
||||||
|
|
||||||
if let emojis = viewModel.identityContext.identity.account?.emojis {
|
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)
|
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
final class StatusBodyView: UIView {
|
final class StatusBodyView: UIView {
|
||||||
let spoilerTextLabel = UILabel()
|
let spoilerTextLabel = AnimatedAttachmentLabel()
|
||||||
let toggleShowContentButton = CapsuleButton()
|
let toggleShowContentButton = CapsuleButton()
|
||||||
let contentTextView = TouchFallthroughTextView()
|
let contentTextView = TouchFallthroughTextView()
|
||||||
let attachmentsView = AttachmentsView()
|
let attachmentsView = AttachmentsView()
|
||||||
|
@ -28,12 +28,16 @@ final class StatusBodyView: UIView {
|
||||||
mutableContent.addAttributes(
|
mutableContent.addAttributes(
|
||||||
[.font: contentFont, .foregroundColor: UIColor.label],
|
[.font: contentFont, .foregroundColor: UIColor.label],
|
||||||
range: contentRange)
|
range: contentRange)
|
||||||
mutableContent.insert(emojis: viewModel.contentEmojis, view: contentTextView)
|
mutableContent.insert(emojis: viewModel.contentEmojis,
|
||||||
|
view: contentTextView,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
|
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
|
||||||
contentTextView.attributedText = mutableContent
|
contentTextView.attributedText = mutableContent
|
||||||
contentTextView.isHidden = contentTextView.text.isEmpty
|
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)
|
mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight)
|
||||||
spoilerTextLabel.font = contentFont
|
spoilerTextLabel.font = contentFont
|
||||||
spoilerTextLabel.attributedText = mutableSpoilerText
|
spoilerTextLabel.attributedText = mutableSpoilerText
|
||||||
|
|
|
@ -2,14 +2,22 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class TouchFallthroughTextView: UITextView {
|
final class TouchFallthroughTextView: UITextView, EmojiInsertable {
|
||||||
var shouldFallthrough: Bool = true
|
var shouldFallthrough: Bool = true
|
||||||
|
|
||||||
private var linkHighlightView: UIView?
|
private var linkHighlightView: UIView?
|
||||||
|
|
||||||
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
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
|
clipsToBounds = false
|
||||||
textDragInteraction?.isEnabled = false
|
textDragInteraction?.isEnabled = false
|
||||||
isEditable = false
|
isEditable = false
|
||||||
|
|
Loading…
Reference in a new issue