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

View File

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

View File

@ -1,15 +1,6 @@
{ {
"object": { "object": {
"pins": [ "pins": [
{
"package": "ActiveLabel",
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
"state": {
"branch": null,
"revision": "d503eb3bfabc54a70139618ab2ba09ebb8c09672",
"version": "5.0.3"
}
},
{ {
"package": "Alamofire", "package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git", "repositoryURL": "https://github.com/Alamofire/Alamofire.git",
@ -100,24 +91,6 @@
"version": "4.2.2" "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", "package": "Nuke",
"repositoryURL": "https://github.com/kean/Nuke.git", "repositoryURL": "https://github.com/kean/Nuke.git",

View File

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

View File

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

View File

@ -45,12 +45,19 @@ extension ComposeStatusSection {
// set display name and username // set display name and username
Publishers.CombineLatest3( Publishers.CombineLatest3(
attribute.displayName, attribute.displayName,
attribute.emojiDict, attribute.emojiMeta,
attribute.username.eraseToAnyPublisher() attribute.username
) )
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { displayName, emojiDict, username in .sink { displayName, emojiMeta, username in
cell.statusView.nameLabel.configure(content: displayName ?? " ", emojiDict: emojiDict) 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 } ?? " " cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)

View File

@ -25,7 +25,12 @@ extension CustomEmojiPickerSection {
.af.imageRounded(withCornerRadius: 4) .af.imageRounded(withCornerRadius: 4)
let url = URL(string: attribute.emoji.url) 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 cell.accessibilityLabel = attribute.emoji.shortcode
return cell return cell
} }

View File

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

View File

@ -11,6 +11,8 @@ import CoreDataStack
import Foundation import Foundation
import MastodonSDK import MastodonSDK
import UIKit import UIKit
import MetaTextKit
import MastodonMeta
enum NotificationSection: Equatable, Hashable { enum NotificationSection: Equatable, Hashable {
case main case main
@ -66,7 +68,14 @@ extension NotificationSection {
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
// configure author name, notification description, timestamp // 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 createAt = notification.createAt
let actionText = notification.notificationType.actionText let actionText = notification.notificationType.actionText
cell.actionLabel.text = actionText + " · " + createAt.timeAgoSinceNow cell.actionLabel.text = actionText + " · " + createAt.timeAgoSinceNow

View File

@ -1,173 +1,66 @@
//extension ActiveEntity {
// //
// ActiveLabel.swift // var accessibilityLabelDescription: String {
// Mastodon // 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 //final class ActiveLabelAccessibilityElement: UIAccessibilityElement {
import Foundation // var index: Int!
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!
}
// MARK: - UIAccessibilityContainer // MARK: - UIAccessibilityContainer
extension ActiveLabel { //extension ActiveLabel {
//
func createAccessibilityElements() -> [UIAccessibilityElement] { // func createAccessibilityElements() -> [UIAccessibilityElement] {
var elements: [UIAccessibilityElement] = [] // var elements: [UIAccessibilityElement] = []
//
let element = ActiveLabelAccessibilityElement(accessibilityContainer: self) // let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
element.accessibilityTraits = .staticText // element.accessibilityTraits = .staticText
element.accessibilityLabel = accessibilityLabel // element.accessibilityLabel = accessibilityLabel
element.accessibilityFrame = superview!.convert(frame, to: nil) // element.accessibilityFrame = superview!.convert(frame, to: nil)
element.accessibilityLanguage = accessibilityLanguage // element.accessibilityLanguage = accessibilityLanguage
elements.append(element) // elements.append(element)
//
for entity in activeEntities { // for entity in activeEntities {
guard let element = entity.accessibilityElement(in: self) else { continue } // guard let element = entity.accessibilityElement(in: self) else { continue }
var glyphRange = NSRange() // var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange) // layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange)
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) // let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
element.accessibilityFrame = self.convert(rect, to: nil) // element.accessibilityFrame = self.convert(rect, to: nil)
element.accessibilityContainer = self // element.accessibilityContainer = self
elements.append(element) // elements.append(element)
} // }
//
return elements // return elements
} // }
//
} //}

View File

@ -23,15 +23,6 @@ extension EmojiContainer {
let decoder = JSONDecoder() let decoder = JSONDecoder()
return emojisData.flatMap { try? decoder.decode([Mastodon.Entity.Emoji].self, from: $0) } 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 emojiMeta: MastodonContent.Emojis {
var dict = MastodonContent.Emojis() var dict = MastodonContent.Emojis()

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
import MastodonSDK import MastodonSDK
import MastodonMeta
extension Mastodon.Entity.Account: Hashable { extension Mastodon.Entity.Account: Hashable {
public func hash(into hasher: inout Hasher) { 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")! 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 UIKit
import Meta
import MetaTextKit import MetaTextKit
extension MetaLabel { extension MetaLabel {
enum Style { enum Style {
case statusHeader case statusHeader
case statusName case statusName
// case profileFieldName case notificationName
// case profileFieldValue case profileFieldName
case profileFieldValue
case recommendAccountName
case titleView
case settingTableFooter
} }
convenience init(style: Style) { convenience init(style: Style) {
@ -33,14 +38,37 @@ extension MetaLabel {
case .statusName: case .statusName:
font = .systemFont(ofSize: 17, weight: .semibold) font = .systemFont(ofSize: 17, weight: .semibold)
textColor = Asset.Colors.Label.primary.color textColor = Asset.Colors.Label.primary.color
// case .profileFieldName:
// font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) case .notificationName:
// textColor = Asset.Colors.Label.primary.color font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
// numberOfLines = 1 textColor = Asset.Colors.brandBlue.color
// case .profileFieldValue:
// font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) case .profileFieldName:
// textColor = Asset.Colors.Label.primary.color font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20)
// numberOfLines = 1 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 self.font = font
@ -50,4 +78,23 @@ extension MetaLabel {
.font: font, .font: font,
.foregroundColor: textColor .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 #if ASDK
import Foundation import Foundation
import ActiveLabel
// MARK: - StatusViewDelegate // MARK: - StatusViewDelegate
extension StatusNodeDelegate where Self: StatusProvider { extension StatusNodeDelegate where Self: StatusProvider {
func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) {
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, node: node, didSelectActiveEntityType: type)
}
} }
#endif #endif

View File

@ -11,7 +11,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import ActiveLabel
import Meta import Meta
import MetaTextKit import MetaTextKit
@ -25,10 +24,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) {
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell) 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) { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta) StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta)

View File

@ -11,7 +11,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import ActiveLabel
import Meta import Meta
import MetaTextKit import MetaTextKit
@ -125,35 +124,6 @@ extension StatusProviderFacade {
} }
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) { static func responseToStatusMetaTextAction(provider: StatusProvider, cell: UITableViewCell, metaText: MetaText, didSelectMeta meta: Meta) {
switch meta { switch meta {
@ -185,31 +155,6 @@ extension StatusProviderFacade {
} }
#if ASDK #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) { private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, node: ASCellNode, mention: String) {
guard let status = provider.status(node: node, indexPath: nil) else { return } guard let status = provider.status(node: node, indexPath: nil) else { return }
coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention, href: nil) coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention, href: nil)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,6 @@ import Combine
import Foundation import Foundation
import CoreDataStack import CoreDataStack
import UIKit import UIKit
import ActiveLabel
import MetaTextKit import MetaTextKit
import Meta import Meta
import FLAnimatedImage import FLAnimatedImage
@ -20,7 +19,7 @@ protocol NotificationTableViewCellDelegate: AnyObject {
func parent() -> UIViewController func parent() -> UIViewController
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, avatarImageViewDidPressed imageView: UIImageView) 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, revealContentWarningButtonDidPressed button: UIButton)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
@ -58,13 +57,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
return label return label
}() }()
let nameLabel: ActiveLabel = { let nameLabel = MetaLabel(style: .notificationName)
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 buttonStackView = UIStackView() let buttonStackView = UIStackView()
@ -318,10 +311,6 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
// do nothing // do nothing
} }
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
// do nothing
}
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) { func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
delegate?.notificationStatusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta) delegate?.notificationStatusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)

View File

@ -57,7 +57,7 @@ extension FavoriteViewController {
.store(in: &disposeBag) .store(in: &disposeBag)
navigationItem.titleView = titleView 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 tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView) view.addSubview(tableView)

View File

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

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
import Kanna import Kanna
import MastodonSDK import MastodonSDK
import MastodonMeta
final class ProfileHeaderViewModel { final class ProfileHeaderViewModel {
@ -24,7 +25,7 @@ final class ProfileHeaderViewModel {
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true) let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false) let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false)
let isTitleViewContentOffsetSet = 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) let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
// output // output
@ -58,10 +59,10 @@ final class ProfileHeaderViewModel {
isEditing.removeDuplicates(), isEditing.removeDuplicates(),
displayProfileInfo.fields.removeDuplicates(), displayProfileInfo.fields.removeDuplicates(),
editProfileInfo.fields.removeDuplicates(), editProfileInfo.fields.removeDuplicates(),
emojiDict.removeDuplicates() emojiMeta.removeDuplicates()
) )
.receive(on: RunLoop.main) .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 self = self else { return }
guard let diffableDataSource = self.fieldDiffableDataSource else { return } guard let diffableDataSource = self.fieldDiffableDataSource else { return }
@ -87,7 +88,7 @@ final class ProfileHeaderViewModel {
let attribute = oldFieldAttributeDict[field.id] ?? ProfileFieldItem.FieldItemAttribute() let attribute = oldFieldAttributeDict[field.id] ?? ProfileFieldItem.FieldItemAttribute()
attribute.isEditing = isEditing attribute.isEditing = isEditing
attribute.emojiDict.value = emojiDict attribute.emojiMeta.value = emojiMeta
attribute.isLast = false attribute.isLast = false
return ProfileFieldItem.field(field: field, attribute: attribute) 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), bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
]) ])
fieldView.titleActiveLabel.isHidden = false fieldView.titleMetaLabel.isHidden = false
fieldView.titleActiveLabel.configure(field: L10n.Scene.Profile.Fields.addRow, emojiDict: [:]) fieldView.titleMetaLabel.configure(content: PlaintextMetaContent(string: L10n.Scene.Profile.Fields.addRow))
fieldView.titleTextField.isHidden = true fieldView.titleTextField.isHidden = true
fieldView.valueActiveLabel.isHidden = false fieldView.valueMetaLabel.isHidden = false
fieldView.valueActiveLabel.configure(field: " ", emojiDict: [:]) fieldView.valueMetaLabel.configure(content: PlaintextMetaContent(string: " "))
fieldView.valueTextField.isHidden = true fieldView.valueTextField.isHidden = true
addGestureRecognizer(singleTagGestureRecognizer) addGestureRecognizer(singleTagGestureRecognizer)

View File

@ -8,11 +8,11 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import ActiveLabel import MetaTextKit
protocol ProfileFieldCollectionViewCellDelegate: AnyObject { protocol ProfileFieldCollectionViewCellDelegate: AnyObject {
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) 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 { final class ProfileFieldCollectionViewCell: UICollectionViewCell {
@ -107,7 +107,7 @@ extension ProfileFieldCollectionViewCell {
editButton.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.editButtonDidPressed(_:)), for: .touchUpInside) editButton.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.editButtonDidPressed(_:)), for: .touchUpInside)
fieldView.valueActiveLabel.delegate = self fieldView.valueMetaLabel.linkDelegate = self
resetSeparatorLineLayout() resetSeparatorLineLayout()
} }
@ -153,13 +153,11 @@ extension ProfileFieldCollectionViewCell {
} }
} }
// MARK: - MetaLabelDelegate
extension ProfileFieldCollectionViewCell: MetaLabelDelegate {
// MARK: - ActiveLabelDelegate func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
extension ProfileFieldCollectionViewCell: ActiveLabelDelegate {
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) 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 UIKit
import Combine import Combine
import ActiveLabel import MetaTextKit
final class ProfileFieldView: UIView { final class ProfileFieldView: UIView {
@ -18,11 +18,7 @@ final class ProfileFieldView: UIView {
let value = PassthroughSubject<String, Never>() let value = PassthroughSubject<String, Never>()
// for custom emoji display // for custom emoji display
let titleActiveLabel: ActiveLabel = { let titleMetaLabel = MetaLabel(style: .profileFieldName)
let label = ActiveLabel(style: .profileFieldName)
label.configure(content: "title", emojiDict: [:])
return label
}()
// for editing // for editing
let titleTextField: UITextField = { let titleTextField: UITextField = {
@ -34,12 +30,7 @@ final class ProfileFieldView: UIView {
}() }()
// for custom emoji display // for custom emoji display
let valueActiveLabel: ActiveLabel = { let valueMetaLabel = MetaLabel(style: .profileFieldValue)
let label = ActiveLabel(style: .profileFieldValue)
label.configure(content: "value", emojiDict: [:])
label.textAlignment = .right
return label
}()
// for editing // for editing
let valueTextField: UITextField = { let valueTextField: UITextField = {
@ -81,10 +72,10 @@ extension ProfileFieldView {
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
titleActiveLabel.translatesAutoresizingMaskIntoConstraints = false titleMetaLabel.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(titleActiveLabel) containerStackView.addArrangedSubview(titleMetaLabel)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
titleActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), titleMetaLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
]) ])
titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
titleTextField.translatesAutoresizingMaskIntoConstraints = false titleTextField.translatesAutoresizingMaskIntoConstraints = false
@ -94,12 +85,12 @@ extension ProfileFieldView {
]) ])
titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false valueMetaLabel.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(valueActiveLabel) containerStackView.addArrangedSubview(valueMetaLabel)
NSLayoutConstraint.activate([ 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 valueTextField.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(valueTextField) containerStackView.addArrangedSubview(valueTextField)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -137,7 +128,8 @@ struct ProfileFieldView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
UIViewPreview(width: 375) { UIViewPreview(width: 375) {
let filedView = ProfileFieldView() 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 return filedView
} }
.previewLayout(.fixed(width: 375, height: 100)) .previewLayout(.fixed(width: 375, height: 100))

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import AVKit
import Combine import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import ActiveLabel
import Meta import Meta
import MetaTextKit import MetaTextKit
@ -212,9 +211,6 @@ extension ReportedStatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { 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) { func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
} }

View File

@ -10,7 +10,8 @@ import CoreDataStack
import Foundation import Foundation
import MastodonSDK import MastodonSDK
import UIKit import UIKit
import ActiveLabel import MetaTextKit
import MastodonMeta
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
func followButtonDidPressed(clickedUser: MastodonUser) func followButtonDidPressed(clickedUser: MastodonUser)
@ -43,14 +44,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
let displayNameLabel: ActiveLabel = { let displayNameLabel = MetaLabel(style: .recommendAccountName)
let label = ActiveLabel(style: .statusName)
label.textColor = .white
label.textAlignment = .center
label.font = .systemFont(ofSize: 18, weight: .semibold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let acctLabel: UILabel = { let acctLabel: UILabel = {
let label = UILabel() let label = UILabel()
@ -165,7 +159,14 @@ extension SearchRecommendAccountsCollectionViewCell {
} }
func config(with mastodonUser: MastodonUser) { 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 acctLabel.text = "@" + mastodonUser.acct
avatarImageView.af.setImage( avatarImageView.af.setImage(
withURL: URL(string: mastodonUser.avatar)!, withURL: URL(string: mastodonUser.avatar)!,

View File

@ -11,6 +11,8 @@ import Foundation
import MastodonSDK import MastodonSDK
import UIKit import UIKit
import FLAnimatedImage import FLAnimatedImage
import MetaTextKit
import MastodonMeta
final class SearchResultTableViewCell: UITableViewCell { final class SearchResultTableViewCell: UITableViewCell {
@ -22,13 +24,7 @@ final class SearchResultTableViewCell: UITableViewCell {
return imageView return imageView
}() }()
let _titleLabel: UILabel = { let _titleLabel = MetaLabel(style: .statusName)
let label = UILabel()
label.textColor = Asset.Colors.brandBlue.color
label.font = .systemFont(ofSize: 17, weight: .semibold)
label.lineBreakMode = .byTruncatingTail
return label
}()
let _subTitleLabel: UILabel = { let _subTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
@ -155,13 +151,28 @@ extension SearchResultTableViewCell {
func config(with account: Mastodon.Entity.Account) { func config(with account: Mastodon.Entity.Account) {
configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL())) 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 _subTitleLabel.text = account.acct
} }
func config(with account: MastodonUser) { func config(with account: MastodonUser) {
configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL())) 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 _subTitleLabel.text = account.acct
} }

View File

@ -8,10 +8,11 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import ActiveLabel
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MetaTextKit
import MastodonMeta
import AuthenticationServices import AuthenticationServices
class SettingsViewController: UIViewController, NeedsDependency { class SettingsViewController: UIViewController, NeedsDependency {
@ -103,12 +104,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
return tableView return tableView
}() }()
let tableFooterActiveLabel: ActiveLabel = { let tableFooterLabel = MetaLabel(style: .settingTableFooter)
let label = ActiveLabel(style: .default)
label.adjustsFontForContentSizeCategory = true
label.textAlignment = .center
return label
}()
lazy var tableFooterView: UIView = { lazy var tableFooterView: UIView = {
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Height' UIStackView:0x7ffe41e47da0.height == 0) // 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)) 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.axis = .vertical
view.alignment = .center view.alignment = .center
tableFooterActiveLabel.delegate = self tableFooterLabel.linkDelegate = self
view.addArrangedSubview(tableFooterActiveLabel) view.addArrangedSubview(tableFooterLabel)
return view return view
}() }()
@ -199,7 +195,15 @@ class SettingsViewController: UIViewController, NeedsDependency {
let version = instance?.version ?? "-" let version = instance?.version ?? "-"
let link = #"<a href="https://github.com/mastodon/mastodon">mastodon/mastodon</a>"# let link = #"<a href="https://github.com/mastodon/mastodon">mastodon/mastodon</a>"#
let content = L10n.Scene.Settings.Footer.mastodonDescription(link, version) 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) .store(in: &disposeBag)
} }
@ -510,13 +514,16 @@ extension SettingsViewController: SettingsToggleCellDelegate {
} }
} }
extension SettingsViewController: ActiveLabelDelegate { // MARK: - MetaLabelDelegate
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { extension SettingsViewController: MetaLabelDelegate {
coordinator.present( func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
scene: .safari(url: URL(string: "https://github.com/mastodon/mastodon")!), switch meta {
from: self, case .url(_, _, let url, _):
transition: .safariPresent(animated: true, completion: nil) 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 UIKit
import ActiveLabel import Meta
import MetaTextKit
final class DoubleTitleLabelNavigationBarTitleView: UIView { final class DoubleTitleLabelNavigationBarTitleView: UIView {
let containerView = UIStackView() let containerView = UIStackView()
let titleLabel: ActiveLabel = { let titleLabel = MetaLabel(style: .titleView)
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 subtitleLabel: UILabel = { let subtitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
@ -58,9 +53,18 @@ extension DoubleTitleLabelNavigationBarTitleView {
containerView.addArrangedSubview(titleLabel) containerView.addArrangedSubview(titleLabel)
containerView.addArrangedSubview(subtitleLabel) 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) { func update(titleMetaContent: MetaContent, subtitle: String?) {
titleLabel.configure(content: title, emojiDict: emojiDict) titleLabel.configure(content: titleMetaContent)
update(subtitle: subtitle)
}
func update(subtitle: String?) {
if let subtitle = subtitle { if let subtitle = subtitle {
subtitleLabel.text = subtitle subtitleLabel.text = subtitle
subtitleLabel.isHidden = false subtitleLabel.isHidden = false

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
import CoreData import CoreData
import AVKit import AVKit
import MastodonMeta
final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -84,18 +85,27 @@ extension ThreadViewController {
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])
viewModel.navigationBarTitle Publishers.CombineLatest(
.receive(on: DispatchQueue.main) viewModel.navigationBarTitle,
.sink { [weak self] tuple in viewModel.navigationBarTitleEmojiMeta
guard let self = self else { return } )
guard let (title, emojiDict) = tuple else { .receive(on: DispatchQueue.main)
self.titleView.update(title: L10n.Scene.Thread.backTitle, subtitle: nil, emojiDict: [:]) .sink { [weak self] title, emojiMeta in
return guard let self = self else { return }
} guard let title = title else {
self.titleView.update(title: title, subtitle: nil, emojiDict: emojiDict) 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) { override func viewWillAppear(_ animated: Bool) {

View File

@ -12,6 +12,7 @@ import CoreData
import CoreDataStack import CoreDataStack
import GameplayKit import GameplayKit
import MastodonSDK import MastodonSDK
import MastodonMeta
class ThreadViewModel { class ThreadViewModel {
@ -45,7 +46,8 @@ class ThreadViewModel {
let ancestorItems = CurrentValueSubject<[Item], Never>([]) let ancestorItems = CurrentValueSubject<[Item], Never>([])
let descendantNodes = CurrentValueSubject<[LeafNode], Never>([]) let descendantNodes = CurrentValueSubject<[LeafNode], Never>([])
let descendantItems = CurrentValueSubject<[Item], 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?) { init(context: AppContext, optionalStatus: Status?) {
self.context = context self.context = context
@ -53,8 +55,8 @@ class ThreadViewModel {
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) }) self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil) self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
self.navigationBarTitle = CurrentValueSubject( 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 // bind fetcher domain
context.authenticationService.activeMastodonAuthenticationBox context.authenticationService.activeMastodonAuthenticationBox
@ -85,7 +87,8 @@ class ThreadViewModel {
return return
} }
self.rootNode.value = RootNode(domain: status.domain, statusID: status.id, replyToID: status.inReplyToID) 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) .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 placeholderImageCacheService = PlaceholderImageCacheService()
let blurhashImageCacheService = BlurhashImageCacheService() let blurhashImageCacheService = BlurhashImageCacheService()
let statusContentCacheService = StatusContentCacheService()
let documentStore: DocumentStore let documentStore: DocumentStore
private var documentStoreSubscription: AnyCancellable! private var documentStoreSubscription: AnyCancellable!