diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 7d42a6e8..d4273355 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -65,7 +65,6 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; }; @@ -309,7 +308,6 @@ DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DB6804922637CD8700430867 /* AppName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804912637CD8700430867 /* AppName.swift */; }; DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; - DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; @@ -442,7 +440,6 @@ DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; }; - DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; @@ -479,11 +476,7 @@ DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; }; DBBC24D226A5488600398BB9 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24B426A540AE00398BB9 /* AvatarConfigurableView.swift */; }; DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; }; - DBBC24DD26A54BCB00398BB9 /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */; }; DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */; }; - DBBC24DF26A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */; }; - DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */; }; - DBBC24E126A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; }; DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; }; @@ -930,6 +923,7 @@ DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + DB3F693926AA97BD00C883AB /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MetaTextKit; path = ../MetaTextKit; sourceTree = ""; }; 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 = ""; }; DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -1143,7 +1137,6 @@ DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = ""; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = ""; }; - DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = ""; }; DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = ""; }; DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = ""; }; DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; }; @@ -1168,11 +1161,7 @@ DBBC24C326A544B900398BB9 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeService+Appearance.swift"; sourceTree = ""; }; DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = ""; }; - DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonStatusContent.swift; sourceTree = ""; }; DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = ""; }; - DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+ParseResult.swift"; sourceTree = ""; }; - DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = ""; }; - DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+Appearance.swift"; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = ""; }; DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = ""; }; @@ -1259,7 +1248,6 @@ DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, DBC6462B26A1738900B0E31B /* MastodonUI in Frameworks */, - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */, 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, @@ -1274,7 +1262,6 @@ DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */, 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, - DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */, DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */, DB01E23526A98F0900C3965B /* MetaTextKit in Frameworks */, ); @@ -1600,7 +1587,6 @@ DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */, DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */, - DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */, DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */, ); path = Service; @@ -1974,6 +1960,7 @@ children = ( DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */, DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */, + DB3F693926AA97BD00C883AB /* MetaTextKit */, DB3D0FED25BAA42200EAA174 /* MastodonSDK */, DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, @@ -2771,11 +2758,7 @@ isa = PBXGroup; children = ( DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */, - DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */, DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */, - DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */, - DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */, - DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */, DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */, ); path = Helper; @@ -2988,7 +2971,6 @@ DB3D0FF225BAA61700EAA174 /* AlamofireImage */, 5D526FE125BE9AC400460CB9 /* MastodonSDK */, 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, - 2D42FF6025C8177C004A627A /* ActiveLabel */, DB0140BC25C40D7500F9F3CF /* CommonOSLog */, 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D939AC725EE14620076FA61 /* CropViewController */, @@ -3163,7 +3145,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1240; + LastUpgradeCheck = 1250; TargetAttributes = { DB427DD125BAA00100D1B89D = { CreatedOnToolsVersion = 12.4; @@ -3210,7 +3192,6 @@ packageReferences = ( DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, @@ -3552,7 +3533,6 @@ DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, - DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */, DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, @@ -3712,7 +3692,6 @@ 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */, 5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */, - DBBC24DF26A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift in Sources */, 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */, DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */, @@ -3726,7 +3705,6 @@ DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */, 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */, - DBBC24E126A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift in Sources */, DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */, 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */, DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */, @@ -3786,7 +3764,6 @@ DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, - DBBC24DD26A54BCB00398BB9 /* MastodonStatusContent.swift in Sources */, 2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */, DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */, DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */, @@ -3838,7 +3815,6 @@ DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, - DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */, DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */, DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, @@ -4746,6 +4722,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ASDK; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = "ASDK - Release"; @@ -5307,14 +5284,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; - requirement = { - kind = exactVersion; - version = 5.0.3; - }; - }; 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; @@ -5352,7 +5321,7 @@ repositoryURL = "https://github.com/TwidereProject/MetaTextKit.git"; requirement = { kind = exactVersion; - version = 2.0.0; + version = 2.1.0; }; }; DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = { @@ -5438,11 +5407,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* ActiveLabel */ = { - isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; - productName = ActiveLabel; - }; 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { isa = XCSwiftPackageProductDependency; package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 1220300f..d551bebb 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 24 + 25 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -22,7 +22,7 @@ Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 4 + 3 Mastodon - Release.xcscheme_^#shared#^_ @@ -37,12 +37,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 22 + 23 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 23 + 24 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 45f1697c..56f675a6 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "ActiveLabel", - "repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift", - "state": { - "branch": null, - "revision": "d503eb3bfabc54a70139618ab2ba09ebb8c09672", - "version": "5.0.3" - } - }, { "package": "Alamofire", "repositoryURL": "https://github.com/Alamofire/Alamofire.git", @@ -100,24 +91,6 @@ "version": "4.2.2" } }, - { - "package": "Kingfisher", - "repositoryURL": "https://github.com/onevcat/Kingfisher.git", - "state": { - "branch": null, - "revision": "44450a8f564d7c0165f736ba2250649ff8d3e556", - "version": "6.3.0" - } - }, - { - "package": "MetaTextKit", - "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", - "state": { - "branch": null, - "revision": "44fc5111269d9862369348870835e17907062115", - "version": "2.0.0" - } - }, { "package": "Nuke", "repositoryURL": "https://github.com/kean/Nuke.git", diff --git a/Mastodon/Diffiable/Item/ComposeStatusItem.swift b/Mastodon/Diffiable/Item/ComposeStatusItem.swift index 7c916b16..c2c3f46d 100644 --- a/Mastodon/Diffiable/Item/ComposeStatusItem.swift +++ b/Mastodon/Diffiable/Item/ComposeStatusItem.swift @@ -8,6 +8,7 @@ import Foundation import Combine import CoreData +import MastodonMeta /// Note: update Equatable when change case enum ComposeStatusItem { @@ -25,7 +26,7 @@ extension ComposeStatusItem { let avatarURL = CurrentValueSubject(nil) let displayName = CurrentValueSubject(nil) - let emojiDict = CurrentValueSubject([:]) + let emojiMeta = CurrentValueSubject([:]) let username = CurrentValueSubject(nil) let composeContent = CurrentValueSubject(nil) @@ -35,7 +36,7 @@ extension ComposeStatusItem { static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool { return lhs.avatarURL.value == rhs.avatarURL.value && lhs.displayName.value == rhs.displayName.value && - lhs.emojiDict.value == rhs.emojiDict.value && + lhs.emojiMeta.value == rhs.emojiMeta.value && lhs.username.value == rhs.username.value && lhs.composeContent.value == rhs.composeContent.value && lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value && diff --git a/Mastodon/Diffiable/Item/ProfileFieldItem.swift b/Mastodon/Diffiable/Item/ProfileFieldItem.swift index 4c6b37da..781da185 100644 --- a/Mastodon/Diffiable/Item/ProfileFieldItem.swift +++ b/Mastodon/Diffiable/Item/ProfileFieldItem.swift @@ -8,6 +8,7 @@ import Foundation import Combine import MastodonSDK +import MastodonMeta enum ProfileFieldItem { case field(field: FieldValue, attribute: FieldItemAttribute) @@ -56,7 +57,7 @@ extension ProfileFieldItem { extension ProfileFieldItem { class FieldItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable { - let emojiDict = CurrentValueSubject([:]) + let emojiMeta = CurrentValueSubject([:]) var isEditing = false var isLast = false diff --git a/Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift index bacc5097..45b0656f 100644 --- a/Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift @@ -45,12 +45,19 @@ extension ComposeStatusSection { // set display name and username Publishers.CombineLatest3( attribute.displayName, - attribute.emojiDict, - attribute.username.eraseToAnyPublisher() + attribute.emojiMeta, + attribute.username ) .receive(on: DispatchQueue.main) - .sink { displayName, emojiDict, username in - cell.statusView.nameLabel.configure(content: displayName ?? " ", emojiDict: emojiDict) + .sink { displayName, emojiMeta, username in + do { + let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.statusView.nameLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: " ") + cell.statusView.nameLabel.configure(content: metaContent) + } cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " " } .store(in: &cell.disposeBag) diff --git a/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift index 70de18c6..251b52f3 100644 --- a/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift +++ b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift @@ -25,7 +25,12 @@ extension CustomEmojiPickerSection { .af.imageRounded(withCornerRadius: 4) let url = URL(string: attribute.emoji.url) - cell.emojiImageView.setImage(url: url, placeholder: placeholder, scaleToSize: CustomEmojiPickerItemCollectionViewCell.itemSize) + cell.emojiImageView.sd_setImage( + with: url, + placeholderImage: placeholder, + options: [], + context: nil + ) cell.accessibilityLabel = attribute.emoji.shortcode return cell } diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Section/ProfileFieldSection.swift index 1c8546eb..e96a16e9 100644 --- a/Mastodon/Diffiable/Section/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Section/ProfileFieldSection.swift @@ -8,6 +8,7 @@ import os import UIKit import Combine +import MastodonMeta enum ProfileFieldSection: Equatable, Hashable { case main @@ -29,32 +30,60 @@ extension ProfileFieldSection { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell // set key - cell.fieldView.titleActiveLabel.configure(field: field.name.value, emojiDict: attribute.emojiDict.value) + do { + let mastodonContent = MastodonContent(content: field.name.value, emojis: attribute.emojiMeta.value) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.fieldView.titleMetaLabel.configure(content: metaContent) + } catch { + let content = PlaintextMetaContent(string: field.name.value) + cell.fieldView.titleMetaLabel.configure(content: content) + } cell.fieldView.titleTextField.text = field.name.value Publishers.CombineLatest( field.name.removeDuplicates(), - attribute.emojiDict.removeDuplicates() + attribute.emojiMeta.removeDuplicates() ) .receive(on: RunLoop.main) - .sink { [weak cell] name, emojiDict in + .sink { [weak cell] name, emojiMeta in guard let cell = cell else { return } - cell.fieldView.titleActiveLabel.configure(field: name, emojiDict: emojiDict) + do { + let mastodonContent = MastodonContent(content: name, emojis: emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.fieldView.titleMetaLabel.configure(content: metaContent) + } catch { + let content = PlaintextMetaContent(string: name) + cell.fieldView.titleMetaLabel.configure(content: content) + } // only bind label. The text field should only set once } .store(in: &cell.disposeBag) // set value - cell.fieldView.valueActiveLabel.configure(field: field.value.value, emojiDict: attribute.emojiDict.value) + do { + let mastodonContent = MastodonContent(content: field.value.value, emojis: attribute.emojiMeta.value) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.fieldView.valueMetaLabel.configure(content: metaContent) + } catch { + let content = PlaintextMetaContent(string: field.value.value) + cell.fieldView.valueMetaLabel.configure(content: content) + } cell.fieldView.valueTextField.text = field.value.value Publishers.CombineLatest( field.value.removeDuplicates(), - attribute.emojiDict.removeDuplicates() + attribute.emojiMeta.removeDuplicates() ) .receive(on: RunLoop.main) - .sink { [weak cell] value, emojiDict in + .sink { [weak cell] value, emojiMeta in guard let cell = cell else { return } - cell.fieldView.valueActiveLabel.configure(field: value, emojiDict: emojiDict) + do { + let mastodonContent = MastodonContent(content: value, emojis: emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.fieldView.valueMetaLabel.configure(content: metaContent) + } catch { + let content = PlaintextMetaContent(string: value) + cell.fieldView.valueMetaLabel.configure(content: content) + } // only bind label. The text field should only set once } .store(in: &cell.disposeBag) @@ -76,8 +105,8 @@ extension ProfileFieldSection { // setup editing state cell.fieldView.titleTextField.isHidden = !attribute.isEditing cell.fieldView.valueTextField.isHidden = !attribute.isEditing - cell.fieldView.titleActiveLabel.isHidden = attribute.isEditing - cell.fieldView.valueActiveLabel.isHidden = attribute.isEditing + cell.fieldView.titleMetaLabel.isHidden = attribute.isEditing + cell.fieldView.valueMetaLabel.isHidden = attribute.isEditing // set control hidden let isHidden = !attribute.isEditing diff --git a/Mastodon/Diffiable/Section/Status/NotificationSection.swift b/Mastodon/Diffiable/Section/Status/NotificationSection.swift index 01229235..3113a446 100644 --- a/Mastodon/Diffiable/Section/Status/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/Status/NotificationSection.swift @@ -11,6 +11,8 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import MetaTextKit +import MastodonMeta enum NotificationSection: Equatable, Hashable { case main @@ -66,7 +68,14 @@ extension NotificationSection { .store(in: &cell.disposeBag) // configure author name, notification description, timestamp - cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict) + do { + let mastodonContent = MastodonContent(content: notification.account.displayNameWithFallback, emojis: notification.account.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.nameLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: notification.account.displayNameWithFallback) + cell.nameLabel.configure(content: metaContent) + } let createAt = notification.createAt let actionText = notification.notificationType.actionText cell.actionLabel.text = actionText + " ยท " + createAt.timeAgoSinceNow diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index 58488819..4b23d4eb 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -1,173 +1,66 @@ +//extension ActiveEntity { // -// ActiveLabel.swift -// Mastodon +// var accessibilityLabelDescription: String { +// switch self.type { +// case .email: return L10n.Common.Controls.Status.Tag.email +// case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag +// case .mention: return L10n.Common.Controls.Status.Tag.mention +// case .url: return L10n.Common.Controls.Status.Tag.url +// case .emoji: return L10n.Common.Controls.Status.Tag.emoji +// } +// } // -// Created by sxiaojian on 2021/1/29. +// var accessibilityValueDescription: String { +// switch self.type { +// case .email(let text, _): return text +// case .hashtag(let text, _): return text +// case .mention(let text, _): return text +// case .url(_, let trimmed, _, _): return trimmed +// case .emoji(let text, _, _): return text +// } +// } // +// func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? { +// if case .emoji = self.type { +// return nil +// } +// +// let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer) +// element.accessibilityTraits = .button +// element.accessibilityLabel = accessibilityLabelDescription +// element.accessibilityValue = accessibilityValueDescription +// return element +// } +//} -import UIKit -import Foundation -import ActiveLabel -import os.log -import MastodonUI - -extension ActiveLabel { - - enum Style { - case `default` - case statusHeader - case statusName - case profileFieldName - case profileFieldValue - } - - convenience init(style: Style) { - self.init() - - numberOfLines = 0 - lineSpacing = 5 - mentionColor = Asset.Colors.brandBlue.color - hashtagColor = Asset.Colors.brandBlue.color - URLColor = Asset.Colors.brandBlue.color - emojiPlaceholderColor = .systemFill - - accessibilityContainerType = .semanticGroup - - switch style { - case .default: - font = .preferredFont(forTextStyle: .body) - textColor = Asset.Colors.Label.primary.color - case .statusHeader: - font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17) - textColor = Asset.Colors.Label.secondary.color - numberOfLines = 1 - case .statusName: - font = .systemFont(ofSize: 17, weight: .semibold) - textColor = Asset.Colors.Label.primary.color - numberOfLines = 1 - case .profileFieldName: - font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) - textColor = Asset.Colors.Label.primary.color - numberOfLines = 1 - case .profileFieldValue: - font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) - textColor = Asset.Colors.Label.primary.color - numberOfLines = 1 - } - } - -} - -extension ActiveLabel { - public func configure(text: String) { - attributedText = nil - activeEntities.removeAll() - self.text = text - accessibilityLabel = text - } -} - -extension ActiveLabel { - - /// status content - public func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) { - attributedText = nil - activeEntities.removeAll() - - if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) { - text = parseResult.trimmed - activeEntities = parseResult.activeEntities - accessibilityLabel = parseResult.original - } else { - text = "" - accessibilityLabel = nil - } - } - - public func configure(contentParseResult parseResult: MastodonStatusContent.ParseResult?) { - attributedText = nil - activeEntities.removeAll() - text = parseResult?.trimmed ?? "" - activeEntities = parseResult?.activeEntities ?? [] - accessibilityLabel = parseResult?.original ?? nil - } - - /// account note - public func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) { - configure(content: note, emojiDict: emojiDict) - } -} - -extension ActiveLabel { - /// account field - public func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) { - configure(content: field, emojiDict: emojiDict) - } -} - -extension ActiveEntity { - - var accessibilityLabelDescription: String { - switch self.type { - case .email: return L10n.Common.Controls.Status.Tag.email - case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag - case .mention: return L10n.Common.Controls.Status.Tag.mention - case .url: return L10n.Common.Controls.Status.Tag.url - case .emoji: return L10n.Common.Controls.Status.Tag.emoji - } - } - - var accessibilityValueDescription: String { - switch self.type { - case .email(let text, _): return text - case .hashtag(let text, _): return text - case .mention(let text, _): return text - case .url(_, let trimmed, _, _): return trimmed - case .emoji(let text, _, _): return text - } - } - - func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? { - if case .emoji = self.type { - return nil - } - - let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer) - element.accessibilityTraits = .button - element.accessibilityLabel = accessibilityLabelDescription - element.accessibilityValue = accessibilityValueDescription - return element - } -} - -final class ActiveLabelAccessibilityElement: UIAccessibilityElement { - var index: Int! -} - +//final class ActiveLabelAccessibilityElement: UIAccessibilityElement { +// var index: Int! +//} +// // MARK: - UIAccessibilityContainer -extension ActiveLabel { - - func createAccessibilityElements() -> [UIAccessibilityElement] { - var elements: [UIAccessibilityElement] = [] - - let element = ActiveLabelAccessibilityElement(accessibilityContainer: self) - element.accessibilityTraits = .staticText - element.accessibilityLabel = accessibilityLabel - element.accessibilityFrame = superview!.convert(frame, to: nil) - element.accessibilityLanguage = accessibilityLanguage - elements.append(element) - - for entity in activeEntities { - guard let element = entity.accessibilityElement(in: self) else { continue } - var glyphRange = NSRange() - layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange) - let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) - element.accessibilityFrame = self.convert(rect, to: nil) - element.accessibilityContainer = self - elements.append(element) - } - - return elements - } - -} +//extension ActiveLabel { +// +// func createAccessibilityElements() -> [UIAccessibilityElement] { +// var elements: [UIAccessibilityElement] = [] +// +// let element = ActiveLabelAccessibilityElement(accessibilityContainer: self) +// element.accessibilityTraits = .staticText +// element.accessibilityLabel = accessibilityLabel +// element.accessibilityFrame = superview!.convert(frame, to: nil) +// element.accessibilityLanguage = accessibilityLanguage +// elements.append(element) +// +// for entity in activeEntities { +// guard let element = entity.accessibilityElement(in: self) else { continue } +// var glyphRange = NSRange() +// layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange) +// let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) +// element.accessibilityFrame = self.convert(rect, to: nil) +// element.accessibilityContainer = self +// elements.append(element) +// } +// +// return elements +// } +// +//} diff --git a/Mastodon/Extension/CoreDataStack/Emojis.swift b/Mastodon/Extension/CoreDataStack/Emojis.swift index 8d7c2975..2f45880b 100644 --- a/Mastodon/Extension/CoreDataStack/Emojis.swift +++ b/Mastodon/Extension/CoreDataStack/Emojis.swift @@ -23,15 +23,6 @@ extension EmojiContainer { let decoder = JSONDecoder() return emojisData.flatMap { try? decoder.decode([Mastodon.Entity.Emoji].self, from: $0) } } - - var emojiDict: MastodonStatusContent.EmojiDict { - var dict = MastodonStatusContent.EmojiDict() - for emoji in emojis ?? [] { - guard let url = URL(string: emoji.url) else { continue } - dict[emoji.shortcode] = url - } - return dict - } var emojiMeta: MastodonContent.Emojis { var dict = MastodonContent.Emojis() diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift index 8109371c..de703fd1 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -7,6 +7,7 @@ import UIKit import MastodonSDK +import MastodonMeta extension Mastodon.Entity.Account: Hashable { public func hash(into hasher: inout Hasher) { @@ -28,3 +29,13 @@ extension Mastodon.Entity.Account { return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! } } + +extension Mastodon.Entity.Account { + var emojiMeta: MastodonContent.Emojis { + var dict = MastodonContent.Emojis() + for emoji in emojis ?? [] { + dict[emoji.shortcode] = emoji.url + } + return dict + } +} diff --git a/Mastodon/Extension/MetaLabel.swift b/Mastodon/Extension/MetaLabel.swift index 67200874..27f04e06 100644 --- a/Mastodon/Extension/MetaLabel.swift +++ b/Mastodon/Extension/MetaLabel.swift @@ -6,14 +6,19 @@ // import UIKit +import Meta import MetaTextKit extension MetaLabel { enum Style { case statusHeader case statusName -// case profileFieldName -// case profileFieldValue + case notificationName + case profileFieldName + case profileFieldValue + case recommendAccountName + case titleView + case settingTableFooter } convenience init(style: Style) { @@ -33,14 +38,37 @@ extension MetaLabel { case .statusName: font = .systemFont(ofSize: 17, weight: .semibold) textColor = Asset.Colors.Label.primary.color -// case .profileFieldName: -// font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) -// textColor = Asset.Colors.Label.primary.color -// numberOfLines = 1 -// case .profileFieldValue: -// font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) -// textColor = Asset.Colors.Label.primary.color -// numberOfLines = 1 + + case .notificationName: + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20) + textColor = Asset.Colors.brandBlue.color + + case .profileFieldName: + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) + textColor = Asset.Colors.Label.primary.color + + case .profileFieldValue: + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) + textColor = Asset.Colors.Label.primary.color + textAlignment = .right + + + case .titleView: + font = .systemFont(ofSize: 17, weight: .semibold) + textColor = Asset.Colors.Label.primary.color + textAlignment = .center + paragraphStyle.alignment = .center + + case .recommendAccountName: + font = .systemFont(ofSize: 18, weight: .semibold) + textColor = .white + + case .settingTableFooter: + font = .preferredFont(forTextStyle: .body) + textColor = Asset.Colors.Label.primary.color + numberOfLines = 0 + textContainer.maximumNumberOfLines = 0 + paragraphStyle.alignment = .center } self.font = font @@ -50,4 +78,23 @@ extension MetaLabel { .font: font, .foregroundColor: textColor ] - }} + linkAttributes = [ + .font: font, + .foregroundColor: Asset.Colors.brandBlue.color + ] + } + +} + +struct PlaintextMetaContent: MetaContent { + let string: String + let entities: [Meta.Entity] = [] + + init(string: String) { + self.string = string + } + + func metaAttachment(for entity: Meta.Entity) -> MetaAttachment? { + return nil + } +} diff --git a/Mastodon/Helper/MastodonField.swift b/Mastodon/Helper/MastodonField.swift deleted file mode 100644 index 86bc2cc9..00000000 --- a/Mastodon/Helper/MastodonField.swift +++ /dev/null @@ -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] -// } -//} diff --git a/Mastodon/Helper/MastodonStatusContent+Appearance.swift b/Mastodon/Helper/MastodonStatusContent+Appearance.swift deleted file mode 100644 index d82d68b8..00000000 --- a/Mastodon/Helper/MastodonStatusContent+Appearance.swift +++ /dev/null @@ -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] - } -} diff --git a/Mastodon/Helper/MastodonStatusContent+ParseResult.swift b/Mastodon/Helper/MastodonStatusContent+ParseResult.swift deleted file mode 100644 index 9de0b341..00000000 --- a/Mastodon/Helper/MastodonStatusContent+ParseResult.swift +++ /dev/null @@ -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 - } -} diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift deleted file mode 100755 index 917c456d..00000000 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ /dev/null @@ -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 { - 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: "

