feat: add APNG supports for more label

This commit is contained in:
CMK 2021-07-23 19:10:27 +08:00
parent 9577512ed5
commit cfc5987528
60 changed files with 562 additions and 1294 deletions

View File

@ -65,7 +65,6 @@
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; };
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; };
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
@ -309,7 +308,6 @@
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
DB6804922637CD8700430867 /* AppName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804912637CD8700430867 /* AppName.swift */; };
DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; };
DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; };
DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; };
@ -442,7 +440,6 @@
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; };
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
@ -479,11 +476,7 @@
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; };
DBBC24D226A5488600398BB9 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24B426A540AE00398BB9 /* AvatarConfigurableView.swift */; };
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
DBBC24DD26A54BCB00398BB9 /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */; };
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */; };
DBBC24DF26A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */; };
DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */; };
DBBC24E126A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */; };
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; };
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; };
@ -930,6 +923,7 @@
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = "<group>"; };
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
DB3F693926AA97BD00C883AB /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MetaTextKit; path = ../MetaTextKit; sourceTree = "<group>"; };
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@ -1143,7 +1137,6 @@
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = "<group>"; };
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
@ -1168,11 +1161,7 @@
DBBC24C326A544B900398BB9 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeService+Appearance.swift"; sourceTree = "<group>"; };
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonStatusContent.swift; sourceTree = "<group>"; };
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+ParseResult.swift"; sourceTree = "<group>"; };
DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = "<group>"; };
DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+Appearance.swift"; sourceTree = "<group>"; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; };
DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = "<group>"; };
@ -1259,7 +1248,6 @@
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
DBC6462B26A1738900B0E31B /* MastodonUI in Frameworks */,
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */,
@ -1274,7 +1262,6 @@
DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */,
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */,
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */,
DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */,
DB01E23526A98F0900C3965B /* MetaTextKit in Frameworks */,
);
@ -1600,7 +1587,6 @@
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
);
path = Service;
@ -1974,6 +1960,7 @@
children = (
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */,
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */,
DB3F693926AA97BD00C883AB /* MetaTextKit */,
DB3D0FED25BAA42200EAA174 /* MastodonSDK */,
DB427DD425BAA00100D1B89D /* Mastodon */,
DB427DEB25BAA00100D1B89D /* MastodonTests */,
@ -2771,11 +2758,7 @@
isa = PBXGroup;
children = (
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */,
DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */,
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */,
DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */,
DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */,
DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */,
DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */,
);
path = Helper;
@ -2988,7 +2971,6 @@
DB3D0FF225BAA61700EAA174 /* AlamofireImage */,
5D526FE125BE9AC400460CB9 /* MastodonSDK */,
2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */,
2D42FF6025C8177C004A627A /* ActiveLabel */,
DB0140BC25C40D7500F9F3CF /* CommonOSLog */,
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
2D939AC725EE14620076FA61 /* CropViewController */,
@ -3163,7 +3145,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1240;
LastUpgradeCheck = 1250;
TargetAttributes = {
DB427DD125BAA00100D1B89D = {
CreatedOnToolsVersion = 12.4;
@ -3210,7 +3192,6 @@
packageReferences = (
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */,
2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */,
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */,
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */,
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
@ -3552,7 +3533,6 @@
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */,
DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
@ -3712,7 +3692,6 @@
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */,
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */,
DBBC24DF26A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift in Sources */,
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */,
@ -3726,7 +3705,6 @@
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
DBBC24E126A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift in Sources */,
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
@ -3786,7 +3764,6 @@
DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */,
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
DBBC24DD26A54BCB00398BB9 /* MastodonStatusContent.swift in Sources */,
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
@ -3838,7 +3815,6 @@
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */,
DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */,
DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */,
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
@ -4746,6 +4722,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ASDK;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = "ASDK - Release";
@ -5307,14 +5284,6 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
requirement = {
kind = exactVersion;
version = 5.0.3;
};
};
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git";
@ -5352,7 +5321,7 @@
repositoryURL = "https://github.com/TwidereProject/MetaTextKit.git";
requirement = {
kind = exactVersion;
version = 2.0.0;
version = 2.1.0;
};
};
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
@ -5438,11 +5407,6 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
2D42FF6025C8177C004A627A /* ActiveLabel */ = {
isa = XCSwiftPackageProductDependency;
package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */;
productName = ActiveLabel;
};
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = {
isa = XCSwiftPackageProductDependency;
package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */;

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>24</integer>
<integer>25</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
@ -22,7 +22,7 @@
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
<integer>3</integer>
</dict>
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
<dict>
@ -37,12 +37,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>22</integer>
<integer>23</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>23</integer>
<integer>24</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -1,15 +1,6 @@
{
"object": {
"pins": [
{
"package": "ActiveLabel",
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
"state": {
"branch": null,
"revision": "d503eb3bfabc54a70139618ab2ba09ebb8c09672",
"version": "5.0.3"
}
},
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
@ -100,24 +91,6 @@
"version": "4.2.2"
}
},
{
"package": "Kingfisher",
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "44450a8f564d7c0165f736ba2250649ff8d3e556",
"version": "6.3.0"
}
},
{
"package": "MetaTextKit",
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
"state": {
"branch": null,
"revision": "44fc5111269d9862369348870835e17907062115",
"version": "2.0.0"
}
},
{
"package": "Nuke",
"repositoryURL": "https://github.com/kean/Nuke.git",

View File

@ -8,6 +8,7 @@
import Foundation
import Combine
import CoreData
import MastodonMeta
/// Note: update Equatable when change case
enum ComposeStatusItem {
@ -25,7 +26,7 @@ extension ComposeStatusItem {
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
let displayName = CurrentValueSubject<String?, Never>(nil)
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
let username = CurrentValueSubject<String?, Never>(nil)
let composeContent = CurrentValueSubject<String?, Never>(nil)
@ -35,7 +36,7 @@ extension ComposeStatusItem {
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
return lhs.avatarURL.value == rhs.avatarURL.value &&
lhs.displayName.value == rhs.displayName.value &&
lhs.emojiDict.value == rhs.emojiDict.value &&
lhs.emojiMeta.value == rhs.emojiMeta.value &&
lhs.username.value == rhs.username.value &&
lhs.composeContent.value == rhs.composeContent.value &&
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&

View File

@ -8,6 +8,7 @@
import Foundation
import Combine
import MastodonSDK
import MastodonMeta
enum ProfileFieldItem {
case field(field: FieldValue, attribute: FieldItemAttribute)
@ -56,7 +57,7 @@ extension ProfileFieldItem {
extension ProfileFieldItem {
class FieldItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable {
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
var isEditing = false
var isLast = false

View File

@ -45,12 +45,19 @@ extension ComposeStatusSection {
// set display name and username
Publishers.CombineLatest3(
attribute.displayName,
attribute.emojiDict,
attribute.username.eraseToAnyPublisher()
attribute.emojiMeta,
attribute.username
)
.receive(on: DispatchQueue.main)
.sink { displayName, emojiDict, username in
cell.statusView.nameLabel.configure(content: displayName ?? " ", emojiDict: emojiDict)
.sink { displayName, emojiMeta, username in
do {
let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.statusView.nameLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: " ")
cell.statusView.nameLabel.configure(content: metaContent)
}
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
}
.store(in: &cell.disposeBag)

View File

@ -25,7 +25,12 @@ extension CustomEmojiPickerSection {
.af.imageRounded(withCornerRadius: 4)
let url = URL(string: attribute.emoji.url)
cell.emojiImageView.setImage(url: url, placeholder: placeholder, scaleToSize: CustomEmojiPickerItemCollectionViewCell.itemSize)
cell.emojiImageView.sd_setImage(
with: url,
placeholderImage: placeholder,
options: [],
context: nil
)
cell.accessibilityLabel = attribute.emoji.shortcode
return cell
}

View File

@ -8,6 +8,7 @@
import os
import UIKit
import Combine
import MastodonMeta
enum ProfileFieldSection: Equatable, Hashable {
case main
@ -29,32 +30,60 @@ extension ProfileFieldSection {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell
// set key
cell.fieldView.titleActiveLabel.configure(field: field.name.value, emojiDict: attribute.emojiDict.value)
do {
let mastodonContent = MastodonContent(content: field.name.value, emojis: attribute.emojiMeta.value)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.titleMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: field.name.value)
cell.fieldView.titleMetaLabel.configure(content: content)
}
cell.fieldView.titleTextField.text = field.name.value
Publishers.CombineLatest(
field.name.removeDuplicates(),
attribute.emojiDict.removeDuplicates()
attribute.emojiMeta.removeDuplicates()
)
.receive(on: RunLoop.main)
.sink { [weak cell] name, emojiDict in
.sink { [weak cell] name, emojiMeta in
guard let cell = cell else { return }
cell.fieldView.titleActiveLabel.configure(field: name, emojiDict: emojiDict)
do {
let mastodonContent = MastodonContent(content: name, emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.titleMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: name)
cell.fieldView.titleMetaLabel.configure(content: content)
}
// only bind label. The text field should only set once
}
.store(in: &cell.disposeBag)
// set value
cell.fieldView.valueActiveLabel.configure(field: field.value.value, emojiDict: attribute.emojiDict.value)
do {
let mastodonContent = MastodonContent(content: field.value.value, emojis: attribute.emojiMeta.value)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.valueMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: field.value.value)
cell.fieldView.valueMetaLabel.configure(content: content)
}
cell.fieldView.valueTextField.text = field.value.value
Publishers.CombineLatest(
field.value.removeDuplicates(),
attribute.emojiDict.removeDuplicates()
attribute.emojiMeta.removeDuplicates()
)
.receive(on: RunLoop.main)
.sink { [weak cell] value, emojiDict in
.sink { [weak cell] value, emojiMeta in
guard let cell = cell else { return }
cell.fieldView.valueActiveLabel.configure(field: value, emojiDict: emojiDict)
do {
let mastodonContent = MastodonContent(content: value, emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.valueMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: value)
cell.fieldView.valueMetaLabel.configure(content: content)
}
// only bind label. The text field should only set once
}
.store(in: &cell.disposeBag)
@ -76,8 +105,8 @@ extension ProfileFieldSection {
// setup editing state
cell.fieldView.titleTextField.isHidden = !attribute.isEditing
cell.fieldView.valueTextField.isHidden = !attribute.isEditing
cell.fieldView.titleActiveLabel.isHidden = attribute.isEditing
cell.fieldView.valueActiveLabel.isHidden = attribute.isEditing
cell.fieldView.titleMetaLabel.isHidden = attribute.isEditing
cell.fieldView.valueMetaLabel.isHidden = attribute.isEditing
// set control hidden
let isHidden = !attribute.isEditing

View File

@ -11,6 +11,8 @@ import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import MetaTextKit
import MastodonMeta
enum NotificationSection: Equatable, Hashable {
case main
@ -66,7 +68,14 @@ extension NotificationSection {
.store(in: &cell.disposeBag)
// configure author name, notification description, timestamp
cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
do {
let mastodonContent = MastodonContent(content: notification.account.displayNameWithFallback, emojis: notification.account.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.nameLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: notification.account.displayNameWithFallback)
cell.nameLabel.configure(content: metaContent)
}
let createAt = notification.createAt
let actionText = notification.notificationType.actionText
cell.actionLabel.text = actionText + " · " + createAt.timeAgoSinceNow

View File

@ -1,173 +1,66 @@
//extension ActiveEntity {
//
// ActiveLabel.swift
// Mastodon
// var accessibilityLabelDescription: String {
// switch self.type {
// case .email: return L10n.Common.Controls.Status.Tag.email
// case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag
// case .mention: return L10n.Common.Controls.Status.Tag.mention
// case .url: return L10n.Common.Controls.Status.Tag.url
// case .emoji: return L10n.Common.Controls.Status.Tag.emoji
// }
// }
//
// Created by sxiaojian on 2021/1/29.
// var accessibilityValueDescription: String {
// switch self.type {
// case .email(let text, _): return text
// case .hashtag(let text, _): return text
// case .mention(let text, _): return text
// case .url(_, let trimmed, _, _): return trimmed
// case .emoji(let text, _, _): return text
// }
// }
//
// func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? {
// if case .emoji = self.type {
// return nil
// }
//
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer)
// element.accessibilityTraits = .button
// element.accessibilityLabel = accessibilityLabelDescription
// element.accessibilityValue = accessibilityValueDescription
// return element
// }
//}
import UIKit
import Foundation
import ActiveLabel
import os.log
import MastodonUI
extension ActiveLabel {
enum Style {
case `default`
case statusHeader
case statusName
case profileFieldName
case profileFieldValue
}
convenience init(style: Style) {
self.init()
numberOfLines = 0
lineSpacing = 5
mentionColor = Asset.Colors.brandBlue.color
hashtagColor = Asset.Colors.brandBlue.color
URLColor = Asset.Colors.brandBlue.color
emojiPlaceholderColor = .systemFill
accessibilityContainerType = .semanticGroup
switch style {
case .default:
font = .preferredFont(forTextStyle: .body)
textColor = Asset.Colors.Label.primary.color
case .statusHeader:
font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17)
textColor = Asset.Colors.Label.secondary.color
numberOfLines = 1
case .statusName:
font = .systemFont(ofSize: 17, weight: .semibold)
textColor = Asset.Colors.Label.primary.color
numberOfLines = 1
case .profileFieldName:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20)
textColor = Asset.Colors.Label.primary.color
numberOfLines = 1
case .profileFieldValue:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20)
textColor = Asset.Colors.Label.primary.color
numberOfLines = 1
}
}
}
extension ActiveLabel {
public func configure(text: String) {
attributedText = nil
activeEntities.removeAll()
self.text = text
accessibilityLabel = text
}
}
extension ActiveLabel {
/// status content
public func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
attributedText = nil
activeEntities.removeAll()
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
text = parseResult.trimmed
activeEntities = parseResult.activeEntities
accessibilityLabel = parseResult.original
} else {
text = ""
accessibilityLabel = nil
}
}
public func configure(contentParseResult parseResult: MastodonStatusContent.ParseResult?) {
attributedText = nil
activeEntities.removeAll()
text = parseResult?.trimmed ?? ""
activeEntities = parseResult?.activeEntities ?? []
accessibilityLabel = parseResult?.original ?? nil
}
/// account note
public func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) {
configure(content: note, emojiDict: emojiDict)
}
}
extension ActiveLabel {
/// account field
public func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) {
configure(content: field, emojiDict: emojiDict)
}
}
extension ActiveEntity {
var accessibilityLabelDescription: String {
switch self.type {
case .email: return L10n.Common.Controls.Status.Tag.email
case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag
case .mention: return L10n.Common.Controls.Status.Tag.mention
case .url: return L10n.Common.Controls.Status.Tag.url
case .emoji: return L10n.Common.Controls.Status.Tag.emoji
}
}
var accessibilityValueDescription: String {
switch self.type {
case .email(let text, _): return text
case .hashtag(let text, _): return text
case .mention(let text, _): return text
case .url(_, let trimmed, _, _): return trimmed
case .emoji(let text, _, _): return text
}
}
func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? {
if case .emoji = self.type {
return nil
}
let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer)
element.accessibilityTraits = .button
element.accessibilityLabel = accessibilityLabelDescription
element.accessibilityValue = accessibilityValueDescription
return element
}
}
final class ActiveLabelAccessibilityElement: UIAccessibilityElement {
var index: Int!
}
//final class ActiveLabelAccessibilityElement: UIAccessibilityElement {
// var index: Int!
//}
//
// MARK: - UIAccessibilityContainer
extension ActiveLabel {
func createAccessibilityElements() -> [UIAccessibilityElement] {
var elements: [UIAccessibilityElement] = []
let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
element.accessibilityTraits = .staticText
element.accessibilityLabel = accessibilityLabel
element.accessibilityFrame = superview!.convert(frame, to: nil)
element.accessibilityLanguage = accessibilityLanguage
elements.append(element)
for entity in activeEntities {
guard let element = entity.accessibilityElement(in: self) else { continue }
var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange)
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
element.accessibilityFrame = self.convert(rect, to: nil)
element.accessibilityContainer = self
elements.append(element)
}
return elements
}
}
//extension ActiveLabel {
//
// func createAccessibilityElements() -> [UIAccessibilityElement] {
// var elements: [UIAccessibilityElement] = []
//
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
// element.accessibilityTraits = .staticText
// element.accessibilityLabel = accessibilityLabel
// element.accessibilityFrame = superview!.convert(frame, to: nil)
// element.accessibilityLanguage = accessibilityLanguage
// elements.append(element)
//
// for entity in activeEntities {
// guard let element = entity.accessibilityElement(in: self) else { continue }
// var glyphRange = NSRange()
// layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange)
// let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// element.accessibilityFrame = self.convert(rect, to: nil)
// element.accessibilityContainer = self
// elements.append(element)
// }
//
// return elements
// }
//
//}

View File

@ -23,15 +23,6 @@ extension EmojiContainer {
let decoder = JSONDecoder()
return emojisData.flatMap { try? decoder.decode([Mastodon.Entity.Emoji].self, from: $0) }
}
var emojiDict: MastodonStatusContent.EmojiDict {
var dict = MastodonStatusContent.EmojiDict()
for emoji in emojis ?? [] {
guard let url = URL(string: emoji.url) else { continue }
dict[emoji.shortcode] = url
}
return dict
}
var emojiMeta: MastodonContent.Emojis {
var dict = MastodonContent.Emojis()

View File

@ -7,6 +7,7 @@
import UIKit
import MastodonSDK
import MastodonMeta
extension Mastodon.Entity.Account: Hashable {
public func hash(into hasher: inout Hasher) {
@ -28,3 +29,13 @@ extension Mastodon.Entity.Account {
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
}
}
extension Mastodon.Entity.Account {
var emojiMeta: MastodonContent.Emojis {
var dict = MastodonContent.Emojis()
for emoji in emojis ?? [] {
dict[emoji.shortcode] = emoji.url
}
return dict
}
}

View File

@ -6,14 +6,19 @@
//
import UIKit
import Meta
import MetaTextKit
extension MetaLabel {
enum Style {
case statusHeader
case statusName
// case profileFieldName
// case profileFieldValue
case notificationName
case profileFieldName
case profileFieldValue
case recommendAccountName
case titleView
case settingTableFooter
}
convenience init(style: Style) {
@ -33,14 +38,37 @@ extension MetaLabel {
case .statusName:
font = .systemFont(ofSize: 17, weight: .semibold)
textColor = Asset.Colors.Label.primary.color
// case .profileFieldName:
// font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20)
// textColor = Asset.Colors.Label.primary.color
// numberOfLines = 1
// case .profileFieldValue:
// font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20)
// textColor = Asset.Colors.Label.primary.color
// numberOfLines = 1
case .notificationName:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
textColor = Asset.Colors.brandBlue.color
case .profileFieldName:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20)
textColor = Asset.Colors.Label.primary.color
case .profileFieldValue:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20)
textColor = Asset.Colors.Label.primary.color
textAlignment = .right
case .titleView:
font = .systemFont(ofSize: 17, weight: .semibold)
textColor = Asset.Colors.Label.primary.color
textAlignment = .center
paragraphStyle.alignment = .center
case .recommendAccountName:
font = .systemFont(ofSize: 18, weight: .semibold)
textColor = .white
case .settingTableFooter:
font = .preferredFont(forTextStyle: .body)
textColor = Asset.Colors.Label.primary.color
numberOfLines = 0
textContainer.maximumNumberOfLines = 0
paragraphStyle.alignment = .center
}
self.font = font
@ -50,4 +78,23 @@ extension MetaLabel {
.font: font,
.foregroundColor: textColor
]
}}
linkAttributes = [
.font: font,
.foregroundColor: Asset.Colors.brandBlue.color
]
}
}
struct PlaintextMetaContent: MetaContent {
let string: String
let entities: [Meta.Entity] = []
init(string: String) {
self.string = string
}
func metaAttachment(for entity: Meta.Entity) -> MetaAttachment? {
return nil
}
}

View File

@ -1,63 +0,0 @@
//
// MastodonField.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-30.
//
//import Foundation
//import ActiveLabel
//
//enum MastodonField {
//
// @available(*, deprecated, message: "rely on server meta rendering")
// public static func parse(field string: String, emojiDict: MastodonStatusContent.EmojiDict) -> ParseResult {
// // use content parser get emoji entities
// let value = string
//
// var string = string
// var entities: [ActiveEntity] = []
//
// do {
// let contentParseresult = try MastodonStatusContent.parse(content: string, emojiDict: emojiDict)
// string = contentParseresult.trimmed
// entities.append(contentsOf: contentParseresult.activeEntities)
// } catch {
// // assertionFailure(error.localizedDescription)
// }
//
// let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?)")
// let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
// let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
//
//
// for match in mentionMatches {
// guard let text = string.substring(with: match, at: 0) else { continue }
// let entity = ActiveEntity(range: match.range, type: .mention(text, userInfo: nil))
// entities.append(entity)
// }
//
// for match in hashtagMatches {
// guard let text = string.substring(with: match, at: 0) else { continue }
// let entity = ActiveEntity(range: match.range, type: .hashtag(text, userInfo: nil))
// entities.append(entity)
// }
//
// for match in urlMatches {
// guard let text = string.substring(with: match, at: 0) else { continue }
// let entity = ActiveEntity(range: match.range, type: .url(text, trimmed: text, url: text, userInfo: nil))
// entities.append(entity)
// }
//
// return ParseResult(value: value, trimmed: string, activeEntities: entities)
// }
//
//}
//
//extension MastodonField {
// public struct ParseResult {
// let value: String
// let trimmed: String
// let activeEntities: [ActiveEntity]
// }
//}

View File

@ -1,17 +0,0 @@
//
// MastodonStatusContent+Appearance.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-6-20.
//
import UIKit
extension MastodonStatusContent {
public struct Appearance {
let attributes: [NSAttributedString.Key: Any]
let urlAttributes: [NSAttributedString.Key: Any]
let hashtagAttributes: [NSAttributedString.Key: Any]
let mentionAttributes: [NSAttributedString.Key: Any]
}
}

View File

@ -1,108 +0,0 @@
//
// MastodonStatusContent+ParseResult.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-6-20.
//
import Foundation
import ActiveLabel
extension MastodonStatusContent {
public struct ParseResult: Hashable {
public let document: String
public let original: String
public let trimmed: String
public let activeEntities: [ActiveEntity]
public static func == (lhs: MastodonStatusContent.ParseResult, rhs: MastodonStatusContent.ParseResult) -> Bool {
return lhs.document == rhs.document
&& lhs.original == rhs.original
&& lhs.trimmed == rhs.trimmed
&& lhs.activeEntities.count == rhs.activeEntities.count // FIXME:
}
public func hash(into hasher: inout Hasher) {
hasher.combine(document)
hasher.combine(original)
hasher.combine(trimmed)
hasher.combine(activeEntities.count) // FIXME:
}
func trimmedAttributedString(appearance: MastodonStatusContent.Appearance) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: trimmed, attributes: appearance.attributes)
for entity in activeEntities {
switch entity.type {
case .url:
attributedString.addAttributes(appearance.urlAttributes, range: entity.range)
case .hashtag:
attributedString.addAttributes(appearance.hashtagAttributes, range: entity.range)
case .mention:
attributedString.addAttributes(appearance.mentionAttributes, range: entity.range)
default:
break
}
if let uri = entity.type.uri {
attributedString.addAttributes([
.link: uri
], range: entity.range)
}
}
return attributedString
}
}
}
extension ActiveEntityType {
static let appScheme = "mastodon"
public init?(url: URL) {
guard let scheme = url.scheme?.lowercased() else { return nil }
guard scheme == ActiveEntityType.appScheme else {
self = .url("", trimmed: "", url: url.absoluteString, userInfo: nil)
return
}
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let parameters = components.queryItems else { return nil }
if let hashtag = parameters.first(where: { $0.name == "hashtag" }), let encoded = hashtag.value, let value = String(base64Encoded: encoded) {
self = .hashtag(value, userInfo: nil)
return
}
if let mention = parameters.first(where: { $0.name == "mention" }), let encoded = mention.value, let value = String(base64Encoded: encoded) {
self = .mention(value, userInfo: nil)
return
}
return nil
}
public var uri: URL? {
switch self {
case .url(_, _, let url, _):
return URL(string: url)
case .hashtag(let hashtag, _):
return URL(string: "\(ActiveEntityType.appScheme)://meta?hashtag=\(hashtag.base64Encoded)")
case .mention(let mention, _):
return URL(string: "\(ActiveEntityType.appScheme)://meta?mention=\(mention.base64Encoded)")
default:
return nil
}
}
}
extension String {
fileprivate var base64Encoded: String {
return Data(self.utf8).base64EncodedString()
}
init?(base64Encoded: String) {
guard let data = Data(base64Encoded: base64Encoded),
let string = String(data: data, encoding: .utf8) else {
return nil
}
self = string
}
}

View File

@ -1,332 +0,0 @@
//
// MastodonStatusContent.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/1.
//
import UIKit
import Combine
import ActiveLabel
import Fuzi
public enum MastodonStatusContent {
public typealias EmojiShortcode = String
public typealias EmojiDict = [EmojiShortcode: URL]
static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent)
public static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher<MastodonStatusContent.ParseResult?, Never> {
return Future { promise in
self.workingQueue.async {
let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict)
promise(.success(parseResult))
}
}
.eraseToAnyPublisher()
}
public static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
let document: String = {
var content = content.replacingOccurrences(of: "</p>", with: "</p>\r\n")
for (shortcode, url) in emojiDict {
let emojiNode = "<span class=\"emoji\" href=\"\(url.absoluteString)\">\(shortcode)</span>"
let pattern = ":\(shortcode):"
content = content.replacingOccurrences(of: pattern, with: emojiNode)
}
return content.trimmingCharacters(in: .whitespacesAndNewlines)
}()
let rootNode = try Node.parse(document: document)
let text = String(rootNode.text)
var activeEntities: [ActiveEntity] = []
let entities = MastodonStatusContent.Node.entities(in: rootNode)
for entity in entities {
let range = NSRange(entity.text.startIndex..<entity.text.endIndex, in: text)
switch entity.type {
case .url:
guard let href = entity.href else { continue }
let text = String(entity.text)
activeEntities.append(ActiveEntity(range: range, type: .url(text, trimmed: entity.hrefEllipsis ?? text, url: href, userInfo: nil)))
case .hashtag:
var userInfo: [AnyHashable: Any] = [:]
entity.href.flatMap { href in
userInfo["href"] = href
}
let hashtag = String(entity.text).deletingPrefix("#")
activeEntities.append(ActiveEntity(range: range, type: .hashtag(hashtag, userInfo: userInfo)))
case .mention:
var userInfo: [AnyHashable: Any] = [:]
entity.href.flatMap { href in
userInfo["href"] = href
}
let mention = String(entity.text).deletingPrefix("@")
activeEntities.append(ActiveEntity(range: range, type: .mention(mention, userInfo: userInfo)))
case .emoji:
var userInfo: [AnyHashable: Any] = [:]
guard let href = entity.href else { continue }
userInfo["href"] = href
let emoji = String(entity.text)
activeEntities.append(ActiveEntity(range: range, type: .emoji(emoji, url: href, userInfo: userInfo)))
case .none:
continue
}
}
var trimmed = text
for activeEntity in activeEntities {
MastodonStatusContent.trimEntity(status: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
}
return ParseResult(
document: document,
original: text,
trimmed: trimmed,
activeEntities: activeEntities
)
}
static func trimEntity(status: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) {
let text: String
let trimmed: String
switch activeEntity.type {
case .url(let _text, let _trimmed, _, _):
text = _text
trimmed = _trimmed
case .emoji(let _text, _, _):
text = _text
trimmed = " "
default:
return
}
guard let index = activeEntities.firstIndex(where: { $0.range == activeEntity.range }) else { return }
guard let range = Range(activeEntity.range, in: status) else { return }
status.replaceSubrange(range, with: trimmed)
let offset = trimmed.count - text.count
activeEntity.range.length += offset
let moveActiveEntities = Array(activeEntities[index...].dropFirst())
for moveActiveEntity in moveActiveEntities {
moveActiveEntity.range.location += offset
}
}
}
extension String {
// ref: https://www.hackingwithswift.com/example-code/strings/how-to-remove-a-prefix-from-a-string
func deletingPrefix(_ prefix: String) -> String {
guard self.hasPrefix(prefix) else { return self }
return String(self.dropFirst(prefix.count))
}
}
extension MastodonStatusContent {
class Node {
let level: Int
let type: Type?
// substring text
let text: Substring
// range in parent String
var range: Range<String.Index> {
return text.startIndex..<text.endIndex
}
let tagName: String?
let attributes: [String : String]
let href: String?
let hrefEllipsis: String?
let children: [Node]
init(
level: Int,
text: Substring,
tagName: String?,
attributes: [String : String],
href: String?,
hrefEllipsis: String?,
children: [Node]
) {
let _classNames: Set<String> = {
guard let className = attributes["class"] else { return Set() }
return Set(className.components(separatedBy: " "))
}()
let _type: Type? = {
if tagName == "a" {
if _classNames.contains("u-url") {
return .mention
}
if _classNames.contains("hashtag") {
return .hashtag
}
return .url
} else {
if _classNames.contains("emoji") {
return .emoji
}
return nil
}
}()
self.level = level
self.type = _type
self.text = text
self.tagName = tagName
self.attributes = attributes
self.href = href
self.hrefEllipsis = hrefEllipsis
self.children = children
}
static func parse(document: String) throws -> MastodonStatusContent.Node {
let document = document.replacingOccurrences(of: "<br>|<br />", with: "\r\n", options: .regularExpression, range: nil)
let html = try HTMLDocument(string: document)
let body = html.body ?? nil
let text = body?.stringValue ?? ""
let level = 0
let children: [MastodonStatusContent.Node] = body.flatMap { body in
return Node.parse(element: body, parentText: text[...], parentLevel: level + 1)
} ?? []
let node = Node(
level: level,
text: text[...],
tagName: body?.tag,
attributes: body?.attributes ?? [:],
href: nil,
hrefEllipsis: nil,
children: children
)
return node
}
static func parse(element: XMLElement, parentText: Substring, parentLevel: Int) -> [Node] {
let parent = element
let scanner = Scanner(string: String(parentText))
scanner.charactersToBeSkipped = .none
var children: [Node] = []
for _element in parent.children {
let _text = _element.stringValue
// scan element text
_ = scanner.scanUpToString(_text)
let startIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string)
guard scanner.scanString(_text) != nil else {
assertionFailure()
continue
}
let endIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string)
// locate substring
let startIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: startIndexOffset)
let endIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: endIndexOffset)
let text = Substring(parentText.utf16[startIndex..<endIndex])
let href = _element["href"]
let hrefEllipsis = href.flatMap { _ in _element.firstChild(css: ".ellipsis")?.stringValue }
let level = parentLevel + 1
let node = Node(
level: level,
text: text,
tagName: _element.tag,
attributes: _element.attributes,
href: href,
hrefEllipsis: hrefEllipsis,
children: Node.parse(element: _element, parentText: text, parentLevel: level + 1)
)
children.append(node)
}
return children
}
static func collect(
node: Node,
where predicate: (Node) -> Bool
) -> [Node] {
var nodes: [Node] = []
if predicate(node) {
nodes.append(node)
}
for child in node.children {
nodes.append(contentsOf: Node.collect(node: child, where: predicate))
}
return nodes
}
}
}
extension MastodonStatusContent.Node {
enum `Type` {
case url
case mention
case hashtag
case emoji
}
static func entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
return MastodonStatusContent.Node.collect(node: node) { node in node.type != nil }
}
static func hashtags(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .hashtag }
}
static func mentions(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .mention }
}
static func urls(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .url }
}
}
extension MastodonStatusContent.Node: CustomDebugStringConvertible {
var debugDescription: String {
let linkInfo: String = {
switch (href, hrefEllipsis) {
case (nil, nil):
return ""
case (let href, let hrefEllipsis):
return "(\(href ?? "nil") - \(hrefEllipsis ?? "nil"))"
}
}()
let classNamesInfo: String = {
guard let className = attributes["class"] else { return "" }
return "@[\(className)]"
}()
let nodeDescription = String(
format: "<%@>%@%@: %@",
tagName ?? "",
classNamesInfo,
linkInfo,
String(text)
)
guard !children.isEmpty else {
return nodeDescription
}
let indent = Array(repeating: " ", count: level).joined()
let childrenDescription = children
.map { indent + $0.debugDescription }
.joined(separator: "\n")
return nodeDescription + "\n" + childrenDescription
}
}

View File

@ -8,13 +8,10 @@
#if ASDK
import Foundation
import ActiveLabel
// MARK: - StatusViewDelegate
extension StatusNodeDelegate where Self: StatusProvider {
func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) {
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, node: node, didSelectActiveEntityType: type)
}
}
#endif

View File

@ -11,7 +11,6 @@ import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import ActiveLabel
import Meta
import MetaTextKit
@ -25,10 +24,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) {
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell)
}
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, cell: cell, activeLabel: activeLabel, didTapEntity: entity)
}
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta)

View File

@ -11,7 +11,6 @@ import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import ActiveLabel
import Meta
import MetaTextKit
@ -125,35 +124,6 @@ extension StatusProviderFacade {
}
extension StatusProviderFacade {
static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) {
switch entity.type {
case .url(_, _, let url, _),
.mention(let url, _) where url.lowercased().hasPrefix("http"):
// note:
// some server mark the normal url as "u-url" class. :
guard let url = URL(string: url) else { return }
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
url.pathComponents.count >= 4,
url.pathComponents[0] == "/",
url.pathComponents[1] == "web",
url.pathComponents[2] == "statuses" {
let statusID = url.pathComponents[3]
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
} else {
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
}
case .hashtag(let text, _):
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text)
provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
case .mention(let text, let userInfo):
let href = userInfo?["href"] as? String
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text, href: href)
default:
break
}
}
static func responseToStatusMetaTextAction(provider: StatusProvider, cell: UITableViewCell, metaText: MetaText, didSelectMeta meta: Meta) {
switch meta {
@ -185,31 +155,6 @@ extension StatusProviderFacade {
}
#if ASDK
static func responseToStatusActiveLabelAction(provider: StatusProvider, node: ASCellNode, didSelectActiveEntityType type: ActiveEntityType) {
switch type {
case .hashtag(let text, _):
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text)
provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
case .mention(let text, _):
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, node: node, mention: text)
case .url(_, _, let url, _):
guard let url = URL(string: url) else { return }
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
url.pathComponents.count >= 4,
url.pathComponents[0] == "/",
url.pathComponents[1] == "web",
url.pathComponents[2] == "statuses" {
let statusID = url.pathComponents[3]
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
} else {
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
}
default:
break
}
}
private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, node: ASCellNode, mention: String) {
guard let status = provider.status(node: node, indexPath: nil) else { return }
coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention, href: nil)

View File

@ -40,20 +40,19 @@ final class ComposeStatusContentTableViewCell: UITableViewCell {
attributes: attributes
)
}()
let paragraphStyle: NSMutableParagraphStyle = {
metaText.paragraphStyle = {
let style = NSMutableParagraphStyle()
style.lineSpacing = 5
style.paragraphSpacing = 8
return style
}()
metaText.textAttributes = [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
.foregroundColor: Asset.Colors.Label.primary.color,
.paragraphStyle: paragraphStyle,
]
metaText.linkAttributes = [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
.foregroundColor: Asset.Colors.brandBlue.color,
.paragraphStyle: paragraphStyle,
]
return metaText
}()

View File

@ -6,14 +6,14 @@
//
import UIKit
import FLAnimatedImage
import SDWebImage
final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
static let itemSize = CGSize(width: 44, height: 44)
let emojiImageView: FLAnimatedImageView = {
let imageView = FLAnimatedImageView()
let emojiImageView: SDAnimatedImageView = {
let imageView = SDAnimatedImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.masksToBounds = true
return imageView

View File

@ -193,8 +193,15 @@ extension ComposeViewModel: UITableViewDataSource {
// set avatar
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
// set name username
cell.statusView.nameLabel.configure(content: status.author.displayNameWithFallback, emojiDict: status.author.emojiDict)
// set name, username
do {
let mastodonContent = MastodonContent(content: status.author.displayNameWithFallback, emojis: status.author.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.statusView.nameLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: status.author.displayNameWithFallback)
cell.statusView.nameLabel.configure(content: metaContent)
}
cell.statusView.usernameLabel.text = "@" + status.author.acct
// set text
let content = MastodonContent(content: status.content, emojis: status.emojiMeta)
@ -226,13 +233,14 @@ extension ComposeViewModel: UITableViewDataSource {
let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Scene.Compose.replyingToUser(name)
}()
MastodonStatusContent.parseResult(content: headerText, emojiDict: replyTo.author.emojiDict)
.receive(on: DispatchQueue.main)
.sink { [weak cell] parseResult in
guard let cell = cell else { return }
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
}
.store(in: &cell.disposeBag)
do {
let mastodonContent = MastodonContent(content: headerText, emojis: replyTo.author.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.statusView.headerInfoLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: headerText)
cell.statusView.headerInfoLabel.configure(content: metaContent)
}
}
// configure author
ComposeStatusSection.configureStatusContent(cell: cell, attribute: composeStatusAttribute)

View File

@ -181,7 +181,7 @@ final class ComposeViewModel: NSObject {
}
return displayName
}()
self.composeStatusAttribute.emojiDict.value = mastodonUser?.emojiDict ?? [:]
self.composeStatusAttribute.emojiMeta.value = mastodonUser?.emojiMeta ?? [:]
self.composeStatusAttribute.username.value = username
}
.store(in: &disposeBag)

View File

@ -7,7 +7,6 @@
import os.log
import UIKit
import ActiveLabel
import FLAnimatedImage
import MetaTextKit
@ -52,12 +51,7 @@ final class ReplicaStatusView: UIView {
return label
}()
let headerInfoLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusHeader)
label.text = "Bob reblogged"
label.layer.masksToBounds = false
return label
}()
let headerInfoLabel = MetaLabel(style: .statusHeader)
let avatarView: UIView = {
let view = UIView()
@ -68,10 +62,7 @@ final class ReplicaStatusView: UIView {
}()
let avatarImageView = FLAnimatedImageView()
let nameLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusName)
return label
}()
let nameLabel = MetaLabel(style: .statusName)
let nameTrialingDotLabel: UILabel = {
let label = UILabel()

View File

@ -56,7 +56,7 @@ extension HashtagTimelineViewController {
super.viewDidLoad()
title = "#\(viewModel.hashtag)"
titleView.update(title: viewModel.hashtag, subtitle: nil, emojiDict: [:])
titleView.update(title: viewModel.hashtag, subtitle: nil)
navigationItem.titleView = titleView
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
@ -150,7 +150,7 @@ extension HashtagTimelineViewController {
private func updatePromptTitle() {
var subtitle: String?
defer {
titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle, emojiDict: [:])
titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle)
}
guard let histories = viewModel.hashtagEntity.value?.history else {
return
@ -209,7 +209,7 @@ extension HashtagTimelineViewController: LoadMoreConfigurableTableViewContainer
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
typealias LoadingState = HashtagTimelineViewModel.LoadOldestState.Loading
var loadMoreConfigurableTableView: UITableView { return tableView }
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadOldestStateMachine }
}
// MARK: - UITableViewDelegate

View File

@ -84,7 +84,7 @@ extension HashtagTimelineViewModel {
newSnapshot.appendItems(statusItemList, toSection: .main)
}
if !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
if !(self.loadOldestStateMachine.currentState is LoadOldestState.NoMore) {
newSnapshot.appendItems([.bottomLoader], toSection: .main)
}

View File

@ -46,7 +46,7 @@ final class HashtagTimelineViewModel: NSObject {
}()
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
// bottom loader
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
LoadOldestState.Initial(viewModel: self),

View File

@ -109,6 +109,12 @@ extension AsyncHomeTimelineViewController: StatusProvider {
return nil
}
}
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
return items
}
}

View File

@ -86,7 +86,7 @@ extension AsyncHomeTimelineViewController {
node.allowsSelection = true
title = L10n.Scene.HomeTimeline.title
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
navigationItem.leftBarButtonItem = settingBarButtonItem
navigationItem.titleView = titleView
titleView.delegate = self
@ -341,7 +341,7 @@ extension AsyncHomeTimelineViewController {
// typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
// typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading
// var loadMoreConfigurableTableView: UITableView { return tableView }
// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadOldestStateMachine }
//}
// MARK: - UITableViewDelegate
@ -556,7 +556,7 @@ extension AsyncHomeTimelineViewController: ASTableDelegate {
}
func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) {
viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
context.completeBatchFetching(true)
}

View File

@ -106,7 +106,7 @@ extension AsyncHomeTimelineViewModel: NSFetchedResultsControllerDelegate {
let endSnapshot = CACurrentMediaTime()
if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
if shouldAddBottomLoader, !(self.loadLatestStateMachine.currentState is LoadOldestState.NoMore) {
newSnapshot.appendItems([.bottomLoader], toSection: .main)
}

View File

@ -18,7 +18,6 @@ import CoreDataStack
import GameplayKit
import AlamofireImage
import DateToolsSwift
import ActiveLabel
import AsyncDisplayKit
final class AsyncHomeTimelineViewModel: NSObject {
@ -59,7 +58,7 @@ final class AsyncHomeTimelineViewModel: NSObject {
}()
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
// bottom loader
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
LoadOldestState.Initial(viewModel: self),

View File

@ -388,7 +388,7 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading
var loadMoreConfigurableTableView: UITableView { return tableView }
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadLatestStateMachine }
}
// MARK: - UITableViewDelegate

View File

@ -115,7 +115,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
let endSnapshot = CACurrentMediaTime()
DispatchQueue.main.async {
if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
if shouldAddBottomLoader, !(self.loadLatestStateMachine.currentState is LoadOldestState.NoMore) {
newSnapshot.appendItems([.bottomLoader], toSection: .main)
}

View File

@ -15,7 +15,6 @@ import CoreDataStack
import GameplayKit
import AlamofireImage
import DateToolsSwift
import ActiveLabel
final class HomeTimelineViewModel: NSObject {
@ -52,7 +51,7 @@ final class HomeTimelineViewModel: NSObject {
}()
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
// bottom loader
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
LoadOldestState.Initial(viewModel: self),

View File

@ -12,7 +12,6 @@ import GameplayKit
import MastodonSDK
import OSLog
import UIKit
import ActiveLabel
import Meta
import MetaTextKit
@ -272,7 +271,7 @@ extension NotificationViewController {
switch item {
case .bottomLoader:
if !tableView.isDragging, !tableView.isDecelerating {
viewModel.loadoldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self)
viewModel.loadOldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self)
}
default:
break
@ -305,7 +304,7 @@ extension NotificationViewController: NotificationTableViewCellDelegate {
}
}
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: ActiveLabel) {
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: MetaLabel) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
@ -319,7 +318,6 @@ extension NotificationViewController: NotificationTableViewCellDelegate {
}
}
func notificationTableViewCell(_ cell: NotificationStatusTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) {
viewModel.acceptFollowRequest(notification: notification)
}
@ -380,7 +378,7 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
typealias LoadingState = NotificationViewModel.LoadOldestState.Loading
var loadMoreConfigurableTableView: UITableView { tableView }
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadOldestStateMachine }
}
extension NotificationViewController {

View File

@ -54,7 +54,7 @@ final class NotificationViewModel: NSObject {
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
// bottom loader
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
LoadOldestState.Initial(viewModel: self),

View File

@ -10,7 +10,6 @@ import Combine
import Foundation
import CoreDataStack
import UIKit
import ActiveLabel
import MetaTextKit
import Meta
import FLAnimatedImage
@ -20,7 +19,7 @@ protocol NotificationTableViewCellDelegate: AnyObject {
func parent() -> UIViewController
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, avatarImageViewDidPressed imageView: UIImageView)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: ActiveLabel)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: MetaLabel)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
@ -58,13 +57,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
return label
}()
let nameLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusName)
label.textColor = Asset.Colors.brandBlue.color
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
label.lineBreakMode = .byTruncatingTail
return label
}()
let nameLabel = MetaLabel(style: .notificationName)
let buttonStackView = UIStackView()
@ -318,10 +311,6 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
// do nothing
}
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
// do nothing
}
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
delegate?.notificationStatusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)

View File

@ -57,7 +57,7 @@ extension FavoriteViewController {
.store(in: &disposeBag)
navigationItem.titleView = titleView
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil, emojiDict: [:])
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -9,16 +9,16 @@ import os.log
import UIKit
import Combine
import PhotosUI
import ActiveLabel
import AlamofireImage
import CropViewController
import TwitterTextEditor
import MastodonMeta
import MetaTextKit
protocol ProfileHeaderViewControllerDelegate: AnyObject {
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int)
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
}
final class ProfileHeaderViewController: UIViewController {
@ -35,6 +35,7 @@ final class ProfileHeaderViewController: UIViewController {
let titleView: DoubleTitleLabelNavigationBarTitleView = {
let titleView = DoubleTitleLabelNavigationBarTitleView()
titleView.titleLabel.textColor = .white
titleView.titleLabel.textAttributes[.foregroundColor] = UIColor.white
titleView.titleLabel.alpha = 0
titleView.subtitleLabel.textColor = .white
titleView.subtitleLabel.alpha = 0
@ -179,19 +180,14 @@ extension ProfileHeaderViewController {
viewModel.isEditing,
viewModel.displayProfileInfo.name.removeDuplicates(),
viewModel.editProfileInfo.name.removeDuplicates(),
viewModel.emojiDict
viewModel.emojiMeta
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, name, editingName, emojiDict in
.sink { [weak self] isEditing, name, editingName, emojiMeta in
guard let self = self else { return }
do {
var emojis = MastodonContent.Emojis()
for (key, value) in emojiDict {
emojis[key] = value.absoluteString
}
let metaContent = try MastodonMetaContent.convert(
document: MastodonContent(content: name ?? " ", emojis: emojis)
)
let mastodonContent = MastodonContent(content: name ?? " ", emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
self.profileHeaderView.nameMetaText.configure(content: metaContent)
} catch {
assertionFailure()
@ -200,25 +196,37 @@ extension ProfileHeaderViewController {
}
.store(in: &disposeBag)
Publishers.CombineLatest3(
viewModel.isEditing.eraseToAnyPublisher(),
viewModel.displayProfileInfo.note.removeDuplicates().eraseToAnyPublisher(),
viewModel.editProfileInfo.note.removeDuplicates().eraseToAnyPublisher()
Publishers.CombineLatest4(
viewModel.isEditing.removeDuplicates(),
viewModel.displayProfileInfo.note.removeDuplicates(),
viewModel.editProfileInfo.note.removeDuplicates(),
viewModel.emojiMeta.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, note, editingNote in
.sink { [weak self] isEditing, note, editingNote, emojiMeta in
guard let self = self else { return }
self.profileHeaderView.bioActiveLabel.configure(note: note ?? "", emojiDict: [:]) // FIXME: custom emoji
// prevent duplicate set
let editingNote = editingNote ?? ""
if self.profileHeaderView.bioTextEditorView.text != editingNote {
self.profileHeaderView.bioTextEditorView.text = editingNote
self.profileHeaderView.bioMetaText.textView.isEditable = isEditing
if isEditing {
if self.profileHeaderView.bioMetaText.backedString != note {
let metaContent = PlaintextMetaContent(string: editingNote ?? "")
self.profileHeaderView.bioMetaText.configure(content: metaContent)
}
} else {
let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta)
do {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
self.profileHeaderView.bioMetaText.configure(content: metaContent)
} catch {
assertionFailure()
self.profileHeaderView.bioMetaText.reset()
}
}
}
.store(in: &disposeBag)
profileHeaderView.bioTextEditorView.changeObserver = self
profileHeaderView.bioMetaText.delegate = self
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField)
.receive(on: DispatchQueue.main)
.sink { [weak self] notification in
@ -450,13 +458,16 @@ extension ProfileHeaderViewController {
}
// MARK: - TextEditorViewChangeObserver
extension ProfileHeaderViewController: TextEditorViewChangeObserver {
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text)
guard changeResult.isTextChanged else { return }
assert(textEditorView === profileHeaderView.bioTextEditorView)
viewModel.editProfileInfo.note.value = textEditorView.text
// MARK: - MetaTextDelegate
extension ProfileHeaderViewController: MetaTextDelegate {
func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, metaText.backedString)
assert(metaText.textView === profileHeaderView.bioMetaText.textView)
if metaText.textView === profileHeaderView.bioMetaText.textView {
viewModel.editProfileInfo.note.value = metaText.backedString
}
return nil
}
}
@ -533,6 +544,7 @@ extension ProfileHeaderViewController: UICollectionViewDelegate {
// MARK: - ProfileFieldCollectionViewCellDelegate
extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate {
// should be remove style edit button
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) {
guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return }
@ -541,8 +553,8 @@ extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate {
viewModel.removeFieldItem(item: item)
}
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, activeLabel: activeLabel, didSelectActiveEntity: entity)
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) {
delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, metaLabel: metaLebel, didSelectMeta: meta)
}
}

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import Kanna
import MastodonSDK
import MastodonMeta
final class ProfileHeaderViewModel {
@ -24,7 +25,7 @@ final class ProfileHeaderViewModel {
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false)
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
// output
@ -58,10 +59,10 @@ final class ProfileHeaderViewModel {
isEditing.removeDuplicates(),
displayProfileInfo.fields.removeDuplicates(),
editProfileInfo.fields.removeDuplicates(),
emojiDict.removeDuplicates()
emojiMeta.removeDuplicates()
)
.receive(on: RunLoop.main)
.sink { [weak self] isEditing, displayFields, editingFields, emojiDict in
.sink { [weak self] isEditing, displayFields, editingFields, emojiMeta in
guard let self = self else { return }
guard let diffableDataSource = self.fieldDiffableDataSource else { return }
@ -87,7 +88,7 @@ final class ProfileHeaderViewModel {
let attribute = oldFieldAttributeDict[field.id] ?? ProfileFieldItem.FieldItemAttribute()
attribute.isEditing = isEditing
attribute.emojiDict.value = emojiDict
attribute.emojiMeta.value = emojiMeta
attribute.isLast = false
return ProfileFieldItem.field(field: field, attribute: attribute)
}

View File

@ -95,12 +95,12 @@ extension ProfileFieldAddEntryCollectionViewCell {
bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
])
fieldView.titleActiveLabel.isHidden = false
fieldView.titleActiveLabel.configure(field: L10n.Scene.Profile.Fields.addRow, emojiDict: [:])
fieldView.titleMetaLabel.isHidden = false
fieldView.titleMetaLabel.configure(content: PlaintextMetaContent(string: L10n.Scene.Profile.Fields.addRow))
fieldView.titleTextField.isHidden = true
fieldView.valueActiveLabel.isHidden = false
fieldView.valueActiveLabel.configure(field: " ", emojiDict: [:])
fieldView.valueMetaLabel.isHidden = false
fieldView.valueMetaLabel.configure(content: PlaintextMetaContent(string: " "))
fieldView.valueTextField.isHidden = true
addGestureRecognizer(singleTagGestureRecognizer)

View File

@ -8,11 +8,11 @@
import os.log
import UIKit
import Combine
import ActiveLabel
import MetaTextKit
protocol ProfileFieldCollectionViewCellDelegate: AnyObject {
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton)
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta)
}
final class ProfileFieldCollectionViewCell: UICollectionViewCell {
@ -107,7 +107,7 @@ extension ProfileFieldCollectionViewCell {
editButton.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.editButtonDidPressed(_:)), for: .touchUpInside)
fieldView.valueActiveLabel.delegate = self
fieldView.valueMetaLabel.linkDelegate = self
resetSeparatorLineLayout()
}
@ -153,13 +153,11 @@ extension ProfileFieldCollectionViewCell {
}
}
// MARK: - ActiveLabelDelegate
extension ProfileFieldCollectionViewCell: ActiveLabelDelegate {
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
// MARK: - MetaLabelDelegate
extension ProfileFieldCollectionViewCell: MetaLabelDelegate {
func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.profileFieldCollectionViewCell(self, activeLabel: activeLabel, didSelectActiveEntity: entity)
delegate?.profileFieldCollectionViewCell(self, metaLebel: metaLabel, didSelectMeta: meta)
}
}

View File

@ -7,7 +7,7 @@
import UIKit
import Combine
import ActiveLabel
import MetaTextKit
final class ProfileFieldView: UIView {
@ -18,11 +18,7 @@ final class ProfileFieldView: UIView {
let value = PassthroughSubject<String, Never>()
// for custom emoji display
let titleActiveLabel: ActiveLabel = {
let label = ActiveLabel(style: .profileFieldName)
label.configure(content: "title", emojiDict: [:])
return label
}()
let titleMetaLabel = MetaLabel(style: .profileFieldName)
// for editing
let titleTextField: UITextField = {
@ -34,12 +30,7 @@ final class ProfileFieldView: UIView {
}()
// for custom emoji display
let valueActiveLabel: ActiveLabel = {
let label = ActiveLabel(style: .profileFieldValue)
label.configure(content: "value", emojiDict: [:])
label.textAlignment = .right
return label
}()
let valueMetaLabel = MetaLabel(style: .profileFieldValue)
// for editing
let valueTextField: UITextField = {
@ -81,10 +72,10 @@ extension ProfileFieldView {
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
titleActiveLabel.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(titleActiveLabel)
titleMetaLabel.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(titleMetaLabel)
NSLayoutConstraint.activate([
titleActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
titleMetaLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
])
titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
titleTextField.translatesAutoresizingMaskIntoConstraints = false
@ -94,12 +85,12 @@ extension ProfileFieldView {
])
titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(valueActiveLabel)
valueMetaLabel.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(valueMetaLabel)
NSLayoutConstraint.activate([
valueActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
valueMetaLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
])
valueActiveLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
valueMetaLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(valueTextField)
NSLayoutConstraint.activate([
@ -137,7 +128,8 @@ struct ProfileFieldView_Previews: PreviewProvider {
static var previews: some View {
UIViewPreview(width: 375) {
let filedView = ProfileFieldView()
filedView.valueActiveLabel.configure(field: "https://mastodon.online", emojiDict: [:])
let content = PlaintextMetaContent(string: "https://mastodon.online")
filedView.valueMetaLabel.configure(content: content)
return filedView
}
.previewLayout(.fixed(width: 375, height: 100))

View File

@ -8,8 +8,6 @@
import os.log
import UIKit
import Combine
import ActiveLabel
import TwitterTextEditor
import FLAnimatedImage
import MetaTextKit
@ -17,7 +15,7 @@ protocol ProfileHeaderViewDelegate: AnyObject {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView)
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView)
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity)
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta)
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView)
@ -166,28 +164,38 @@ final class ProfileHeaderView: UIView {
}()
let bioContainerView = UIView()
let bioContainerStackView = UIStackView()
let fieldContainerStackView = UIStackView()
let bioActiveLabelContainer: UIView = {
// use to set margin for active label
// the display/edit mode bio transition animation should without flicker with that
let view = UIView()
// note: comment out to see how it works
view.layoutMargins = UIEdgeInsets(top: 8, left: 5, bottom: 8, right: 5) // magic from TextEditorView
return view
}()
let bioActiveLabel = ActiveLabel(style: .default)
let bioTextEditorView: TextEditorView = {
let textEditorView = TextEditorView()
textEditorView.scrollView.isScrollEnabled = false
textEditorView.isScrollEnabled = false
textEditorView.font = .preferredFont(forTextStyle: .body)
textEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
textEditorView.layer.masksToBounds = true
textEditorView.layer.cornerCurve = .continuous
textEditorView.layer.cornerRadius = 10
return textEditorView
let bioMetaText: MetaText = {
let metaText = MetaText()
metaText.textView.backgroundColor = .clear
metaText.textView.isEditable = false
metaText.textView.isSelectable = true
metaText.textView.isScrollEnabled = false
//metaText.textView.textContainer.lineFragmentPadding = 0
//metaText.textView.textContainerInset = .zero
metaText.textView.layer.masksToBounds = false
metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
metaText.textView.layer.masksToBounds = true
metaText.textView.layer.cornerCurve = .continuous
metaText.textView.layer.cornerRadius = 10
metaText.paragraphStyle = {
let style = NSMutableParagraphStyle()
style.lineSpacing = 5
style.paragraphSpacing = 8
return style
}()
metaText.textAttributes = [
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: Asset.Colors.Label.primary.color,
]
metaText.linkAttributes = [
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: Asset.Colors.brandBlue.color,
]
return metaText
}()
static func createFieldCollectionViewLayout() -> UICollectionViewLayout {
@ -405,28 +413,15 @@ extension ProfileHeaderView {
bioContainerView.preservesSuperviewLayoutMargins = true
metaContainerStackView.addArrangedSubview(bioContainerView)
bioContainerStackView.translatesAutoresizingMaskIntoConstraints = false
bioContainerView.addSubview(bioContainerStackView)
bioMetaText.textView.translatesAutoresizingMaskIntoConstraints = false
bioContainerView.addSubview(bioMetaText.textView)
NSLayoutConstraint.activate([
bioContainerStackView.topAnchor.constraint(equalTo: bioContainerView.topAnchor),
bioContainerStackView.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor),
bioContainerStackView.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor),
bioContainerStackView.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor),
bioMetaText.textView.topAnchor.constraint(equalTo: bioContainerView.topAnchor),
bioMetaText.textView.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor),
bioMetaText.textView.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor),
bioMetaText.textView.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor),
])
bioActiveLabel.translatesAutoresizingMaskIntoConstraints = false
bioActiveLabelContainer.addSubview(bioActiveLabel)
NSLayoutConstraint.activate([
bioActiveLabel.topAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.topAnchor),
bioActiveLabel.leadingAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.leadingAnchor),
bioActiveLabel.trailingAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.trailingAnchor),
bioActiveLabel.bottomAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.bottomAnchor),
])
bioContainerStackView.axis = .vertical
bioContainerStackView.addArrangedSubview(bioActiveLabelContainer)
bioContainerStackView.addArrangedSubview(bioTextEditorView)
fieldCollectionView.translatesAutoresizingMaskIntoConstraints = false
metaContainerStackView.addArrangedSubview(fieldCollectionView)
fieldCollectionViewHeightLayoutConstraint = fieldCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
@ -445,7 +440,7 @@ extension ProfileHeaderView {
bringSubviewToFront(bannerContainerView)
bringSubviewToFront(nameContainerStackView)
bioActiveLabel.delegate = self
bioMetaText.textView.linkDelegate = self
let avatarImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
avatarImageView.addGestureRecognizer(avatarImageViewSingleTapGestureRecognizer)
@ -479,9 +474,8 @@ extension ProfileHeaderView {
nameMetaText.textView.alpha = 1
nameTextField.alpha = 0
nameTextField.isEnabled = false
bioActiveLabelContainer.isHidden = false
bioTextEditorView.isHidden = true
bioMetaText.textView.backgroundColor = .clear
animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
self.nameTextFieldBackgroundView.backgroundColor = .clear
@ -494,17 +488,15 @@ extension ProfileHeaderView {
nameMetaText.textView.alpha = 0
nameTextField.isEnabled = true
nameTextField.alpha = 1
bioActiveLabelContainer.isHidden = true
bioTextEditorView.isHidden = false
editAvatarBackgroundView.isHidden = false
editAvatarBackgroundView.alpha = 0
bioTextEditorView.backgroundColor = .clear
bioMetaText.textView.backgroundColor = .clear
animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
self.editAvatarBackgroundView.alpha = 1
self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
}
}
@ -530,11 +522,11 @@ extension ProfileHeaderView {
}
}
// MARK: - ActiveLabelDelegate
extension ProfileHeaderView: ActiveLabelDelegate {
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
delegate?.profileHeaderView(self, activeLabel: activeLabel, entityDidPressed: entity)
// MARK: - MetaTextViewDelegate
extension ProfileHeaderView: MetaTextViewDelegate {
func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.profileHeaderView(self, metaTextView: metaTextView, metaDidPressed: meta)
}
}

View File

@ -8,7 +8,8 @@
import os.log
import UIKit
import Combine
import ActiveLabel
import MastodonMeta
import MetaTextKit
final class ProfileViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -316,20 +317,26 @@ extension ProfileViewController {
// bind view model
Publishers.CombineLatest3(
viewModel.name,
viewModel.emojiDict,
viewModel.emojiMeta,
viewModel.statusesCount
)
.receive(on: DispatchQueue.main)
.sink { [weak self] name, emojiDict, statusesCount in
.sink { [weak self] name, emojiMeta, statusesCount in
guard let self = self else { return }
guard let title = name, let statusesCount = statusesCount,
let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else {
self.titleView.isHidden = true
return
}
let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount)
self.titleView.update(title: title, subtitle: subtitle, emojiDict: emojiDict)
self.titleView.isHidden = false
let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount)
let mastodonContent = MastodonContent(content: title, emojis: emojiMeta)
do {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle)
} catch {
}
}
.store(in: &disposeBag)
viewModel.name
@ -391,9 +398,9 @@ extension ProfileViewController {
viewModel.accountForEdit
.assign(to: \.value, on: profileHeaderViewController.viewModel.accountForEdit)
.store(in: &disposeBag)
viewModel.emojiDict
viewModel.emojiMeta
.receive(on: DispatchQueue.main)
.assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict)
.assign(to: \.value, on: profileHeaderViewController.viewModel.emojiMeta)
.store(in: &disposeBag)
viewModel.username
.map { username in username.flatMap { "@" + $0 } ?? " " }
@ -695,7 +702,7 @@ extension ProfileViewController: UIScrollViewDelegate {
// MARK: - ProfileHeaderViewControllerDelegate
extension ProfileViewController: ProfileHeaderViewControllerDelegate {
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) {
guard let scrollView = (profileSegmentedViewController.pagingViewController.currentViewController as? UserTimelineViewController)?.scrollView else {
// assertionFailure()
@ -712,23 +719,24 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
)
}
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
// handle profile fields interaction
switch entity.type {
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) {
switch meta {
case .url(_, _, let url, _):
guard let url = URL(string: url) else { return }
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
case .hashtag(let hashtag, _):
case .hashtag(_, let hashtag, _):
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
case .mention(_, let userInfo):
case .mention(_, _, let userInfo):
guard let href = userInfo?["href"] as? String else {
// currently we cannot present profile scene without userID
return
}
guard let url = URL(string: href) else { return }
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
default:
case .email:
break
case .emoji:
break
}
}
@ -758,7 +766,7 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
// MARK: - ProfileHeaderViewDelegate
extension ProfileViewController: ProfileHeaderViewDelegate {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) {
guard let mastodonUser = viewModel.mastodonUser.value else { return }
guard let avatar = imageView.image else { return }
@ -953,24 +961,24 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
}
}
}
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) {
switch entity.type {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta) {
switch meta {
case .url(_, _, let url, _):
guard let url = URL(string: url) else { return }
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
case .mention(_, let userInfo):
case .mention(_, _, let userInfo):
guard let href = userInfo?["href"] as? String,
let url = URL(string: href) else { return }
let url = URL(string: href) else { return }
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
case .hashtag(let hashtag, _):
case .hashtag(_, let hashtag, _):
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
default:
case .email, .emoji:
break
}
}
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
}

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import CoreDataStack
import MastodonSDK
import MastodonMeta
// please override this base class
class ProfileViewModel: NSObject {
@ -40,7 +41,7 @@ class ProfileViewModel: NSObject {
let followingCount: CurrentValueSubject<Int?, Never>
let followersCount: CurrentValueSubject<Int?, Never>
let fields: CurrentValueSubject<[Mastodon.Entity.Field], Never>
let emojiDict: CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>
let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
// fulfill this before editing
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
@ -83,7 +84,7 @@ class ProfileViewModel: NSObject {
self.protected = CurrentValueSubject(mastodonUser?.locked)
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:])
self.emojiMeta = CurrentValueSubject(mastodonUser?.emojiMeta ?? [:])
super.init()
relationshipActionOptionSet
@ -258,7 +259,7 @@ extension ProfileViewModel {
self.protected.value = mastodonUser?.locked
self.suspended.value = mastodonUser?.suspended ?? false
self.fields.value = mastodonUser?.fields ?? []
self.emojiDict.value = mastodonUser?.emojiDict ?? [:]
self.emojiMeta.value = mastodonUser?.emojiMeta ?? [:]
}
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {

View File

@ -11,7 +11,6 @@ import AVKit
import Combine
import CoreData
import CoreDataStack
import ActiveLabel
import Meta
import MetaTextKit
@ -212,9 +211,6 @@ extension ReportedStatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
}
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
}
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
}

View File

@ -10,7 +10,8 @@ import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import ActiveLabel
import MetaTextKit
import MastodonMeta
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
func followButtonDidPressed(clickedUser: MastodonUser)
@ -43,14 +44,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
let displayNameLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusName)
label.textColor = .white
label.textAlignment = .center
label.font = .systemFont(ofSize: 18, weight: .semibold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let displayNameLabel = MetaLabel(style: .recommendAccountName)
let acctLabel: UILabel = {
let label = UILabel()
@ -165,7 +159,14 @@ extension SearchRecommendAccountsCollectionViewCell {
}
func config(with mastodonUser: MastodonUser) {
displayNameLabel.configure(content: mastodonUser.displayNameWithFallback, emojiDict: mastodonUser.emojiDict)
do {
let mastodonContent = MastodonContent(content: mastodonUser.displayNameWithFallback, emojis: mastodonUser.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
displayNameLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: mastodonUser.displayNameWithFallback)
displayNameLabel.configure(content: metaContent)
}
acctLabel.text = "@" + mastodonUser.acct
avatarImageView.af.setImage(
withURL: URL(string: mastodonUser.avatar)!,

View File

@ -11,6 +11,8 @@ import Foundation
import MastodonSDK
import UIKit
import FLAnimatedImage
import MetaTextKit
import MastodonMeta
final class SearchResultTableViewCell: UITableViewCell {
@ -22,13 +24,7 @@ final class SearchResultTableViewCell: UITableViewCell {
return imageView
}()
let _titleLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.brandBlue.color
label.font = .systemFont(ofSize: 17, weight: .semibold)
label.lineBreakMode = .byTruncatingTail
return label
}()
let _titleLabel = MetaLabel(style: .statusName)
let _subTitleLabel: UILabel = {
let label = UILabel()
@ -155,13 +151,28 @@ extension SearchResultTableViewCell {
func config(with account: Mastodon.Entity.Account) {
configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL()))
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
let name = account.displayName.isEmpty ? account.username : account.displayName
do {
let mastodonContent = MastodonContent(content: name, emojis: account.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
_titleLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: name)
_titleLabel.configure(content: metaContent)
}
_subTitleLabel.text = account.acct
}
func config(with account: MastodonUser) {
configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL()))
_titleLabel.text = account.displayNameWithFallback
do {
let mastodonContent = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
_titleLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
_titleLabel.configure(content: metaContent)
}
_subTitleLabel.text = account.acct
}

View File

@ -8,10 +8,11 @@
import os.log
import UIKit
import Combine
import ActiveLabel
import CoreData
import CoreDataStack
import MastodonSDK
import MetaTextKit
import MastodonMeta
import AuthenticationServices
class SettingsViewController: UIViewController, NeedsDependency {
@ -103,12 +104,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
return tableView
}()
let tableFooterActiveLabel: ActiveLabel = {
let label = ActiveLabel(style: .default)
label.adjustsFontForContentSizeCategory = true
label.textAlignment = .center
return label
}()
let tableFooterLabel = MetaLabel(style: .settingTableFooter)
lazy var tableFooterView: UIView = {
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Height' UIStackView:0x7ffe41e47da0.height == 0)
let view = UIStackView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))
@ -117,8 +113,8 @@ class SettingsViewController: UIViewController, NeedsDependency {
view.axis = .vertical
view.alignment = .center
tableFooterActiveLabel.delegate = self
view.addArrangedSubview(tableFooterActiveLabel)
tableFooterLabel.linkDelegate = self
view.addArrangedSubview(tableFooterLabel)
return view
}()
@ -199,7 +195,15 @@ class SettingsViewController: UIViewController, NeedsDependency {
let version = instance?.version ?? "-"
let link = #"<a href="https://github.com/mastodon/mastodon">mastodon/mastodon</a>"#
let content = L10n.Scene.Settings.Footer.mastodonDescription(link, version)
self.tableFooterActiveLabel.configure(content: content, emojiDict: [:])
let mastodonContent = MastodonContent(content: content, emojis: [:])
do {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
self.tableFooterLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: "")
self.tableFooterLabel.configure(content: metaContent)
assertionFailure()
}
}
.store(in: &disposeBag)
}
@ -510,13 +514,16 @@ extension SettingsViewController: SettingsToggleCellDelegate {
}
}
extension SettingsViewController: ActiveLabelDelegate {
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
coordinator.present(
scene: .safari(url: URL(string: "https://github.com/mastodon/mastodon")!),
from: self,
transition: .safariPresent(animated: true, completion: nil)
)
// MARK: - MetaLabelDelegate
extension SettingsViewController: MetaLabelDelegate {
func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
switch meta {
case .url(_, _, let url, _):
guard let url = URL(string: url) else { return }
coordinator.present(scene: .safari(url: url), from: self, transition: .safariPresent(animated: true, completion: nil))
default:
assertionFailure()
}
}
}

View File

@ -6,19 +6,14 @@
//
import UIKit
import ActiveLabel
import Meta
import MetaTextKit
final class DoubleTitleLabelNavigationBarTitleView: UIView {
let containerView = UIStackView()
let titleLabel: ActiveLabel = {
let label = ActiveLabel(style: .default)
label.font = .systemFont(ofSize: 17, weight: .semibold)
label.textColor = Asset.Colors.Label.primary.color
label.textAlignment = .center
return label
}()
let titleLabel = MetaLabel(style: .titleView)
let subtitleLabel: UILabel = {
let label = UILabel()
@ -58,9 +53,18 @@ extension DoubleTitleLabelNavigationBarTitleView {
containerView.addArrangedSubview(titleLabel)
containerView.addArrangedSubview(subtitleLabel)
}
func update(title: String, subtitle: String?) {
titleLabel.configure(content: PlaintextMetaContent(string: title))
update(subtitle: subtitle)
}
func update(title: String, subtitle: String?, emojiDict: MastodonStatusContent.EmojiDict) {
titleLabel.configure(content: title, emojiDict: emojiDict)
func update(titleMetaContent: MetaContent, subtitle: String?) {
titleLabel.configure(content: titleMetaContent)
update(subtitle: subtitle)
}
func update(subtitle: String?) {
if let subtitle = subtitle {
subtitleLabel.text = subtitle
subtitleLabel.isHidden = false

View File

@ -9,7 +9,6 @@ import os.log
import UIKit
import Combine
import AVKit
import ActiveLabel
import AlamofireImage
import FLAnimatedImage
import MetaTextKit
@ -26,7 +25,6 @@ protocol StatusViewDelegate: AnyObject {
func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
}
@ -215,7 +213,7 @@ final class StatusView: UIView {
metaText.textView.layer.masksToBounds = false
metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
let paragraphStyle: NSMutableParagraphStyle = {
metaText.paragraphStyle = {
let style = NSMutableParagraphStyle()
style.lineSpacing = 5
style.paragraphSpacing = 8
@ -224,12 +222,10 @@ final class StatusView: UIView {
metaText.textAttributes = [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
.foregroundColor: Asset.Colors.Label.primary.color,
.paragraphStyle: paragraphStyle,
]
metaText.linkAttributes = [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
.foregroundColor: Asset.Colors.brandBlue.color,
.paragraphStyle: paragraphStyle,
]
return metaText
}()
@ -559,11 +555,10 @@ extension StatusView {
// MARK: - MetaTextViewDelegate
extension StatusView: MetaTextViewDelegate {
func metaTextView(_ metaTextView: MetaTextView, didSelectLink link: URL) {
func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
switch metaTextView {
case contentMetaText.textView:
guard let meta = Meta(url: link) else { return }
delegate?.statusView(self, metaText: contentMetaText, didSelectMeta: meta)
default:
assertionFailure()
@ -596,14 +591,6 @@ extension StatusView: UITextViewDelegate {
}
}
// MARK: - ActiveLabelDelegate
extension StatusView: ActiveLabelDelegate {
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
delegate?.statusView(self, activeLabel: activeLabel, didSelectActiveEntity: entity)
}
}
// MARK: - ContentWarningOverlayViewDelegate
extension StatusView: ContentWarningOverlayViewDelegate {
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {

View File

@ -11,11 +11,10 @@ import UIKit
import Combine
import AsyncDisplayKit
import CoreDataStack
import ActiveLabel
import func AVFoundation.AVMakeRect
protocol StatusNodeDelegate: AnyObject {
func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType)
//func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType)
}
final class StatusNode: ASCellNode {
@ -29,21 +28,21 @@ final class StatusNode: ASCellNode {
static let avatarImageSize = CGSize(width: 42, height: 42)
static let avatarImageCornerRadius: CGFloat = 4
static let statusContentAppearance: MastodonStatusContent.Appearance = {
let linkAttributes: [NSAttributedString.Key: Any] = [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
.foregroundColor: Asset.Colors.brandBlue.color
]
return MastodonStatusContent.Appearance(
attributes: [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
.foregroundColor: Asset.Colors.Label.primary.color
],
urlAttributes: linkAttributes,
hashtagAttributes: linkAttributes,
mentionAttributes: linkAttributes
)
}()
// static let statusContentAppearance: MastodonStatusContent.Appearance = {
// let linkAttributes: [NSAttributedString.Key: Any] = [
// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
// .foregroundColor: Asset.Colors.brandBlue.color
// ]
// return MastodonStatusContent.Appearance(
// attributes: [
// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
// .foregroundColor: Asset.Colors.Label.primary.color
// ],
// urlAttributes: linkAttributes,
// hashtagAttributes: linkAttributes,
// mentionAttributes: linkAttributes
// )
// }()
let avatarImageNode: ASNetworkImageNode = {
let node = ASNetworkImageNode()
@ -112,13 +111,14 @@ final class StatusNode: ASCellNode {
.font: UIFont.systemFont(ofSize: 15, weight: .regular)
])
statusContentTextNode.metaEditableTextNodeDelegate = self
if let parseResult = try? MastodonStatusContent.parse(
content: (status.reblog ?? status).content,
emojiDict: (status.reblog ?? status).emojiDict
) {
statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance)
}
// FIXME:
// statusContentTextNode.metaEditableTextNodeDelegate = self
// if let parseResult = try? MastodonStatusContent.parse(
// content: (status.reblog ?? status).content,
// emojiDict: (status.reblog ?? status).emojiDict
// ) {
// statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance)
// }
for imageNode in mediaMultiplexImageNodes {
imageNode.dataSource = self
@ -200,29 +200,19 @@ final class StatusNode: ASCellNode {
}
//extension StatusNode: ASImageDownloaderProtocol {
// func downloadImage(with URL: URL, callbackQueue: DispatchQueue, downloadProgress: ASImageDownloaderProgress?, completion: @escaping ASImageDownloaderCompletion) -> Any? {
//
// }
//
// func cancelImageDownload(forIdentifier downloadIdentifier: Any) {
//
// MARK: - ASEditableTextNodeDelegate
//extension StatusNode: ASMetaEditableTextNodeDelegate {
// func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// guard let activityEntityType = ActiveEntityType(url: URL) else {
// return false
// }
// defer {
// delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType)
// }
// return false
// }
//}
// MARK: - ASEditableTextNodeDelegate
extension StatusNode: ASMetaEditableTextNodeDelegate {
func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
guard let activityEntityType = ActiveEntityType(url: URL) else {
return false
}
defer {
delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType)
}
return false
}
}
// MARK: - ASMultiplexImageNodeDataSource
extension StatusNode: ASMultiplexImageNodeDataSource {
func multiplexImageNode(_ imageNode: ASMultiplexImageNode, urlForImageIdentifier imageIdentifier: ASImageIdentifier) -> URL? {

View File

@ -11,7 +11,6 @@ import AVKit
import Combine
import CoreData
import CoreDataStack
import ActiveLabel
import Meta
import MetaTextKit
@ -27,7 +26,6 @@ protocol StatusTableViewCellDelegate: AnyObject {
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
@ -328,10 +326,6 @@ extension StatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
}
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
delegate?.statusTableViewCell(self, statusView: statusView, activeLabel: activeLabel, didSelectActiveEntity: entity)
}
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
delegate?.statusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)

View File

@ -11,7 +11,8 @@ import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import ActiveLabel
import MetaTextKit
import MastodonMeta
protocol SuggestionAccountTableViewCellDelegate: AnyObject {
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell)
@ -29,13 +30,7 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
return imageView
}()
let titleLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusName)
label.textColor = Asset.Colors.brandBlue.color
label.font = .systemFont(ofSize: 17, weight: .semibold)
label.lineBreakMode = .byTruncatingTail
return label
}()
let titleLabel = MetaLabel(style: .statusName)
let subTitleLabel: UILabel = {
let label = UILabel()
@ -152,8 +147,15 @@ extension SuggestionAccountTableViewCell {
imageTransition: .crossDissolve(0.2)
)
}
titleLabel.configure(content: account.displayNameWithFallback, emojiDict: account.emojiDict)
subTitleLabel.text = account.acct
let mastodonContent = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta)
do {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
titleLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
titleLabel.configure(content: metaContent)
}
subTitleLabel.text = "@" + account.acct
button.isSelected = isSelected
button.publisher(for: .touchUpInside)
.sink { [weak self] _ in

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import CoreData
import AVKit
import MastodonMeta
final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -84,18 +85,27 @@ extension ThreadViewController {
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
viewModel.navigationBarTitle
.receive(on: DispatchQueue.main)
.sink { [weak self] tuple in
guard let self = self else { return }
guard let (title, emojiDict) = tuple else {
self.titleView.update(title: L10n.Scene.Thread.backTitle, subtitle: nil, emojiDict: [:])
return
}
self.titleView.update(title: title, subtitle: nil, emojiDict: emojiDict)
Publishers.CombineLatest(
viewModel.navigationBarTitle,
viewModel.navigationBarTitleEmojiMeta
)
.receive(on: DispatchQueue.main)
.sink { [weak self] title, emojiMeta in
guard let self = self else { return }
guard let title = title else {
self.titleView.update(title: "", subtitle: nil)
return
}
.store(in: &disposeBag)
let mastodonContent = MastodonContent(content: title, emojis: emojiMeta ?? [:])
do {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
self.titleView.update(titleMetaContent: metaContent, subtitle: nil)
} catch {
assertionFailure()
}
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {

View File

@ -12,6 +12,7 @@ import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import MastodonMeta
class ThreadViewModel {
@ -45,7 +46,8 @@ class ThreadViewModel {
let ancestorItems = CurrentValueSubject<[Item], Never>([])
let descendantNodes = CurrentValueSubject<[LeafNode], Never>([])
let descendantItems = CurrentValueSubject<[Item], Never>([])
let navigationBarTitle: CurrentValueSubject<(String, MastodonStatusContent.EmojiDict)?, Never>
let navigationBarTitle: CurrentValueSubject<String?, Never>
let navigationBarTitleEmojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
init(context: AppContext, optionalStatus: Status?) {
self.context = context
@ -53,8 +55,8 @@ class ThreadViewModel {
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
self.navigationBarTitle = CurrentValueSubject(
optionalStatus.flatMap { (L10n.Scene.Thread.title($0.author.displayNameWithFallback), $0.author.emojiDict) }
)
optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) })
self.navigationBarTitleEmojiMeta = CurrentValueSubject(optionalStatus.flatMap { $0.author.emojiMeta } ?? [:])
// bind fetcher domain
context.authenticationService.activeMastodonAuthenticationBox
@ -85,7 +87,8 @@ class ThreadViewModel {
return
}
self.rootNode.value = RootNode(domain: status.domain, statusID: status.id, replyToID: status.inReplyToID)
self.navigationBarTitle.value = (L10n.Scene.Thread.title(status.author.displayNameWithFallback), status.author.emojiDict)
self.navigationBarTitle.value = L10n.Scene.Thread.title(status.author.displayNameWithFallback)
self.navigationBarTitleEmojiMeta.value = status.author.emojiMeta ?? [:]
}
}
.store(in: &disposeBag)

View File

@ -1,77 +0,0 @@
//
// StatusContentCacheService.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-6-17.
//
import UIKit
import Combine
final class StatusContentCacheService {
var disposeBag = Set<AnyCancellable>()
let cache = NSCache<Key, ParseResultWrapper>()
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent)
func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> MastodonStatusContent.ParseResult? {
let key = Key(content: content, emojiDict: emojiDict)
return cache.object(forKey: key)?.parseResult
}
func prefetch(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
let key = Key(content: content, emojiDict: emojiDict)
guard cache.object(forKey: key) == nil else { return }
MastodonStatusContent.parseResult(content: content, emojiDict: emojiDict)
.sink { [weak self] parseResult in
guard let self = self else { return }
guard let parseResult = parseResult else { return }
let wrapper = ParseResultWrapper(parseResult: parseResult)
self.cache.setObject(wrapper, forKey: key)
}
.store(in: &disposeBag)
}
}
extension StatusContentCacheService {
class Key: NSObject {
let content: String
let emojiDict: MastodonStatusContent.EmojiDict
init(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
self.content = content
self.emojiDict = emojiDict
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Key else { return false }
return object.content == content
&& object.emojiDict == emojiDict
}
override var hash: Int {
return content.hashValue ^
emojiDict.hashValue
}
}
class ParseResultWrapper: NSObject {
let parseResult: MastodonStatusContent.ParseResult
init(parseResult: MastodonStatusContent.ParseResult) {
self.parseResult = parseResult
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? ParseResultWrapper else { return false }
return object.parseResult == parseResult
}
override var hash: Int {
return parseResult.hashValue
}
}
}

View File

@ -38,7 +38,6 @@ class AppContext: ObservableObject {
let placeholderImageCacheService = PlaceholderImageCacheService()
let blurhashImageCacheService = BlurhashImageCacheService()
let statusContentCacheService = StatusContentCacheService()
let documentStore: DocumentStore
private var documentStoreSubscription: AnyCancellable!