", with: "

\r\n") - for (shortcode, url) in emojiDict { - let emojiNode = "\(shortcode)" - 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.. 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 { - return text.startIndex.. = { - 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: "
|
", 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.. 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 - } -} diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift index 918e960f..3c6d7da1 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift @@ -8,13 +8,10 @@ #if ASDK import Foundation -import ActiveLabel // MARK: - StatusViewDelegate extension StatusNodeDelegate where Self: StatusProvider { - func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) { - StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, node: node, didSelectActiveEntityType: type) - } + } #endif diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index 544de889..5dab0529 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -11,7 +11,6 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import ActiveLabel import Meta import MetaTextKit @@ -25,10 +24,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) { StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell) } - - func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, cell: cell, activeLabel: activeLabel, didTapEntity: entity) - } func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) { StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta) diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index e5bb15a8..e115151e 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -11,7 +11,6 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import ActiveLabel import Meta import MetaTextKit @@ -125,35 +124,6 @@ extension StatusProviderFacade { } extension StatusProviderFacade { - - static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) { - switch entity.type { - case .url(_, _, let url, _), - .mention(let url, _) where url.lowercased().hasPrefix("http"): - // note: - // some server mark the normal url as "u-url" class. : - guard let url = URL(string: url) else { return } - if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain, - url.pathComponents.count >= 4, - url.pathComponents[0] == "/", - url.pathComponents[1] == "web", - url.pathComponents[2] == "statuses" { - let statusID = url.pathComponents[3] - let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID) - provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - } else { - provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - } - case .hashtag(let text, _): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text) - provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show) - case .mention(let text, let userInfo): - let href = userInfo?["href"] as? String - coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text, href: href) - default: - break - } - } static func responseToStatusMetaTextAction(provider: StatusProvider, cell: UITableViewCell, metaText: MetaText, didSelectMeta meta: Meta) { switch meta { @@ -185,31 +155,6 @@ extension StatusProviderFacade { } #if ASDK - static func responseToStatusActiveLabelAction(provider: StatusProvider, node: ASCellNode, didSelectActiveEntityType type: ActiveEntityType) { - switch type { - case .hashtag(let text, _): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text) - provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show) - case .mention(let text, _): - coordinateToStatusMentionProfileScene(for: .primary, provider: provider, node: node, mention: text) - case .url(_, _, let url, _): - guard let url = URL(string: url) else { return } - if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain, - url.pathComponents.count >= 4, - url.pathComponents[0] == "/", - url.pathComponents[1] == "web", - url.pathComponents[2] == "statuses" { - let statusID = url.pathComponents[3] - let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID) - provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - } else { - provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - } - default: - break - } - } - private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, node: ASCellNode, mention: String) { guard let status = provider.status(node: node, indexPath: nil) else { return } coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention, href: nil) diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentTableViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentTableViewCell.swift index d970b673..ffeae767 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentTableViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentTableViewCell.swift @@ -40,20 +40,19 @@ final class ComposeStatusContentTableViewCell: UITableViewCell { attributes: attributes ) }() - let paragraphStyle: NSMutableParagraphStyle = { + metaText.paragraphStyle = { let style = NSMutableParagraphStyle() style.lineSpacing = 5 + style.paragraphSpacing = 8 return style }() metaText.textAttributes = [ .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), .foregroundColor: Asset.Colors.Label.primary.color, - .paragraphStyle: paragraphStyle, ] metaText.linkAttributes = [ .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), .foregroundColor: Asset.Colors.brandBlue.color, - .paragraphStyle: paragraphStyle, ] return metaText }() diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift index d88b919f..c92e689b 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift @@ -6,14 +6,14 @@ // import UIKit -import FLAnimatedImage +import SDWebImage final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell { static let itemSize = CGSize(width: 44, height: 44) - let emojiImageView: FLAnimatedImageView = { - let imageView = FLAnimatedImageView() + let emojiImageView: SDAnimatedImageView = { + let imageView = SDAnimatedImageView() imageView.contentMode = .scaleAspectFit imageView.layer.masksToBounds = true return imageView diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift index 2642808d..79fe538d 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift @@ -193,8 +193,15 @@ extension ComposeViewModel: UITableViewDataSource { // set avatar cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) - // set name username - cell.statusView.nameLabel.configure(content: status.author.displayNameWithFallback, emojiDict: status.author.emojiDict) + // set name, username + do { + let mastodonContent = MastodonContent(content: status.author.displayNameWithFallback, emojis: status.author.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.statusView.nameLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: status.author.displayNameWithFallback) + cell.statusView.nameLabel.configure(content: metaContent) + } cell.statusView.usernameLabel.text = "@" + status.author.acct // set text let content = MastodonContent(content: status.content, emojis: status.emojiMeta) @@ -226,13 +233,14 @@ extension ComposeViewModel: UITableViewDataSource { let name = author.displayName.isEmpty ? author.username : author.displayName return L10n.Scene.Compose.replyingToUser(name) }() - MastodonStatusContent.parseResult(content: headerText, emojiDict: replyTo.author.emojiDict) - .receive(on: DispatchQueue.main) - .sink { [weak cell] parseResult in - guard let cell = cell else { return } - cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult) - } - .store(in: &cell.disposeBag) + do { + let mastodonContent = MastodonContent(content: headerText, emojis: replyTo.author.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.statusView.headerInfoLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: headerText) + cell.statusView.headerInfoLabel.configure(content: metaContent) + } } // configure author ComposeStatusSection.configureStatusContent(cell: cell, attribute: composeStatusAttribute) diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 2e059c1a..52af2cb6 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -181,7 +181,7 @@ final class ComposeViewModel: NSObject { } return displayName }() - self.composeStatusAttribute.emojiDict.value = mastodonUser?.emojiDict ?? [:] + self.composeStatusAttribute.emojiMeta.value = mastodonUser?.emojiMeta ?? [:] self.composeStatusAttribute.username.value = username } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift index 8e6d4687..8948a2f6 100644 --- a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift +++ b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift @@ -7,7 +7,6 @@ import os.log import UIKit -import ActiveLabel import FLAnimatedImage import MetaTextKit @@ -52,12 +51,7 @@ final class ReplicaStatusView: UIView { return label }() - let headerInfoLabel: ActiveLabel = { - let label = ActiveLabel(style: .statusHeader) - label.text = "Bob reblogged" - label.layer.masksToBounds = false - return label - }() + let headerInfoLabel = MetaLabel(style: .statusHeader) let avatarView: UIView = { let view = UIView() @@ -68,10 +62,7 @@ final class ReplicaStatusView: UIView { }() let avatarImageView = FLAnimatedImageView() - let nameLabel: ActiveLabel = { - let label = ActiveLabel(style: .statusName) - return label - }() + let nameLabel = MetaLabel(style: .statusName) let nameTrialingDotLabel: UILabel = { let label = UILabel() diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index c0de5c53..8d4a8b37 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -56,7 +56,7 @@ extension HashtagTimelineViewController { super.viewDidLoad() title = "#\(viewModel.hashtag)" - titleView.update(title: viewModel.hashtag, subtitle: nil, emojiDict: [:]) + titleView.update(title: viewModel.hashtag, subtitle: nil) navigationItem.titleView = titleView view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor @@ -150,7 +150,7 @@ extension HashtagTimelineViewController { private func updatePromptTitle() { var subtitle: String? defer { - titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle, emojiDict: [:]) + titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle) } guard let histories = viewModel.hashtagEntity.value?.history else { return @@ -209,7 +209,7 @@ extension HashtagTimelineViewController: LoadMoreConfigurableTableViewContainer typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell typealias LoadingState = HashtagTimelineViewModel.LoadOldestState.Loading var loadMoreConfigurableTableView: UITableView { return tableView } - var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } + var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadOldestStateMachine } } // MARK: - UITableViewDelegate diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift index a90afdad..09325873 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift @@ -84,7 +84,7 @@ extension HashtagTimelineViewModel { newSnapshot.appendItems(statusItemList, toSection: .main) } - if !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) { + if !(self.loadOldestStateMachine.currentState is LoadOldestState.NoMore) { newSnapshot.appendItems([.bottomLoader], toSection: .main) } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index 54acbf4f..c1698f5d 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -46,7 +46,7 @@ final class HashtagTimelineViewModel: NSObject { }() lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) // bottom loader - private(set) lazy var loadoldestStateMachine: GKStateMachine = { + private(set) lazy var loadOldestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ LoadOldestState.Initial(viewModel: self), diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+Provider.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+Provider.swift index 4896c58b..5f97ebea 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+Provider.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+Provider.swift @@ -109,6 +109,12 @@ extension AsyncHomeTimelineViewController: StatusProvider { return nil } } + + func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] { + guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] } + let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem } + return items + } } diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift index c8ccfdd3..c90b703e 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift @@ -86,7 +86,7 @@ extension AsyncHomeTimelineViewController { node.allowsSelection = true title = L10n.Scene.HomeTimeline.title - view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color + view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor navigationItem.leftBarButtonItem = settingBarButtonItem navigationItem.titleView = titleView titleView.delegate = self @@ -341,7 +341,7 @@ extension AsyncHomeTimelineViewController { // typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell // typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading // var loadMoreConfigurableTableView: UITableView { return tableView } -// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } +// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadOldestStateMachine } //} // MARK: - UITableViewDelegate @@ -556,7 +556,7 @@ extension AsyncHomeTimelineViewController: ASTableDelegate { } func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) { - viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) + viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) context.completeBatchFetching(true) } diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+Diffable.swift index fbd5c143..7799c216 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+Diffable.swift @@ -106,7 +106,7 @@ extension AsyncHomeTimelineViewModel: NSFetchedResultsControllerDelegate { let endSnapshot = CACurrentMediaTime() - if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) { + if shouldAddBottomLoader, !(self.loadLatestStateMachine.currentState is LoadOldestState.NoMore) { newSnapshot.appendItems([.bottomLoader], toSection: .main) } diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel.swift index c33b91c0..d7ed0b10 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel.swift @@ -18,7 +18,6 @@ import CoreDataStack import GameplayKit import AlamofireImage import DateToolsSwift -import ActiveLabel import AsyncDisplayKit final class AsyncHomeTimelineViewModel: NSObject { @@ -59,7 +58,7 @@ final class AsyncHomeTimelineViewModel: NSObject { }() lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) // bottom loader - private(set) lazy var loadoldestStateMachine: GKStateMachine = { + private(set) lazy var loadOldestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ LoadOldestState.Initial(viewModel: self), diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 73650b6e..e69d3bae 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -388,7 +388,7 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer { typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading var loadMoreConfigurableTableView: UITableView { return tableView } - var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } + var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadLatestStateMachine } } // MARK: - UITableViewDelegate diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index b2ea2035..d25f30ae 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -115,7 +115,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { let endSnapshot = CACurrentMediaTime() DispatchQueue.main.async { - if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) { + if shouldAddBottomLoader, !(self.loadLatestStateMachine.currentState is LoadOldestState.NoMore) { newSnapshot.appendItems([.bottomLoader], toSection: .main) } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 9d8f6f70..611e9536 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -15,7 +15,6 @@ import CoreDataStack import GameplayKit import AlamofireImage import DateToolsSwift -import ActiveLabel final class HomeTimelineViewModel: NSObject { @@ -52,7 +51,7 @@ final class HomeTimelineViewModel: NSObject { }() lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) // bottom loader - private(set) lazy var loadoldestStateMachine: GKStateMachine = { + private(set) lazy var loadOldestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ LoadOldestState.Initial(viewModel: self), diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 5528adf6..100cd3d8 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -12,7 +12,6 @@ import GameplayKit import MastodonSDK import OSLog import UIKit -import ActiveLabel import Meta import MetaTextKit @@ -272,7 +271,7 @@ extension NotificationViewController { switch item { case .bottomLoader: if !tableView.isDragging, !tableView.isDecelerating { - viewModel.loadoldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self) + viewModel.loadOldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self) } default: break @@ -305,7 +304,7 @@ extension NotificationViewController: NotificationTableViewCellDelegate { } } - func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: ActiveLabel) { + func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: MetaLabel) { guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let indexPath = tableView.indexPath(for: cell) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } @@ -319,7 +318,6 @@ extension NotificationViewController: NotificationTableViewCellDelegate { } } - func notificationTableViewCell(_ cell: NotificationStatusTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) { viewModel.acceptFollowRequest(notification: notification) } @@ -380,7 +378,7 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer { typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell typealias LoadingState = NotificationViewModel.LoadOldestState.Loading var loadMoreConfigurableTableView: UITableView { tableView } - var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine } + var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadOldestStateMachine } } extension NotificationViewController { diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 8102f770..71238091 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -54,7 +54,7 @@ final class NotificationViewModel: NSObject { lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) // bottom loader - private(set) lazy var loadoldestStateMachine: GKStateMachine = { + private(set) lazy var loadOldestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ LoadOldestState.Initial(viewModel: self), diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 6afc61e9..65e4dbcd 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -10,7 +10,6 @@ import Combine import Foundation import CoreDataStack import UIKit -import ActiveLabel import MetaTextKit import Meta import FLAnimatedImage @@ -20,7 +19,7 @@ protocol NotificationTableViewCellDelegate: AnyObject { func parent() -> UIViewController func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, avatarImageViewDidPressed imageView: UIImageView) - func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: ActiveLabel) + func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: MetaLabel) func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) @@ -58,13 +57,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { return label }() - let nameLabel: ActiveLabel = { - let label = ActiveLabel(style: .statusName) - label.textColor = Asset.Colors.brandBlue.color - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20) - label.lineBreakMode = .byTruncatingTail - return label - }() + let nameLabel = MetaLabel(style: .notificationName) let buttonStackView = UIStackView() @@ -318,10 +311,6 @@ extension NotificationStatusTableViewCell: StatusViewDelegate { func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { // do nothing } - - func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - // do nothing - } func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) { delegate?.notificationStatusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta) diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index 3af4f884..c9890c24 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -57,7 +57,7 @@ extension FavoriteViewController { .store(in: &disposeBag) navigationItem.titleView = titleView - titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil, emojiDict: [:]) + titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil) tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index a88d3e06..f0bf72af 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -9,16 +9,16 @@ import os.log import UIKit import Combine import PhotosUI -import ActiveLabel import AlamofireImage import CropViewController import TwitterTextEditor import MastodonMeta +import MetaTextKit protocol ProfileHeaderViewControllerDelegate: AnyObject { func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int) - func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) + func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) } final class ProfileHeaderViewController: UIViewController { @@ -35,6 +35,7 @@ final class ProfileHeaderViewController: UIViewController { let titleView: DoubleTitleLabelNavigationBarTitleView = { let titleView = DoubleTitleLabelNavigationBarTitleView() titleView.titleLabel.textColor = .white + titleView.titleLabel.textAttributes[.foregroundColor] = UIColor.white titleView.titleLabel.alpha = 0 titleView.subtitleLabel.textColor = .white titleView.subtitleLabel.alpha = 0 @@ -179,19 +180,14 @@ extension ProfileHeaderViewController { viewModel.isEditing, viewModel.displayProfileInfo.name.removeDuplicates(), viewModel.editProfileInfo.name.removeDuplicates(), - viewModel.emojiDict + viewModel.emojiMeta ) .receive(on: DispatchQueue.main) - .sink { [weak self] isEditing, name, editingName, emojiDict in + .sink { [weak self] isEditing, name, editingName, emojiMeta in guard let self = self else { return } do { - var emojis = MastodonContent.Emojis() - for (key, value) in emojiDict { - emojis[key] = value.absoluteString - } - let metaContent = try MastodonMetaContent.convert( - document: MastodonContent(content: name ?? " ", emojis: emojis) - ) + let mastodonContent = MastodonContent(content: name ?? " ", emojis: emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) self.profileHeaderView.nameMetaText.configure(content: metaContent) } catch { assertionFailure() @@ -200,25 +196,37 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) - Publishers.CombineLatest3( - viewModel.isEditing.eraseToAnyPublisher(), - viewModel.displayProfileInfo.note.removeDuplicates().eraseToAnyPublisher(), - viewModel.editProfileInfo.note.removeDuplicates().eraseToAnyPublisher() + Publishers.CombineLatest4( + viewModel.isEditing.removeDuplicates(), + viewModel.displayProfileInfo.note.removeDuplicates(), + viewModel.editProfileInfo.note.removeDuplicates(), + viewModel.emojiMeta.removeDuplicates() ) .receive(on: DispatchQueue.main) - .sink { [weak self] isEditing, note, editingNote in + .sink { [weak self] isEditing, note, editingNote, emojiMeta in guard let self = self else { return } - self.profileHeaderView.bioActiveLabel.configure(note: note ?? "", emojiDict: [:]) // FIXME: custom emoji - // prevent duplicate set - let editingNote = editingNote ?? "" - if self.profileHeaderView.bioTextEditorView.text != editingNote { - self.profileHeaderView.bioTextEditorView.text = editingNote + self.profileHeaderView.bioMetaText.textView.isEditable = isEditing + + if isEditing { + if self.profileHeaderView.bioMetaText.backedString != note { + let metaContent = PlaintextMetaContent(string: editingNote ?? "") + self.profileHeaderView.bioMetaText.configure(content: metaContent) + } + } else { + let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + self.profileHeaderView.bioMetaText.configure(content: metaContent) + } catch { + assertionFailure() + self.profileHeaderView.bioMetaText.reset() + } } } .store(in: &disposeBag) - - profileHeaderView.bioTextEditorView.changeObserver = self + profileHeaderView.bioMetaText.delegate = self + NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField) .receive(on: DispatchQueue.main) .sink { [weak self] notification in @@ -450,13 +458,16 @@ extension ProfileHeaderViewController { } -// MARK: - TextEditorViewChangeObserver -extension ProfileHeaderViewController: TextEditorViewChangeObserver { - func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text) - guard changeResult.isTextChanged else { return } - assert(textEditorView === profileHeaderView.bioTextEditorView) - viewModel.editProfileInfo.note.value = textEditorView.text +// MARK: - MetaTextDelegate +extension ProfileHeaderViewController: MetaTextDelegate { + func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, metaText.backedString) + assert(metaText.textView === profileHeaderView.bioMetaText.textView) + if metaText.textView === profileHeaderView.bioMetaText.textView { + viewModel.editProfileInfo.note.value = metaText.backedString + } + + return nil } } @@ -533,6 +544,7 @@ extension ProfileHeaderViewController: UICollectionViewDelegate { // MARK: - ProfileFieldCollectionViewCellDelegate extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate { + // should be remove style edit button func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) { guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return } @@ -541,8 +553,8 @@ extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate { viewModel.removeFieldItem(item: item) } - func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, activeLabel: activeLabel, didSelectActiveEntity: entity) + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) { + delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, metaLabel: metaLebel, didSelectMeta: meta) } } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 9a014575..c9875858 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -10,6 +10,7 @@ import UIKit import Combine import Kanna import MastodonSDK +import MastodonMeta final class ProfileHeaderViewModel { @@ -24,7 +25,7 @@ final class ProfileHeaderViewModel { let needsSetupBottomShadow = CurrentValueSubject(true) let needsFiledCollectionViewHidden = CurrentValueSubject(false) let isTitleViewContentOffsetSet = CurrentValueSubject(false) - let emojiDict = CurrentValueSubject([:]) + let emojiMeta = CurrentValueSubject([:]) let accountForEdit = CurrentValueSubject(nil) // output @@ -58,10 +59,10 @@ final class ProfileHeaderViewModel { isEditing.removeDuplicates(), displayProfileInfo.fields.removeDuplicates(), editProfileInfo.fields.removeDuplicates(), - emojiDict.removeDuplicates() + emojiMeta.removeDuplicates() ) .receive(on: RunLoop.main) - .sink { [weak self] isEditing, displayFields, editingFields, emojiDict in + .sink { [weak self] isEditing, displayFields, editingFields, emojiMeta in guard let self = self else { return } guard let diffableDataSource = self.fieldDiffableDataSource else { return } @@ -87,7 +88,7 @@ final class ProfileHeaderViewModel { let attribute = oldFieldAttributeDict[field.id] ?? ProfileFieldItem.FieldItemAttribute() attribute.isEditing = isEditing - attribute.emojiDict.value = emojiDict + attribute.emojiMeta.value = emojiMeta attribute.isLast = false return ProfileFieldItem.field(field: field, attribute: attribute) } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift index d8ecb198..cafe0eda 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift @@ -95,12 +95,12 @@ extension ProfileFieldAddEntryCollectionViewCell { bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), ]) - fieldView.titleActiveLabel.isHidden = false - fieldView.titleActiveLabel.configure(field: L10n.Scene.Profile.Fields.addRow, emojiDict: [:]) + fieldView.titleMetaLabel.isHidden = false + fieldView.titleMetaLabel.configure(content: PlaintextMetaContent(string: L10n.Scene.Profile.Fields.addRow)) fieldView.titleTextField.isHidden = true - fieldView.valueActiveLabel.isHidden = false - fieldView.valueActiveLabel.configure(field: " ", emojiDict: [:]) + fieldView.valueMetaLabel.isHidden = false + fieldView.valueMetaLabel.configure(content: PlaintextMetaContent(string: " ")) fieldView.valueTextField.isHidden = true addGestureRecognizer(singleTagGestureRecognizer) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift index 6efa92e1..9106b0e4 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift @@ -8,11 +8,11 @@ import os.log import UIKit import Combine -import ActiveLabel +import MetaTextKit protocol ProfileFieldCollectionViewCellDelegate: AnyObject { func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) - func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) } final class ProfileFieldCollectionViewCell: UICollectionViewCell { @@ -107,7 +107,7 @@ extension ProfileFieldCollectionViewCell { editButton.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.editButtonDidPressed(_:)), for: .touchUpInside) - fieldView.valueActiveLabel.delegate = self + fieldView.valueMetaLabel.linkDelegate = self resetSeparatorLineLayout() } @@ -153,13 +153,11 @@ extension ProfileFieldCollectionViewCell { } } - - -// MARK: - ActiveLabelDelegate -extension ProfileFieldCollectionViewCell: ActiveLabelDelegate { - func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { +// MARK: - MetaLabelDelegate +extension ProfileFieldCollectionViewCell: MetaLabelDelegate { + func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.profileFieldCollectionViewCell(self, activeLabel: activeLabel, didSelectActiveEntity: entity) + delegate?.profileFieldCollectionViewCell(self, metaLebel: metaLabel, didSelectMeta: meta) } } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift index 956b8d6d..ee17d7e4 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift @@ -7,7 +7,7 @@ import UIKit import Combine -import ActiveLabel +import MetaTextKit final class ProfileFieldView: UIView { @@ -18,11 +18,7 @@ final class ProfileFieldView: UIView { let value = PassthroughSubject() // for custom emoji display - let titleActiveLabel: ActiveLabel = { - let label = ActiveLabel(style: .profileFieldName) - label.configure(content: "title", emojiDict: [:]) - return label - }() + let titleMetaLabel = MetaLabel(style: .profileFieldName) // for editing let titleTextField: UITextField = { @@ -34,12 +30,7 @@ final class ProfileFieldView: UIView { }() // for custom emoji display - let valueActiveLabel: ActiveLabel = { - let label = ActiveLabel(style: .profileFieldValue) - label.configure(content: "value", emojiDict: [:]) - label.textAlignment = .right - return label - }() + let valueMetaLabel = MetaLabel(style: .profileFieldValue) // for editing let valueTextField: UITextField = { @@ -81,10 +72,10 @@ extension ProfileFieldView { containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) - titleActiveLabel.translatesAutoresizingMaskIntoConstraints = false - containerStackView.addArrangedSubview(titleActiveLabel) + titleMetaLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(titleMetaLabel) NSLayoutConstraint.activate([ - titleActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), + titleMetaLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), ]) titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) titleTextField.translatesAutoresizingMaskIntoConstraints = false @@ -94,12 +85,12 @@ extension ProfileFieldView { ]) titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) - valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false - containerStackView.addArrangedSubview(valueActiveLabel) + valueMetaLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(valueMetaLabel) NSLayoutConstraint.activate([ - valueActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), + valueMetaLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), ]) - valueActiveLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + valueMetaLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) valueTextField.translatesAutoresizingMaskIntoConstraints = false containerStackView.addArrangedSubview(valueTextField) NSLayoutConstraint.activate([ @@ -137,7 +128,8 @@ struct ProfileFieldView_Previews: PreviewProvider { static var previews: some View { UIViewPreview(width: 375) { let filedView = ProfileFieldView() - filedView.valueActiveLabel.configure(field: "https://mastodon.online", emojiDict: [:]) + let content = PlaintextMetaContent(string: "https://mastodon.online") + filedView.valueMetaLabel.configure(content: content) return filedView } .previewLayout(.fixed(width: 375, height: 100)) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 043e1b66..eb6e06a0 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -8,8 +8,6 @@ import os.log import UIKit import Combine -import ActiveLabel -import TwitterTextEditor import FLAnimatedImage import MetaTextKit @@ -17,7 +15,7 @@ protocol ProfileHeaderViewDelegate: AnyObject { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView) @@ -166,28 +164,38 @@ final class ProfileHeaderView: UIView { }() let bioContainerView = UIView() - let bioContainerStackView = UIStackView() let fieldContainerStackView = UIStackView() - - let bioActiveLabelContainer: UIView = { - // use to set margin for active label - // the display/edit mode bio transition animation should without flicker with that - let view = UIView() - // note: comment out to see how it works - view.layoutMargins = UIEdgeInsets(top: 8, left: 5, bottom: 8, right: 5) // magic from TextEditorView - return view - }() - let bioActiveLabel = ActiveLabel(style: .default) - let bioTextEditorView: TextEditorView = { - let textEditorView = TextEditorView() - textEditorView.scrollView.isScrollEnabled = false - textEditorView.isScrollEnabled = false - textEditorView.font = .preferredFont(forTextStyle: .body) - textEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color - textEditorView.layer.masksToBounds = true - textEditorView.layer.cornerCurve = .continuous - textEditorView.layer.cornerRadius = 10 - return textEditorView + + let bioMetaText: MetaText = { + let metaText = MetaText() + metaText.textView.backgroundColor = .clear + metaText.textView.isEditable = false + metaText.textView.isSelectable = true + metaText.textView.isScrollEnabled = false + //metaText.textView.textContainer.lineFragmentPadding = 0 + //metaText.textView.textContainerInset = .zero + metaText.textView.layer.masksToBounds = false + metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment + + metaText.textView.layer.masksToBounds = true + metaText.textView.layer.cornerCurve = .continuous + metaText.textView.layer.cornerRadius = 10 + + metaText.paragraphStyle = { + let style = NSMutableParagraphStyle() + style.lineSpacing = 5 + style.paragraphSpacing = 8 + return style + }() + metaText.textAttributes = [ + .font: UIFont.preferredFont(forTextStyle: .body), + .foregroundColor: Asset.Colors.Label.primary.color, + ] + metaText.linkAttributes = [ + .font: UIFont.preferredFont(forTextStyle: .body), + .foregroundColor: Asset.Colors.brandBlue.color, + ] + return metaText }() static func createFieldCollectionViewLayout() -> UICollectionViewLayout { @@ -405,28 +413,15 @@ extension ProfileHeaderView { bioContainerView.preservesSuperviewLayoutMargins = true metaContainerStackView.addArrangedSubview(bioContainerView) - bioContainerStackView.translatesAutoresizingMaskIntoConstraints = false - bioContainerView.addSubview(bioContainerStackView) + bioMetaText.textView.translatesAutoresizingMaskIntoConstraints = false + bioContainerView.addSubview(bioMetaText.textView) NSLayoutConstraint.activate([ - bioContainerStackView.topAnchor.constraint(equalTo: bioContainerView.topAnchor), - bioContainerStackView.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor), - bioContainerStackView.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor), - bioContainerStackView.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor), + bioMetaText.textView.topAnchor.constraint(equalTo: bioContainerView.topAnchor), + bioMetaText.textView.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor), + bioMetaText.textView.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor), + bioMetaText.textView.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor), ]) - bioActiveLabel.translatesAutoresizingMaskIntoConstraints = false - bioActiveLabelContainer.addSubview(bioActiveLabel) - NSLayoutConstraint.activate([ - bioActiveLabel.topAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.topAnchor), - bioActiveLabel.leadingAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.leadingAnchor), - bioActiveLabel.trailingAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.trailingAnchor), - bioActiveLabel.bottomAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.bottomAnchor), - ]) - - bioContainerStackView.axis = .vertical - bioContainerStackView.addArrangedSubview(bioActiveLabelContainer) - bioContainerStackView.addArrangedSubview(bioTextEditorView) - fieldCollectionView.translatesAutoresizingMaskIntoConstraints = false metaContainerStackView.addArrangedSubview(fieldCollectionView) fieldCollectionViewHeightLayoutConstraint = fieldCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh) @@ -445,7 +440,7 @@ extension ProfileHeaderView { bringSubviewToFront(bannerContainerView) bringSubviewToFront(nameContainerStackView) - bioActiveLabel.delegate = self + bioMetaText.textView.linkDelegate = self let avatarImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer avatarImageView.addGestureRecognizer(avatarImageViewSingleTapGestureRecognizer) @@ -479,9 +474,8 @@ extension ProfileHeaderView { nameMetaText.textView.alpha = 1 nameTextField.alpha = 0 nameTextField.isEnabled = false - bioActiveLabelContainer.isHidden = false - bioTextEditorView.isHidden = true - + bioMetaText.textView.backgroundColor = .clear + animator.addAnimations { self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor self.nameTextFieldBackgroundView.backgroundColor = .clear @@ -494,17 +488,15 @@ extension ProfileHeaderView { nameMetaText.textView.alpha = 0 nameTextField.isEnabled = true nameTextField.alpha = 1 - bioActiveLabelContainer.isHidden = true - bioTextEditorView.isHidden = false editAvatarBackgroundView.isHidden = false editAvatarBackgroundView.alpha = 0 - bioTextEditorView.backgroundColor = .clear + bioMetaText.textView.backgroundColor = .clear animator.addAnimations { self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color self.editAvatarBackgroundView.alpha = 1 - self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color + self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color } } @@ -530,11 +522,11 @@ extension ProfileHeaderView { } } -// MARK: - ActiveLabelDelegate -extension ProfileHeaderView: ActiveLabelDelegate { - func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText) - delegate?.profileHeaderView(self, activeLabel: activeLabel, entityDidPressed: entity) +// MARK: - MetaTextViewDelegate +extension ProfileHeaderView: MetaTextViewDelegate { + func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.profileHeaderView(self, metaTextView: metaTextView, metaDidPressed: meta) } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 1ac0c4a4..fa54abf7 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -8,7 +8,8 @@ import os.log import UIKit import Combine -import ActiveLabel +import MastodonMeta +import MetaTextKit final class ProfileViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -316,20 +317,26 @@ extension ProfileViewController { // bind view model Publishers.CombineLatest3( viewModel.name, - viewModel.emojiDict, + viewModel.emojiMeta, viewModel.statusesCount ) .receive(on: DispatchQueue.main) - .sink { [weak self] name, emojiDict, statusesCount in + .sink { [weak self] name, emojiMeta, statusesCount in guard let self = self else { return } guard let title = name, let statusesCount = statusesCount, let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { self.titleView.isHidden = true return } - let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount) - self.titleView.update(title: title, subtitle: subtitle, emojiDict: emojiDict) self.titleView.isHidden = false + let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount) + let mastodonContent = MastodonContent(content: title, emojis: emojiMeta) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle) + } catch { + + } } .store(in: &disposeBag) viewModel.name @@ -391,9 +398,9 @@ extension ProfileViewController { viewModel.accountForEdit .assign(to: \.value, on: profileHeaderViewController.viewModel.accountForEdit) .store(in: &disposeBag) - viewModel.emojiDict + viewModel.emojiMeta .receive(on: DispatchQueue.main) - .assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict) + .assign(to: \.value, on: profileHeaderViewController.viewModel.emojiMeta) .store(in: &disposeBag) viewModel.username .map { username in username.flatMap { "@" + $0 } ?? " " } @@ -695,7 +702,7 @@ extension ProfileViewController: UIScrollViewDelegate { // MARK: - ProfileHeaderViewControllerDelegate extension ProfileViewController: ProfileHeaderViewControllerDelegate { - + func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) { guard let scrollView = (profileSegmentedViewController.pagingViewController.currentViewController as? UserTimelineViewController)?.scrollView else { // assertionFailure() @@ -712,23 +719,24 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { ) } - func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - // handle profile fields interaction - switch entity.type { + func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) { + switch meta { case .url(_, _, let url, _): guard let url = URL(string: url) else { return } coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - case .hashtag(let hashtag, _): + case .hashtag(_, let hashtag, _): let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) - case .mention(_, let userInfo): + case .mention(_, _, let userInfo): guard let href = userInfo?["href"] as? String else { // currently we cannot present profile scene without userID return } guard let url = URL(string: href) else { return } coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - default: + case .email: + break + case .emoji: break } } @@ -758,7 +766,7 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate { // MARK: - ProfileHeaderViewDelegate extension ProfileViewController: ProfileHeaderViewDelegate { - + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) { guard let mastodonUser = viewModel.mastodonUser.value else { return } guard let avatar = imageView.image else { return } @@ -953,24 +961,24 @@ extension ProfileViewController: ProfileHeaderViewDelegate { } } } - - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) { - switch entity.type { + + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta) { + switch meta { case .url(_, _, let url, _): guard let url = URL(string: url) else { return } coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - case .mention(_, let userInfo): + case .mention(_, _, let userInfo): guard let href = userInfo?["href"] as? String, - let url = URL(string: href) else { return } + let url = URL(string: href) else { return } coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - case .hashtag(let hashtag, _): + case .hashtag(_, let hashtag, _): let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) - default: + case .email, .emoji: break } } - + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) { } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 4b2809a3..5efbaa68 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreDataStack import MastodonSDK +import MastodonMeta // please override this base class class ProfileViewModel: NSObject { @@ -40,7 +41,7 @@ class ProfileViewModel: NSObject { let followingCount: CurrentValueSubject let followersCount: CurrentValueSubject let fields: CurrentValueSubject<[Mastodon.Entity.Field], Never> - let emojiDict: CurrentValueSubject + let emojiMeta: CurrentValueSubject // fulfill this before editing let accountForEdit = CurrentValueSubject(nil) @@ -83,7 +84,7 @@ class ProfileViewModel: NSObject { self.protected = CurrentValueSubject(mastodonUser?.locked) self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false) self.fields = CurrentValueSubject(mastodonUser?.fields ?? []) - self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:]) + self.emojiMeta = CurrentValueSubject(mastodonUser?.emojiMeta ?? [:]) super.init() relationshipActionOptionSet @@ -258,7 +259,7 @@ extension ProfileViewModel { self.protected.value = mastodonUser?.locked self.suspended.value = mastodonUser?.suspended ?? false self.fields.value = mastodonUser?.fields ?? [] - self.emojiDict.value = mastodonUser?.emojiDict ?? [:] + self.emojiMeta.value = mastodonUser?.emojiMeta ?? [:] } private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) { diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift index 32d096a3..ff566a24 100644 --- a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift +++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift @@ -11,7 +11,6 @@ import AVKit import Combine import CoreData import CoreDataStack -import ActiveLabel import Meta import MetaTextKit @@ -212,9 +211,6 @@ extension ReportedStatusTableViewCell: StatusViewDelegate { func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { } - - func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - } func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) { } diff --git a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index b69914db..cd1d196e 100644 --- a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -10,7 +10,8 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit -import ActiveLabel +import MetaTextKit +import MastodonMeta protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { func followButtonDidPressed(clickedUser: MastodonUser) @@ -43,14 +44,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) - let displayNameLabel: ActiveLabel = { - let label = ActiveLabel(style: .statusName) - label.textColor = .white - label.textAlignment = .center - label.font = .systemFont(ofSize: 18, weight: .semibold) - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() + let displayNameLabel = MetaLabel(style: .recommendAccountName) let acctLabel: UILabel = { let label = UILabel() @@ -165,7 +159,14 @@ extension SearchRecommendAccountsCollectionViewCell { } func config(with mastodonUser: MastodonUser) { - displayNameLabel.configure(content: mastodonUser.displayNameWithFallback, emojiDict: mastodonUser.emojiDict) + do { + let mastodonContent = MastodonContent(content: mastodonUser.displayNameWithFallback, emojis: mastodonUser.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + displayNameLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: mastodonUser.displayNameWithFallback) + displayNameLabel.configure(content: metaContent) + } acctLabel.text = "@" + mastodonUser.acct avatarImageView.af.setImage( withURL: URL(string: mastodonUser.avatar)!, diff --git a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift index 1b48d1d2..4c1753ff 100644 --- a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift @@ -11,6 +11,8 @@ import Foundation import MastodonSDK import UIKit import FLAnimatedImage +import MetaTextKit +import MastodonMeta final class SearchResultTableViewCell: UITableViewCell { @@ -22,13 +24,7 @@ final class SearchResultTableViewCell: UITableViewCell { return imageView }() - let _titleLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.brandBlue.color - label.font = .systemFont(ofSize: 17, weight: .semibold) - label.lineBreakMode = .byTruncatingTail - return label - }() + let _titleLabel = MetaLabel(style: .statusName) let _subTitleLabel: UILabel = { let label = UILabel() @@ -155,13 +151,28 @@ extension SearchResultTableViewCell { func config(with account: Mastodon.Entity.Account) { configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL())) - _titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName + let name = account.displayName.isEmpty ? account.username : account.displayName + do { + let mastodonContent = MastodonContent(content: name, emojis: account.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + _titleLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: name) + _titleLabel.configure(content: metaContent) + } _subTitleLabel.text = account.acct } func config(with account: MastodonUser) { configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL())) - _titleLabel.text = account.displayNameWithFallback + do { + let mastodonContent = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + _titleLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback) + _titleLabel.configure(content: metaContent) + } _subTitleLabel.text = account.acct } diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 744900be..e8ff5784 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -8,10 +8,11 @@ import os.log import UIKit import Combine -import ActiveLabel import CoreData import CoreDataStack import MastodonSDK +import MetaTextKit +import MastodonMeta import AuthenticationServices class SettingsViewController: UIViewController, NeedsDependency { @@ -103,12 +104,7 @@ class SettingsViewController: UIViewController, NeedsDependency { return tableView }() - let tableFooterActiveLabel: ActiveLabel = { - let label = ActiveLabel(style: .default) - label.adjustsFontForContentSizeCategory = true - label.textAlignment = .center - return label - }() + let tableFooterLabel = MetaLabel(style: .settingTableFooter) lazy var tableFooterView: UIView = { // init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Height' UIStackView:0x7ffe41e47da0.height == 0) let view = UIStackView(frame: CGRect(x: 0, y: 0, width: 320, height: 320)) @@ -117,8 +113,8 @@ class SettingsViewController: UIViewController, NeedsDependency { view.axis = .vertical view.alignment = .center - tableFooterActiveLabel.delegate = self - view.addArrangedSubview(tableFooterActiveLabel) + tableFooterLabel.linkDelegate = self + view.addArrangedSubview(tableFooterLabel) return view }() @@ -199,7 +195,15 @@ class SettingsViewController: UIViewController, NeedsDependency { let version = instance?.version ?? "-" let link = #"mastodon/mastodon"# let content = L10n.Scene.Settings.Footer.mastodonDescription(link, version) - self.tableFooterActiveLabel.configure(content: content, emojiDict: [:]) + let mastodonContent = MastodonContent(content: content, emojis: [:]) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + self.tableFooterLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: "") + self.tableFooterLabel.configure(content: metaContent) + assertionFailure() + } } .store(in: &disposeBag) } @@ -510,13 +514,16 @@ extension SettingsViewController: SettingsToggleCellDelegate { } } -extension SettingsViewController: ActiveLabelDelegate { - func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - coordinator.present( - scene: .safari(url: URL(string: "https://github.com/mastodon/mastodon")!), - from: self, - transition: .safariPresent(animated: true, completion: nil) - ) +// MARK: - MetaLabelDelegate +extension SettingsViewController: MetaLabelDelegate { + func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) { + switch meta { + case .url(_, _, let url, _): + guard let url = URL(string: url) else { return } + coordinator.present(scene: .safari(url: url), from: self, transition: .safariPresent(animated: true, completion: nil)) + default: + assertionFailure() + } } } diff --git a/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift b/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift index 33ef86dd..d900307a 100644 --- a/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift +++ b/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift @@ -6,19 +6,14 @@ // import UIKit -import ActiveLabel +import Meta +import MetaTextKit final class DoubleTitleLabelNavigationBarTitleView: UIView { let containerView = UIStackView() - let titleLabel: ActiveLabel = { - let label = ActiveLabel(style: .default) - label.font = .systemFont(ofSize: 17, weight: .semibold) - label.textColor = Asset.Colors.Label.primary.color - label.textAlignment = .center - return label - }() + let titleLabel = MetaLabel(style: .titleView) let subtitleLabel: UILabel = { let label = UILabel() @@ -58,9 +53,18 @@ extension DoubleTitleLabelNavigationBarTitleView { containerView.addArrangedSubview(titleLabel) containerView.addArrangedSubview(subtitleLabel) } + + func update(title: String, subtitle: String?) { + titleLabel.configure(content: PlaintextMetaContent(string: title)) + update(subtitle: subtitle) + } - func update(title: String, subtitle: String?, emojiDict: MastodonStatusContent.EmojiDict) { - titleLabel.configure(content: title, emojiDict: emojiDict) + func update(titleMetaContent: MetaContent, subtitle: String?) { + titleLabel.configure(content: titleMetaContent) + update(subtitle: subtitle) + } + + func update(subtitle: String?) { if let subtitle = subtitle { subtitleLabel.text = subtitle subtitleLabel.isHidden = false diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 0964ee3b..c1294bff 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine import AVKit -import ActiveLabel import AlamofireImage import FLAnimatedImage import MetaTextKit @@ -26,7 +25,6 @@ protocol StatusViewDelegate: AnyObject { func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) - func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) } @@ -215,7 +213,7 @@ final class StatusView: UIView { metaText.textView.layer.masksToBounds = false metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment - let paragraphStyle: NSMutableParagraphStyle = { + metaText.paragraphStyle = { let style = NSMutableParagraphStyle() style.lineSpacing = 5 style.paragraphSpacing = 8 @@ -224,12 +222,10 @@ final class StatusView: UIView { metaText.textAttributes = [ .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), .foregroundColor: Asset.Colors.Label.primary.color, - .paragraphStyle: paragraphStyle, ] metaText.linkAttributes = [ .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), .foregroundColor: Asset.Colors.brandBlue.color, - .paragraphStyle: paragraphStyle, ] return metaText }() @@ -559,11 +555,10 @@ extension StatusView { // MARK: - MetaTextViewDelegate extension StatusView: MetaTextViewDelegate { - func metaTextView(_ metaTextView: MetaTextView, didSelectLink link: URL) { + func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") switch metaTextView { case contentMetaText.textView: - guard let meta = Meta(url: link) else { return } delegate?.statusView(self, metaText: contentMetaText, didSelectMeta: meta) default: assertionFailure() @@ -596,14 +591,6 @@ extension StatusView: UITextViewDelegate { } } -// MARK: - ActiveLabelDelegate -extension StatusView: ActiveLabelDelegate { - func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText) - delegate?.statusView(self, activeLabel: activeLabel, didSelectActiveEntity: entity) - } -} - // MARK: - ContentWarningOverlayViewDelegate extension StatusView: ContentWarningOverlayViewDelegate { func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) { diff --git a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift index 9e62fd74..69354047 100644 --- a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift +++ b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift @@ -11,11 +11,10 @@ import UIKit import Combine import AsyncDisplayKit import CoreDataStack -import ActiveLabel import func AVFoundation.AVMakeRect protocol StatusNodeDelegate: AnyObject { - func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) + //func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) } final class StatusNode: ASCellNode { @@ -29,21 +28,21 @@ final class StatusNode: ASCellNode { static let avatarImageSize = CGSize(width: 42, height: 42) static let avatarImageCornerRadius: CGFloat = 4 - static let statusContentAppearance: MastodonStatusContent.Appearance = { - let linkAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), - .foregroundColor: Asset.Colors.brandBlue.color - ] - return MastodonStatusContent.Appearance( - attributes: [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), - .foregroundColor: Asset.Colors.Label.primary.color - ], - urlAttributes: linkAttributes, - hashtagAttributes: linkAttributes, - mentionAttributes: linkAttributes - ) - }() +// static let statusContentAppearance: MastodonStatusContent.Appearance = { +// let linkAttributes: [NSAttributedString.Key: Any] = [ +// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), +// .foregroundColor: Asset.Colors.brandBlue.color +// ] +// return MastodonStatusContent.Appearance( +// attributes: [ +// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), +// .foregroundColor: Asset.Colors.Label.primary.color +// ], +// urlAttributes: linkAttributes, +// hashtagAttributes: linkAttributes, +// mentionAttributes: linkAttributes +// ) +// }() let avatarImageNode: ASNetworkImageNode = { let node = ASNetworkImageNode() @@ -112,13 +111,14 @@ final class StatusNode: ASCellNode { .font: UIFont.systemFont(ofSize: 15, weight: .regular) ]) - statusContentTextNode.metaEditableTextNodeDelegate = self - if let parseResult = try? MastodonStatusContent.parse( - content: (status.reblog ?? status).content, - emojiDict: (status.reblog ?? status).emojiDict - ) { - statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance) - } + // FIXME: + // statusContentTextNode.metaEditableTextNodeDelegate = self +// if let parseResult = try? MastodonStatusContent.parse( +// content: (status.reblog ?? status).content, +// emojiDict: (status.reblog ?? status).emojiDict +// ) { +// statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance) +// } for imageNode in mediaMultiplexImageNodes { imageNode.dataSource = self @@ -200,29 +200,19 @@ final class StatusNode: ASCellNode { } -//extension StatusNode: ASImageDownloaderProtocol { -// func downloadImage(with URL: URL, callbackQueue: DispatchQueue, downloadProgress: ASImageDownloaderProgress?, completion: @escaping ASImageDownloaderCompletion) -> Any? { -// -// } -// -// func cancelImageDownload(forIdentifier downloadIdentifier: Any) { -// +// MARK: - ASEditableTextNodeDelegate +//extension StatusNode: ASMetaEditableTextNodeDelegate { +// func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { +// guard let activityEntityType = ActiveEntityType(url: URL) else { +// return false +// } +// defer { +// delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType) +// } +// return false // } //} -// MARK: - ASEditableTextNodeDelegate -extension StatusNode: ASMetaEditableTextNodeDelegate { - func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { - guard let activityEntityType = ActiveEntityType(url: URL) else { - return false - } - defer { - delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType) - } - return false - } -} - // MARK: - ASMultiplexImageNodeDataSource extension StatusNode: ASMultiplexImageNodeDataSource { func multiplexImageNode(_ imageNode: ASMultiplexImageNode, urlForImageIdentifier imageIdentifier: ASImageIdentifier) -> URL? { diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 3a186f00..8e9f4a64 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -11,7 +11,6 @@ import AVKit import Combine import CoreData import CoreDataStack -import ActiveLabel import Meta import MetaTextKit @@ -27,7 +26,6 @@ protocol StatusTableViewCellDelegate: AnyObject { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) - func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) @@ -328,10 +326,6 @@ extension StatusTableViewCell: StatusViewDelegate { func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button) } - - func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { - delegate?.statusTableViewCell(self, statusView: statusView, activeLabel: activeLabel, didSelectActiveEntity: entity) - } func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) { delegate?.statusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta) diff --git a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift index 30f7688a..905e1db3 100644 --- a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift +++ b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift @@ -11,7 +11,8 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit -import ActiveLabel +import MetaTextKit +import MastodonMeta protocol SuggestionAccountTableViewCellDelegate: AnyObject { func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell) @@ -29,13 +30,7 @@ final class SuggestionAccountTableViewCell: UITableViewCell { return imageView }() - let titleLabel: ActiveLabel = { - let label = ActiveLabel(style: .statusName) - label.textColor = Asset.Colors.brandBlue.color - label.font = .systemFont(ofSize: 17, weight: .semibold) - label.lineBreakMode = .byTruncatingTail - return label - }() + let titleLabel = MetaLabel(style: .statusName) let subTitleLabel: UILabel = { let label = UILabel() @@ -152,8 +147,15 @@ extension SuggestionAccountTableViewCell { imageTransition: .crossDissolve(0.2) ) } - titleLabel.configure(content: account.displayNameWithFallback, emojiDict: account.emojiDict) - subTitleLabel.text = account.acct + let mastodonContent = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + titleLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback) + titleLabel.configure(content: metaContent) + } + subTitleLabel.text = "@" + account.acct button.isSelected = isSelected button.publisher(for: .touchUpInside) .sink { [weak self] _ in diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index 71a7de9e..c1d580a7 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreData import AVKit +import MastodonMeta final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -84,18 +85,27 @@ extension ThreadViewController { tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - - viewModel.navigationBarTitle - .receive(on: DispatchQueue.main) - .sink { [weak self] tuple in - guard let self = self else { return } - guard let (title, emojiDict) = tuple else { - self.titleView.update(title: L10n.Scene.Thread.backTitle, subtitle: nil, emojiDict: [:]) - return - } - self.titleView.update(title: title, subtitle: nil, emojiDict: emojiDict) + + Publishers.CombineLatest( + viewModel.navigationBarTitle, + viewModel.navigationBarTitleEmojiMeta + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] title, emojiMeta in + guard let self = self else { return } + guard let title = title else { + self.titleView.update(title: "", subtitle: nil) + return } - .store(in: &disposeBag) + let mastodonContent = MastodonContent(content: title, emojis: emojiMeta ?? [:]) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + self.titleView.update(titleMetaContent: metaContent, subtitle: nil) + } catch { + assertionFailure() + } + } + .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index 26be5643..1c4a7071 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -12,6 +12,7 @@ import CoreData import CoreDataStack import GameplayKit import MastodonSDK +import MastodonMeta class ThreadViewModel { @@ -45,7 +46,8 @@ class ThreadViewModel { let ancestorItems = CurrentValueSubject<[Item], Never>([]) let descendantNodes = CurrentValueSubject<[LeafNode], Never>([]) let descendantItems = CurrentValueSubject<[Item], Never>([]) - let navigationBarTitle: CurrentValueSubject<(String, MastodonStatusContent.EmojiDict)?, Never> + let navigationBarTitle: CurrentValueSubject + let navigationBarTitleEmojiMeta: CurrentValueSubject init(context: AppContext, optionalStatus: Status?) { self.context = context @@ -53,8 +55,8 @@ class ThreadViewModel { self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) }) self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil) self.navigationBarTitle = CurrentValueSubject( - optionalStatus.flatMap { (L10n.Scene.Thread.title($0.author.displayNameWithFallback), $0.author.emojiDict) } - ) + optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }) + self.navigationBarTitleEmojiMeta = CurrentValueSubject(optionalStatus.flatMap { $0.author.emojiMeta } ?? [:]) // bind fetcher domain context.authenticationService.activeMastodonAuthenticationBox @@ -85,7 +87,8 @@ class ThreadViewModel { return } self.rootNode.value = RootNode(domain: status.domain, statusID: status.id, replyToID: status.inReplyToID) - self.navigationBarTitle.value = (L10n.Scene.Thread.title(status.author.displayNameWithFallback), status.author.emojiDict) + self.navigationBarTitle.value = L10n.Scene.Thread.title(status.author.displayNameWithFallback) + self.navigationBarTitleEmojiMeta.value = status.author.emojiMeta ?? [:] } } .store(in: &disposeBag) diff --git a/Mastodon/Service/StatusContentCacheService.swift b/Mastodon/Service/StatusContentCacheService.swift deleted file mode 100644 index ff17b047..00000000 --- a/Mastodon/Service/StatusContentCacheService.swift +++ /dev/null @@ -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() - - let cache = NSCache() - - 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 - } - } -} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 7db669e5..d4682ed5 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -38,7 +38,6 @@ class AppContext: ObservableObject { let placeholderImageCacheService = PlaceholderImageCacheService() let blurhashImageCacheService = BlurhashImageCacheService() - let statusContentCacheService = StatusContentCacheService() let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable!