From c1c10ce660ffba7cac2f3331717785608ec0edb7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 19 Jun 2021 00:14:41 +0200 Subject: [PATCH 01/18] Add boop sound for notifications --- Mastodon.xcodeproj/project.pbxproj | 4 ++++ .../xcshareddata/swiftpm/Package.resolved | 6 +++--- Mastodon/Resources/BoopSound.caf | Bin 0 -> 9332 bytes NotificationService/NotificationService.swift | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 Mastodon/Resources/BoopSound.caf diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 98caf2775..e8177de1a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; }; 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */; }; 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; + 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; }; 2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */; }; @@ -595,6 +596,7 @@ 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = ""; }; 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = ""; }; 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; + 164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = ""; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+diffable.swift"; sourceTree = ""; }; 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+LoadLatestState.swift"; sourceTree = ""; }; @@ -1680,6 +1682,7 @@ DB3D0FF825BAA6B200EAA174 /* Resources */ = { isa = PBXGroup; children = ( + 164F0EBB267D4FE400249499 /* BoopSound.caf */, DB427DDE25BAA00100D1B89D /* Assets.xcassets */, DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */, DB3D100F25BAA75E00EAA174 /* Localizable.strings */, @@ -2719,6 +2722,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */, DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */, DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */, DB427DDF25BAA00100D1B89D /* Assets.xcassets in Resources */, diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1ca83d0d7..a64f6ed7d 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire.git", "state": { "branch": null, - "revision": "f96b619bcb2383b43d898402283924b80e2c4bae", - "version": "5.4.3" + "revision": "4d19ad82f80cc71ff829b941ded114c56f4f604c", + "version": "5.4.2" } }, { @@ -60,7 +60,7 @@ "repositoryURL": "https://github.com/onevcat/Kingfisher.git", "state": { "branch": null, - "revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8", + "revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e", "version": "6.2.1" } }, diff --git a/Mastodon/Resources/BoopSound.caf b/Mastodon/Resources/BoopSound.caf new file mode 100644 index 0000000000000000000000000000000000000000..f0a3ebc420394579564cfe6ef50104347c432745 GIT binary patch literal 9332 zcmeHLL5$;88Gg=8xA81({hXoQ>>`xdp0Z0(wf1wUDtc(g84ldK?vcxC+DcW0fVL79 zr_8oMX1WDuOJQEJU4|BwiJbv$_Y~Xd9uN{8XL{k*ti%Nn(r`cpsiITHe7`e8xx%69 zA(oPO{NDTh|KI=p-~ZmMUVQRNrZD#S6VI-)=~a99Pwf`nKJ|@@?(`8e@cS@6n910; zEm?1DjV1~d9ff)ib z1ZD`#5SStG$sq9f#pfTrW8-H3%K~*`n5NMc;H+EQ{U@XuA4KpOeTNQj4OUX%9sj9SwrXVCt;VSQ_a7u%#3O<3R z;a=6QEGUx8yoGlt)lR^wJkV5_&^nsLit2T)$UxyI^VS?Y-RWp&IyJ_XpvqN6m0VE+ zsdVdC77!0?D-tm&odw>JxRPGsN4=HhcAc<_xLv*(=mPP5gY%XU!Jhdb>+ymQiC zyuO!z|7>Mzx3~OyCac~S)4WZ8t9#wN?U#tY`v(|R6 z9N)O>FK1h?*oJv2JHtfA}^Tz7s-#oST!e_P~E*ry>{c={9H_kkIdv~4X-(z+t zp6!fU-+rrF3l@84etY?$zgP17hb>ljJkJx_{hnbsy58EJOeX0hOVTt;2FW0fI$xaXL!JNt$IzmLz`-2T|1Rw(GvT0-KiQIhbn-VHzda z-Op0~%XEafFr7tN7>2UVmz%!lFAEnwctTj(Iny*blkg&&BVvr<*zugPV<=V1~*)H4=a2nGMlPlZ1<4Klg{_+FI*Q>Jx2&}$CfaZAlu8vSyH$RHxON~t7>A!_uQuV&~jbxXtsm7 z$-C(oP7Jc$7;`Z>{Kx0JZRRxLL{l{3vv3{HFbzX#4aeg&-5Ct#ma-V(A;1t`XvJJi zF2HbvXE^nRp4J}T%EoYF04TDUun4>B+9O`WZNOje&>);m%r^8=Yq*d209=~F334Np z>mU4-Ijf$F^Z_}*Y+3-0=8ne`*iDliRtFeyf-775IluN|0?K zjM7?6cBdF;hp#0`8YiIKDf=;?Oc&gs^cf%=>rdu`bOe{Dj7wvXsJl_-zVCx@zeO3 zy+iGoas@MtItzv)U<|7S8-P31QGb1@IV;lumr*0Y4P4fhP0Sso%AEx}rz8T4)Uwl9 zDS#t1mXSW?4k0Q$81BJvitK^+Sp?#PMCCfs!p}9NZ%TYv29^6%s}uMDCxBg;9KIBH z`)%8wQtm$~5jto6_aFhIl)Zdhe;`Izs$6_kG{7NBiRlu`$_cZsB+wRO25^Q56xFw( z)~KGK7x~MS>m>m;8@dh+NDv@_jJXjhQF!>`M!5=zNnQXnW+T_;Vo8zlh-xIw*vVuk zi%ABeUL?72SCJB^0L%hpk}cM%$QZ%xft^QH0yq*`*InPjO!%=ulv_4T$JFaGj(-&s zmnkQ4y4y|qU70gxJ4>SBdz29+Ig$) zEFpD}45*7$vFy6Z(T*@2!DgC<`6Y-z8ilb`6=B==)SX4q)RAUO*o&NZLUh99YCP(t z!<%v#Uyb^*pRnP5SE}=0`GVrucl&LV-KMRyO7^8kZ+!iQ-Ww0y_%*wgq#LJCq_yqV zNUE2<*2vquZ|v}AK70PH@6Pu&do_RU!Fv`eSH;y!*Mg1h^9N`0mkw&d=H*xJ{P8tK zEtS3b>b+%SJA6d{-P0#d|Ni;!JfPg+v}Cde>@u{FQ#Q$_2aPujLu*4L#>u$UK&d5B z2=Vg^?W0BnHI4T`K&1Z0Ro{bus@%*S3OcU#GAP_c#Q`W;?xuTG$wfm#yY-qyK%}R2ww;Z~*_?&e0*=RhRU^&y zpk1_aP-FoHn(9ZtfYaeRKLUfwNavdwTrTPd9{=+ynisYcg+6&kUE4$-A}u8ilTx!A z2dA+mfN($uOr@Q?TZ~fvdK;$*Mo;8FYdWTdwe66=AXdTh26Lz#L3BK4?ixwta6H}k zKsv%MK%Vwu2Yl4q^NYjL-Xwu!5HY};#|kb(f2fo85pC%V$r=hi!mg%?_k!sF(x_vb zlV^EKbKKcom;WbvMQ_&$j0)ui-mJ63|| zZe|X*Rxaz{Yd>i!RJK`l7`+LDS#Iqco_}6BBay`EM~O-!HtU*CnD;tkW~AII}Qm@EDffqxs!t{DQKHU$0+ DA9;j2 literal 0 HcmV?d00001 diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 5bc7252b0..4e09e4939 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -57,7 +57,7 @@ class NotificationService: UNNotificationServiceExtension { bestAttemptContent.title = notification.title bestAttemptContent.subtitle = "" bestAttemptContent.body = notification.body - bestAttemptContent.sound = .default + bestAttemptContent.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: "BoopSound.caf")) bestAttemptContent.userInfo["plaintext"] = plaintextData UserDefaults.shared.notificationBadgeCount += 1 From 1a3135b9981c3e6db930ee5186399abb83c8f843 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 19 Jun 2021 18:33:29 +0800 Subject: [PATCH 02/18] feat: [WIP] migrate to Texture (AsyncDisplayKit) for better performance --- Mastodon.xcodeproj/project.pbxproj | 78 ++++ .../xcschemes/xcschememanagement.plist | 8 +- .../xcshareddata/swiftpm/Package.resolved | 18 + .../Diffiable/DataSource/ASTableNode.swift | 81 ++++ .../TableNodeDiffableDataSource.swift | 111 ++++++ Mastodon/Diffiable/Item/Item.swift | 3 + .../Diffiable/Section/StatusSection.swift | 28 ++ .../NSDiffableDataSourceSnapshot.swift | 24 ++ ...meTimelineViewController+DebugAction.swift | 29 +- .../HomeTimelineViewController+Provider.swift | 2 +- .../HomeTimelineViewController.swift | 377 +++++++++--------- .../HomeTimelineViewModel+Diffable.swift | 92 ++--- .../HomeTimeline/HomeTimelineViewModel.swift | 15 +- .../Scene/Share/View/Node/StatusNode.swift | 109 +++++ .../View/Node/TimelineBottomLoaderNode.swift | 37 ++ .../View/Node/TimelineMiddleLoaderNode.swift | 50 +++ Mastodon/State/AppContext.swift | 4 + Mastodon/Supporting Files/AppDelegate.swift | 2 + Mastodon/Vender/ActivityIndicatorNode.swift | 71 ++++ Podfile | 3 +- Podfile.lock | 46 ++- 21 files changed, 933 insertions(+), 255 deletions(-) create mode 100644 Mastodon/Diffiable/DataSource/ASTableNode.swift create mode 100644 Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift create mode 100644 Mastodon/Extension/NSDiffableDataSourceSnapshot.swift create mode 100644 Mastodon/Scene/Share/View/Node/StatusNode.swift create mode 100644 Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift create mode 100644 Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift create mode 100644 Mastodon/Vender/ActivityIndicatorNode.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 98caf2775..5201c5409 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -407,6 +407,15 @@ DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; }; DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; + DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC6482267D0B21007FE9FD /* DifferenceKit */; }; + DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6484267D0F9E007FE9FD /* StatusNode.swift */; }; + DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6487267D388B007FE9FD /* ASTableNode.swift */; }; + DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */; }; + DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */; }; + DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */; }; + DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */; }; + DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */; }; + DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC649D267DFE43007FE9FD /* DiffableDataSources */; }; DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; }; DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; }; DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */; }; @@ -981,6 +990,13 @@ DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = ""; }; DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; + DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = ""; }; + DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = ""; }; + DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSDiffableDataSourceSnapshot.swift; sourceTree = ""; }; + DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = ""; }; + DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = ""; }; + DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = ""; }; + DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorNode.swift; sourceTree = ""; }; DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = ""; }; DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = ""; }; DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProviderFacade.swift; sourceTree = ""; }; @@ -1065,8 +1081,10 @@ DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, + DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, + DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */, 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */, @@ -1337,6 +1355,7 @@ DB51D171262832380062B7A1 /* BlurHashEncode.swift */, DB6180EC26391C6C0018D199 /* TransitioningMath.swift */, DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */, + DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */, ); path = Vender; sourceTree = ""; @@ -1414,6 +1433,7 @@ 2D76319D25C151F600929FB9 /* Section */, 2D7631B125C159E700929FB9 /* Item */, DBCBED2226132E1D00B49291 /* FetchedResultsController */, + DBAC6490267DC84F007FE9FD /* DataSource */, ); path = Diffiable; sourceTree = ""; @@ -1464,6 +1484,7 @@ DB87D45C2609DE6600D12C0D /* TextField */, DB1D187125EF5BBD003F1F23 /* TableView */, 2D7631A625C1533800929FB9 /* TableviewCell */, + DBAC6486267D0FAC007FE9FD /* Node */, ); path = View; sourceTree = ""; @@ -2167,6 +2188,7 @@ DBCC3B35261440BA0045B23D /* UINavigationController.swift */, DB6D1B23263684C600ACB481 /* UserDefaults.swift */, DB97131E2666078B00BD1E90 /* Date.swift */, + DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */, ); path = Extension; sourceTree = ""; @@ -2328,6 +2350,25 @@ path = View; sourceTree = ""; }; + DBAC6486267D0FAC007FE9FD /* Node */ = { + isa = PBXGroup; + children = ( + DBAC6484267D0F9E007FE9FD /* StatusNode.swift */, + DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */, + DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */, + ); + path = Node; + sourceTree = ""; + }; + DBAC6490267DC84F007FE9FD /* DataSource */ = { + isa = PBXGroup; + children = ( + DBAC6487267D388B007FE9FD /* ASTableNode.swift */, + DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */, + ); + path = DataSource; + sourceTree = ""; + }; DBAE3F742615DD63004B8251 /* UserProvider */ = { isa = PBXGroup; children = ( @@ -2510,6 +2551,8 @@ DBB525072611EAC0002F1F29 /* Tabman */, DB6F5E31264E7410009108F4 /* TwitterTextEditor */, DBAEDE5E267A0B1500D25FF5 /* Nuke */, + DBAC6482267D0B21007FE9FD /* DifferenceKit */, + DBAC649D267DFE43007FE9FD /* DiffableDataSources */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2698,6 +2741,8 @@ DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */, + DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */, + DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -2992,9 +3037,11 @@ DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */, DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */, + DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */, 2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */, 0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */, DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */, + DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */, DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */, @@ -3036,6 +3083,7 @@ DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */, DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, + DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */, DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */, DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */, 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, @@ -3056,7 +3104,9 @@ DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, + DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */, DB97131F2666078B00BD1E90 /* Date.swift in Sources */, + DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */, DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, @@ -3129,6 +3179,7 @@ 5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */, 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */, + DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */, DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */, DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */, 5D0393902612D259007FE196 /* WebViewController.swift in Sources */, @@ -3262,6 +3313,7 @@ 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, + DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */, 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */, DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */, @@ -4118,6 +4170,22 @@ minimumVersion = 1.4.1; }; }; + DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ra1028/DifferenceKit.git"; + requirement = { + kind = exactVersion; + version = 1.2.0; + }; + }; + DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/MainasuK/DiffableDataSources.git"; + requirement = { + branch = "feature/async-display-table"; + kind = branch; + }; + }; DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/Nuke.git"; @@ -4201,6 +4269,16 @@ package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; productName = "UITextView+Placeholder"; }; + DBAC6482267D0B21007FE9FD /* DifferenceKit */ = { + isa = XCSwiftPackageProductDependency; + package = DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */; + productName = DifferenceKit; + }; + DBAC649D267DFE43007FE9FD /* DiffableDataSources */ = { + isa = XCSwiftPackageProductDependency; + package = DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */; + productName = DiffableDataSources; + }; DBAEDE5E267A0B1500D25FF5 /* Nuke */ = { isa = XCSwiftPackageProductDependency; package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 2fb2d9806..50a025853 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 14 + 19 CoreDataStack.xcscheme_^#shared#^_ orderHint - 17 + 18 Mastodon - RTL.xcscheme_^#shared#^_ @@ -27,12 +27,12 @@ Mastodon.xcscheme_^#shared#^_ orderHint - 2 + 17 NotificationService.xcscheme_^#shared#^_ orderHint - 16 + 20 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1ca83d0d7..f46eff823 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -46,6 +46,24 @@ "version": "0.1.1" } }, + { + "package": "DiffableDataSources", + "repositoryURL": "https://github.com/MainasuK/DiffableDataSources.git", + "state": { + "branch": "feature/async-display-table", + "revision": "73393a97690959d24387c95594c045c62d9c47cf", + "version": null + } + }, + { + "package": "DifferenceKit", + "repositoryURL": "https://github.com/ra1028/DifferenceKit.git", + "state": { + "branch": null, + "revision": "62745d7780deef4a023a792a1f8f763ec7bf9705", + "version": "1.2.0" + } + }, { "package": "KeychainAccess", "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", diff --git a/Mastodon/Diffiable/DataSource/ASTableNode.swift b/Mastodon/Diffiable/DataSource/ASTableNode.swift new file mode 100644 index 000000000..f2849cfe8 --- /dev/null +++ b/Mastodon/Diffiable/DataSource/ASTableNode.swift @@ -0,0 +1,81 @@ +// +// ASTableNode.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-19. +// + +import UIKit +import AsyncDisplayKit +import DifferenceKit +import DiffableDataSources + +extension ASTableNode: ReloadableTableView { + public func reload( + using stagedChangeset: StagedChangeset, + deleteSectionsAnimation: @autoclosure () -> UITableView.RowAnimation, + insertSectionsAnimation: @autoclosure () -> UITableView.RowAnimation, + reloadSectionsAnimation: @autoclosure () -> UITableView.RowAnimation, + deleteRowsAnimation: @autoclosure () -> UITableView.RowAnimation, + insertRowsAnimation: @autoclosure () -> UITableView.RowAnimation, + reloadRowsAnimation: @autoclosure () -> UITableView.RowAnimation, + interrupt: ((Changeset) -> Bool)? = nil, + setData: (C) -> Void + ) { + if case .none = view.window, let data = stagedChangeset.last?.data { + setData(data) + return reloadData() + } + + for changeset in stagedChangeset { + if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { + setData(data) + return reloadData() + } + + func updates() { + setData(changeset.data) + + if !changeset.sectionDeleted.isEmpty { + deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation()) + } + + if !changeset.sectionInserted.isEmpty { + insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation()) + } + + if !changeset.sectionUpdated.isEmpty { + reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation()) + } + + for (source, target) in changeset.sectionMoved { + moveSection(source, toSection: target) + } + + if !changeset.elementDeleted.isEmpty { + deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation()) + } + + if !changeset.elementInserted.isEmpty { + insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation()) + } + + if !changeset.elementUpdated.isEmpty { + reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation()) + } + + for (source, target) in changeset.elementMoved { + moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section)) + } + } + + if isNodeLoaded { + view.beginUpdates() + updates() + view.endUpdates(animated: false, completion: nil) + } else { + updates() + } + } + } +} diff --git a/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift b/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift new file mode 100644 index 000000000..508f07de9 --- /dev/null +++ b/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift @@ -0,0 +1,111 @@ +// +// TableNodeDiffableDataSource.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-19. +// + +import UIKit +import AsyncDisplayKit +import DiffableDataSources + +open class TableNodeDiffableDataSource: NSObject, ASTableDataSource { + /// The type of closure providing the cell. + public typealias CellProvider = (ASTableNode, IndexPath, ItemIdentifierType) -> ASCellNodeBlock? + + /// The default animation to updating the views. + public var defaultRowAnimation: UITableView.RowAnimation = .automatic + + private weak var tableNode: ASTableNode? + private let cellProvider: CellProvider + private let core = DiffableDataSourceCore() + + /// Creates a new data source. + /// + /// - Parameters: + /// - tableView: A table view instance to be managed. + /// - cellProvider: A closure to dequeue the cell for rows. + public init(tableNode: ASTableNode, cellProvider: @escaping CellProvider) { + self.tableNode = tableNode + self.cellProvider = cellProvider + super.init() + + tableNode.dataSource = self + } + + /// Applies given snapshot to perform automatic diffing update. + /// + /// - Parameters: + /// - snapshot: A snapshot object to be applied to data model. + /// - animatingDifferences: A Boolean value indicating whether to update with + /// diffing animation. + /// - completion: An optional completion block which is called when the complete + /// performing updates. + public func apply(_ snapshot: DiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) { + core.apply(snapshot, view: tableNode, animatingDifferences: animatingDifferences, completion: completion) + } + + /// Returns a new snapshot object of current state. + /// + /// - Returns: A new snapshot object of current state. + public func snapshot() -> DiffableDataSourceSnapshot { + return core.snapshot() + } + + /// Returns an item identifier for given index path. + /// + /// - Parameters: + /// - indexPath: An index path for the item identifier. + /// + /// - Returns: An item identifier for given index path. + public func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { + return core.itemIdentifier(for: indexPath) + } + + /// Returns an index path for given item identifier. + /// + /// - Parameters: + /// - itemIdentifier: An identifier of item. + /// + /// - Returns: An index path for given item identifier. + public func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? { + return core.indexPath(for: itemIdentifier) + } + + /// Returns the number of sections in the data source. + /// + /// - Parameters: + /// - tableNode: A table node instance managed by `self`. + /// + /// - Returns: The number of sections in the data source. + public func numberOfSections(in tableNode: ASTableNode) -> Int { + return core.numberOfSections() + } + + /// Returns the number of items in the specified section. + /// + /// - Parameters: + /// - tableNode: A table node instance managed by `self`. + /// - section: An index of section. + /// + /// - Returns: The number of items in the specified section. + public func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { + return core.numberOfItems(inSection: section) + } + + /// Returns a cell for row at specified index path. + /// + /// - Parameters: + /// - tableView: A table view instance managed by `self`. + /// - indexPath: An index path for cell. + /// + /// - Returns: A cell for row at specified index path. + open func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + let itemIdentifier = core.unsafeItemIdentifier(for: indexPath) + guard let block = cellProvider(tableNode, indexPath, itemIdentifier) else { + fatalError("UITableView dataSource returned a nil cell for row at index path: \(indexPath), tableNode: \(tableNode), itemIdentifier: \(itemIdentifier)") + } + + return block + } +} diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 04a1262d5..fe40cfd6c 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -10,6 +10,7 @@ import CoreData import CoreDataStack import Foundation import MastodonSDK +import DifferenceKit /// Note: update Equatable when change case enum Item { @@ -158,3 +159,5 @@ extension Item: Hashable { } } } + +extension Item: Differentiable { } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index c5d0eb19b..5cc2ed198 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -11,6 +11,7 @@ import CoreDataStack import os.log import UIKit import AVKit +import AsyncDisplayKit import Nuke protocol StatusCell: DisposeBagCollectable { @@ -23,6 +24,33 @@ enum StatusSection: Equatable, Hashable { } extension StatusSection { + static func tableNodeDiffableDataSource( + tableNode: ASTableNode, + managedObjectContext: NSManagedObjectContext + ) -> TableNodeDiffableDataSource { + TableNodeDiffableDataSource(tableNode: tableNode) { tableNode, indexPath, item in + switch item { + case .homeTimelineIndex(let objectID, let attribute): + guard let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { + return { ASCellNode() } + } + let status = homeTimelineIndex.status + + return { () -> ASCellNode in + let cellNode = StatusNode(status: status) + return cellNode + } + case .homeMiddleLoader: + return { TimelineMiddleLoaderNode() } + case .bottomLoader: + return { TimelineBottomLoaderNode() } + default: + return { ASCellNode() } + } + } + } + + static func tableViewDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, diff --git a/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift b/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift new file mode 100644 index 000000000..c2ff341d9 --- /dev/null +++ b/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift @@ -0,0 +1,24 @@ +// +// NSDiffableDataSourceSnapshot.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-19. +// + +import UIKit + +//extension NSDiffableDataSourceSnapshot { +// func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { +// guard 0.. NSManagedObjectID? in switch item { case .homeTimelineIndex(let objectID, _): return objectID @@ -354,5 +354,30 @@ extension HomeTimelineViewController { transition: .modal(animated: true, completion: nil) ) } + + @objc func signOutAction(_ sender: UIAction) { + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + + context.authenticationService.signOutMastodonUser( + domain: activeMastodonAuthenticationBox.domain, + userID: activeMastodonAuthenticationBox.userID + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + assertionFailure(error.localizedDescription) + case .success(let isSignOut): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail") + guard isSignOut else { return } + self.coordinator.setup() + self.coordinator.setupOnboardingIfNeeds(animated: true) + } + } + .store(in: &disposeBag) + } } #endif diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift index d735d5843..38d843114 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift @@ -53,7 +53,7 @@ extension HomeTimelineViewController: StatusProvider { } var tableViewDiffableDataSource: UITableViewDiffableDataSource? { - return viewModel.diffableDataSource + return nil } func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 529f2d81c..68afb41e2 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -14,12 +14,13 @@ import CoreDataStack import GameplayKit import MastodonSDK import AlamofireImage +import AsyncDisplayKit #if DEBUG import GDPerformanceView_Swift #endif -final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { +final class HomeTimelineViewController: ASDKViewController, NeedsDependency, MediaPreviewableViewController { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -53,17 +54,18 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate) return barButtonItem }() - - let tableView: UITableView = { - let tableView = ControlContainableTableView() - tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) - tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self)) - tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) - tableView.rowHeight = UITableView.automaticDimension - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - return tableView - }() + + var tableView: UITableView { node.view } + //let tableView: UITableView = { + // let tableView = ControlContainableTableView() + // tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) + // tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self)) + // tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + // tableView.rowHeight = UITableView.automaticDimension + // tableView.separatorStyle = .none + // tableView.backgroundColor = .clear + // return tableView + //}() let publishProgressView: UIProgressView = { let progressView = UIProgressView(progressViewStyle: .bar) @@ -72,7 +74,16 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media }() let refreshControl = UIRefreshControl() - + + + override init() { + super.init(node: ASTableNode()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) } @@ -83,13 +94,15 @@ extension HomeTimelineViewController { override func viewDidLoad() { super.viewDidLoad() + + node.allowsSelection = true title = L10n.Scene.HomeTimeline.title view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color navigationItem.leftBarButtonItem = settingBarButtonItem navigationItem.titleView = titleView titleView.delegate = self - + viewModel.homeTimelineNavigationBarTitleViewModel.state .removeDuplicates() .receive(on: DispatchQueue.main) @@ -98,52 +111,56 @@ extension HomeTimelineViewController { self.titleView.configure(state: state) } .store(in: &disposeBag) - + #if DEBUG // long press to trigger debug menu settingBarButtonItem.menu = debugMenu PerformanceMonitor.shared().delegate = self - + #else settingBarButtonItem.target = self settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) #endif - + navigationItem.rightBarButtonItem = composeBarButtonItem composeBarButtonItem.target = self composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:)) - - tableView.refreshControl = refreshControl + + node.view.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) - - tableView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableView) - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - publishProgressView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(publishProgressView) - NSLayoutConstraint.activate([ - publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - ]) - - viewModel.tableView = tableView +// +// tableView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(tableView) +// NSLayoutConstraint.activate([ +// tableView.topAnchor.constraint(equalTo: view.topAnchor), +// tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// +// publishProgressView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(publishProgressView) +// NSLayoutConstraint.activate([ +// publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), +// publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// ]) +// +// viewModel.tableView = tableView + viewModel.tableNode = node viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self - tableView.delegate = self - tableView.prefetchDataSource = self + node.delegate = self viewModel.setupDiffableDataSource( - for: tableView, + tableNode: node, dependency: self, statusTableViewCellDelegate: self, timelineMiddleLoaderTableViewCellDelegate: self ) + +// tableView.delegate = self +// tableView.prefetchDataSource = self + // bind refresh control viewModel.isFetchingLatestTimeline .receive(on: DispatchQueue.main) @@ -157,88 +174,88 @@ extension HomeTimelineViewController { } } .store(in: &disposeBag) - - viewModel.homeTimelineNavigationBarTitleViewModel.publishingProgress - .receive(on: DispatchQueue.main) - .sink { [weak self] progress in - guard let self = self else { return } - guard progress > 0 else { - let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) - dismissAnimator.addAnimations { - self.publishProgressView.alpha = 0 - } - dismissAnimator.addCompletion { _ in - self.publishProgressView.setProgress(0, animated: false) - } - dismissAnimator.startAnimation() - return - } - if self.publishProgressView.alpha == 0 { - let progressAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut) - progressAnimator.addAnimations { - self.publishProgressView.alpha = 1 - } - progressAnimator.startAnimation() - } - - self.publishProgressView.setProgress(progress, animated: true) - } - .store(in: &disposeBag) - - viewModel.timelineIsEmpty - .receive(on: DispatchQueue.main) - .sink { [weak self] isEmpty in - if isEmpty { - self?.showEmptyView() - } else { - self?.emptyView.removeFromSuperview() - } - } - .store(in: &disposeBag) + +// viewModel.homeTimelineNavigationBarTitleViewModel.publishingProgress +// .receive(on: DispatchQueue.main) +// .sink { [weak self] progress in +// guard let self = self else { return } +// guard progress > 0 else { +// let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) +// dismissAnimator.addAnimations { +// self.publishProgressView.alpha = 0 +// } +// dismissAnimator.addCompletion { _ in +// self.publishProgressView.setProgress(0, animated: false) +// } +// dismissAnimator.startAnimation() +// return +// } +// if self.publishProgressView.alpha == 0 { +// let progressAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut) +// progressAnimator.addAnimations { +// self.publishProgressView.alpha = 1 +// } +// progressAnimator.startAnimation() +// } +// +// self.publishProgressView.setProgress(progress, animated: true) +// } +// .store(in: &disposeBag) +// +// viewModel.timelineIsEmpty +// .receive(on: DispatchQueue.main) +// .sink { [weak self] isEmpty in +// if isEmpty { +// self?.showEmptyView() +// } else { +// self?.emptyView.removeFromSuperview() +// } +// } +// .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - aspectViewWillAppear(animated) - - // needs trigger manually after onboarding dismiss - setNeedsStatusBarAppearanceUpdate() - - if (viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty { - viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) - } +// aspectViewWillAppear(animated) +// +// // needs trigger manually after onboarding dismiss +// setNeedsStatusBarAppearanceUpdate() +// +// if (viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty { +// viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) +// } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppear.send() - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 { - self.viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) - } - } +// viewModel.viewDidAppear.send() +// +// DispatchQueue.main.async { [weak self] in +// guard let self = self else { return } +// if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 { +// self.viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) +// } +// } } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - aspectViewDidDisappear(animated) +// aspectViewDidDisappear(animated) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - coordinator.animate { _ in - // do nothing - } completion: { _ in - // fix AutoLayout cell height not update after rotate issue - self.viewModel.cellFrameCache.removeAllObjects() - self.tableView.reloadData() - } +// coordinator.animate { _ in +// // do nothing +// } completion: { _ in +// // fix AutoLayout cell height not update after rotate issue +// self.viewModel.cellFrameCache.removeAllObjects() +// self.tableView.reloadData() +// } } } @@ -315,100 +332,75 @@ extension HomeTimelineViewController { return } } - - @objc func signOutAction(_ sender: UIAction) { - guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - - context.authenticationService.signOutMastodonUser( - domain: activeMastodonAuthenticationBox.domain, - userID: activeMastodonAuthenticationBox.userID - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .failure(let error): - assertionFailure(error.localizedDescription) - case .success(let isSignOut): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail") - guard isSignOut else { return } - self.coordinator.setup() - self.coordinator.setupOnboardingIfNeeds(animated: true) - } - } - .store(in: &disposeBag) - } } // MARK: - StatusTableViewControllerAspect -extension HomeTimelineViewController: StatusTableViewControllerAspect { } +//extension HomeTimelineViewController: StatusTableViewControllerAspect { } -extension HomeTimelineViewController: TableViewCellHeightCacheableContainer { - var cellFrameCache: NSCache { return viewModel.cellFrameCache } -} +//extension HomeTimelineViewController: TableViewCellHeightCacheableContainer { +// var cellFrameCache: NSCache { return viewModel.cellFrameCache } +//} // MARK: - UIScrollViewDelegate extension HomeTimelineViewController { func scrollViewDidScroll(_ scrollView: UIScrollView) { - aspectScrollViewDidScroll(scrollView) + //aspectScrollViewDidScroll(scrollView) viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView) } } -extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer { - typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell - typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading - var loadMoreConfigurableTableView: UITableView { return tableView } - var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } -} +//extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer { +// typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell +// typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading +// var loadMoreConfigurableTableView: UITableView { return tableView } +// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } +//} // MARK: - UITableViewDelegate -extension HomeTimelineViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - aspectTableView(tableView, estimatedHeightForRowAt: indexPath) - } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath) - } - - func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - aspectTableView(tableView, didSelectRowAt: indexPath) - } - - func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) - } - - func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) - } - - func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) - } - - func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { - aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) - } - -} +//extension HomeTimelineViewController: UITableViewDelegate { +// +// func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { +// aspectTableView(tableView, estimatedHeightForRowAt: indexPath) +// } +// +// func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { +// aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath) +// } +// +// func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { +// aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) +// } +// +// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// aspectTableView(tableView, didSelectRowAt: indexPath) +// } +// +// func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { +// return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) +// } +// +// func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { +// return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) +// } +// +// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { +// return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) +// } +// +// func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { +// aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) +// } +// +//} // MARK: - UITableViewDataSourcePrefetching -extension HomeTimelineViewController: UITableViewDataSourcePrefetching { - func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - aspectTableView(tableView, prefetchRowsAt: indexPaths) - } -} +//extension HomeTimelineViewController: UITableViewDataSourcePrefetching { +// func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { +// aspectTableView(tableView, prefetchRowsAt: indexPaths) +// } +//} // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { @@ -482,9 +474,9 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate // MARK: - ScrollViewContainer extension HomeTimelineViewController: ScrollViewContainer { - + var scrollView: UIScrollView { return tableView } - + func scrollToTop(animated: Bool) { if scrollView.contentOffset.y < scrollView.frame.height, viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self), @@ -499,10 +491,10 @@ extension HomeTimelineViewController: ScrollViewContainer { } else { let indexPath = IndexPath(row: 0, section: 0) guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return } - tableView.scrollToRow(at: indexPath, at: .top, animated: true) + node.scrollToRow(at: indexPath, at: .top, animated: true) } } - + } // MARK: - AVPlayerViewControllerDelegate @@ -532,7 +524,7 @@ extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate guard let diffableDataSource = viewModel.diffableDataSource else { return } let indexPath = IndexPath(row: 0, section: 0) guard diffableDataSource.itemIdentifier(for: indexPath) != nil else { return } - tableView.scrollToRow(at: indexPath, at: .top, animated: true) + node.scrollToRow(at: indexPath, at: .top, animated: true) case .offlineButton: // TODO: retry break @@ -568,3 +560,20 @@ extension HomeTimelineViewController: PerformanceMonitorDelegate { } } #endif + +// MARK: - ASTableDelegate +extension HomeTimelineViewController: ASTableDelegate { + func shouldBatchFetch(for tableNode: ASTableNode) -> Bool { + switch viewModel.loadLatestStateMachine.currentState { + case is HomeTimelineViewModel.LoadOldestState.NoMore: + return false + default: + return true + } + } + + func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) { + viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) + context.completeBatchFetching(true) + } +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 6f5e66c0e..5667af39f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -9,35 +9,30 @@ import os.log import UIKit import CoreData import CoreDataStack +import AsyncDisplayKit +import DifferenceKit +import DiffableDataSources extension HomeTimelineViewModel { - + func setupDiffableDataSource( - for tableView: UITableView, + tableNode: ASTableNode, dependency: NeedsDependency, statusTableViewCellDelegate: StatusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate ) { - let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) - .autoconnect() - .share() - .eraseToAnyPublisher() - - diffableDataSource = StatusSection.tableViewDiffableDataSource( - for: tableView, - dependency: dependency, - managedObjectContext: fetchedResultsController.managedObjectContext, - timestampUpdatePublisher: timestampUpdatePublisher, - statusTableViewCellDelegate: statusTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate, - threadReplyLoaderTableViewCellDelegate: nil + tableNode.automaticallyAdjustsContentOffset = true + + diffableDataSource = StatusSection.tableNodeDiffableDataSource( + tableNode: tableNode, + managedObjectContext: fetchedResultsController.managedObjectContext ) - -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.main]) -// diffableDataSource?.apply(snapshot) + + var snapshot = DiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) } - + } // MARK: - NSFetchedResultsControllerDelegate @@ -49,21 +44,18 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - guard let tableView = self.tableView else { return } - guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return } - + guard let diffableDataSource = self.diffableDataSource else { return } let oldSnapshot = diffableDataSource.snapshot() - + let predicate = fetchedResultsController.fetchRequest.predicate let parentManagedObjectContext = fetchedResultsController.managedObjectContext let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) managedObjectContext.parent = parentManagedObjectContext - + managedObjectContext.perform { var shouldAddBottomLoader = false - + let timelineIndexes: [HomeTimelineIndex] = { let request = HomeTimelineIndex.sortedFetchRequest request.returnsObjectsAsFaults = false @@ -75,25 +67,25 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { return [] } }() - + // that's will be the most fastest fetch because of upstream just update and no modify needs consider - + var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] - + for item in oldSnapshot.itemIdentifiers { guard case let .homeTimelineIndex(objectID, attribute) = item else { continue } oldSnapshotAttributeDict[objectID] = attribute } - + var newTimelineItems: [Item] = [] for (i, timelineIndex) in timelineIndexes.enumerated() { let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute() attribute.isSeparatorLineHidden = false - + // append new item into snapshot newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute)) - + let isLast = i == timelineIndexes.count - 1 switch (isLast, timelineIndex.hasMore) { case (false, true): @@ -105,30 +97,22 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { break } } // end for - - var newSnapshot = NSDiffableDataSourceSnapshot() + + var newSnapshot = DiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) newSnapshot.appendItems(newTimelineItems, toSection: .main) - + let endSnapshot = CACurrentMediaTime() - - DispatchQueue.main.async { - if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) { - newSnapshot.appendItems([.bottomLoader], toSection: .main) - } - - guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { - diffableDataSource.apply(newSnapshot) - self.isFetchingLatestTimeline.value = false - return - } - - diffableDataSource.apply(newSnapshot, animatingDifferences: false) { - tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) - tableView.contentOffset.y = tableView.contentOffset.y - difference.offset + + if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) { + newSnapshot.appendItems([.bottomLoader], toSection: .main) + } + + diffableDataSource.apply(newSnapshot, animatingDifferences: false) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.isFetchingLatestTimeline.value = false } - + let end = CACurrentMediaTime() os_log("%{public}s[%{public}ld], %{public}s: calculate home timeline layout cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - endSnapshot) } @@ -145,8 +129,8 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { private func calculateReloadSnapshotDifference( navigationBar: UINavigationBar, tableView: UITableView, - oldSnapshot: NSDiffableDataSourceSnapshot, - newSnapshot: NSDiffableDataSourceSnapshot + oldSnapshot: DiffableDataSourceSnapshot, + newSnapshot: DiffableDataSourceSnapshot ) -> Difference? { guard oldSnapshot.numberOfItems != 0 else { return nil } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index fdbbfba9d..11893e33c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -16,6 +16,7 @@ import GameplayKit import AlamofireImage import DateToolsSwift import ActiveLabel +import AsyncDisplayKit final class HomeTimelineViewModel: NSObject { @@ -29,15 +30,18 @@ final class HomeTimelineViewModel: NSObject { let isFetchingLatestTimeline = CurrentValueSubject(false) let viewDidAppear = PassthroughSubject() let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel - + + weak var tableNode: ASTableNode? weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? - weak var tableView: UITableView? + //weak var tableView: UITableView? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? let timelineIsEmpty = CurrentValueSubject(false) let homeTimelineNeedRefresh = PassthroughSubject() // output + var diffableDataSource: TableNodeDiffableDataSource? + // top loader private(set) lazy var loadLatestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state @@ -67,7 +71,7 @@ final class HomeTimelineViewModel: NSObject { lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) // middle loader let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine - var diffableDataSource: UITableViewDiffableDataSource? + // var diffableDataSource: UITableViewDiffableDataSource? var cellFrameCache = NSCache() @@ -100,12 +104,7 @@ final class HomeTimelineViewModel: NSObject { guard let self = self else { return } self.fetchedResultsController.fetchRequest.predicate = predicate do { - self.diffableDataSource?.defaultRowAnimation = .fade try self.fetchedResultsController.performFetch() - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in - guard let self = self else { return } - self.diffableDataSource?.defaultRowAnimation = .automatic - } } catch { assertionFailure(error.localizedDescription) } diff --git a/Mastodon/Scene/Share/View/Node/StatusNode.swift b/Mastodon/Scene/Share/View/Node/StatusNode.swift new file mode 100644 index 000000000..818711083 --- /dev/null +++ b/Mastodon/Scene/Share/View/Node/StatusNode.swift @@ -0,0 +1,109 @@ +// +// StatusNNode.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-19. +// + +import UIKit +import Combine +import AsyncDisplayKit +import CoreDataStack + +final class StatusNode: ASCellNode { + + var disposeBag = Set() + + static let avatarImageSize = CGSize(width: 42, height: 42) + static let avatarImageCornerRadius: CGFloat = 4 + + let avatarImageNode: ASNetworkImageNode = { + let node = ASNetworkImageNode() + node.contentMode = .scaleAspectFill + node.defaultImage = UIImage.placeholder(color: .systemFill) + node.cornerRadius = StatusNode.avatarImageCornerRadius + // node.cornerRoundingType = .precomposited + return node + }() + + let nameTextNode = ASTextNode() + let nameDotTextNode = ASTextNode() + let dateTextNode = ASTextNode() + let usernameTextNode = ASTextNode() + + init(status: Status) { + super.init() + + automaticallyManagesSubnodes = true + + if let url = (status.reblog ?? status).author.avatarImageURL() { + avatarImageNode.url = url + } + nameTextNode.attributedText = NSAttributedString(string: status.author.displayNameWithFallback, attributes: [ + .foregroundColor: Asset.Colors.Label.primary.color, + .font: UIFont.systemFont(ofSize: 17, weight: .semibold) + ]) + nameDotTextNode.attributedText = NSAttributedString(string: "·", attributes: [ + .foregroundColor: Asset.Colors.Label.secondary.color, + .font: UIFont.systemFont(ofSize: 13, weight: .regular) + ]) + // set date + let createdAt = (status.reblog ?? status).createdAt + dateTextNode.attributedText = NSAttributedString(string: createdAt.slowedTimeAgoSinceNow, attributes: [ + .foregroundColor: Asset.Colors.Label.secondary.color, + .font: UIFont.systemFont(ofSize: 13, weight: .regular) + ]) +// RunLoop.main.perform { [weak self] in +// guard let self = self else { return } +// AppContext.shared.timestampUpdatePublisher +// .sink { [weak self] _ in +// guard let self = self else { return } +// self.dateTextNode.attributedText = NSAttributedString(string: createdAt.slowedTimeAgoSinceNow, attributes: [ +// .foregroundColor: Asset.Colors.Label.secondary.color, +// .font: UIFont.systemFont(ofSize: 13, weight: .regular) +// ]) +// } +// .store(in: &self.disposeBag) +// } + usernameTextNode.attributedText = NSAttributedString(string: "@" + status.author.acct, attributes: [ + .foregroundColor: Asset.Colors.Label.secondary.color, + .font: UIFont.systemFont(ofSize: 15, weight: .regular) + ]) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let headerStack = ASStackLayoutSpec.horizontal() + headerStack.alignItems = .center + headerStack.spacing = 5 + var headerStackChildren: [ASLayoutElement] = [] + + avatarImageNode.style.preferredSize = StatusNode.avatarImageSize + headerStackChildren.append(avatarImageNode) + + let authorMetaHeaderStack = ASStackLayoutSpec.horizontal() + authorMetaHeaderStack.alignItems = .center + authorMetaHeaderStack.spacing = 4 + authorMetaHeaderStack.children = [ + nameTextNode, + nameDotTextNode, + dateTextNode, + ] + let authorMetaStack = ASStackLayoutSpec.vertical() + authorMetaStack.children = [ + authorMetaHeaderStack, + usernameTextNode, + ] + + headerStackChildren.append(authorMetaStack) + + headerStack.children = headerStackChildren + + let verticalStack = ASStackLayoutSpec.vertical() + verticalStack.children = [ + headerStack + ] + + return verticalStack + } + +} diff --git a/Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift b/Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift new file mode 100644 index 000000000..aeff71e4f --- /dev/null +++ b/Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift @@ -0,0 +1,37 @@ +// +// TimelineBottomLoaderNode.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-19. +// + +import UIKit +import AsyncDisplayKit + +final class TimelineBottomLoaderNode: ASCellNode { + + let activityIndicatorNode = ActivityIndicatorNode() + + override init() { + super.init() + + automaticallyManagesSubnodes = true + activityIndicatorNode.bounds = CGRect(x: 0, y: 0, width: 40, height: 40) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let contentStack = ASStackLayoutSpec.horizontal() + contentStack.alignItems = .center + contentStack.spacing = 7 + + contentStack.children = [activityIndicatorNode] + + return contentStack + } + + override func didEnterDisplayState() { + super.didEnterDisplayState() + activityIndicatorNode.animating = true + } + +} diff --git a/Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift b/Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift new file mode 100644 index 000000000..33a15dd75 --- /dev/null +++ b/Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift @@ -0,0 +1,50 @@ +// +// TimelineMiddleLoaderNode.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-19. +// + +import UIKit +import AsyncDisplayKit + +final class TimelineMiddleLoaderNode: ASCellNode { + + static let loadButtonFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) + + let activityIndicatorNode = ASDisplayNode(viewBlock: { + let view = UIActivityIndicatorView(style: .medium) + view.hidesWhenStopped = true + return view + }) + + let loadButtonNode = ASButtonNode() + + override init() { + super.init() + + automaticallyManagesSubnodes = true + + loadButtonNode.setAttributedTitle( + NSAttributedString( + string: L10n.Common.Controls.Timeline.Loader.loadMissingPosts, + attributes: [ + .foregroundColor: Asset.Colors.brandBlue.color, + .font: TimelineMiddleLoaderNode.loadButtonFont + ]), + for: .normal + ) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let contentStack = ASStackLayoutSpec.horizontal() + contentStack.alignItems = .center + contentStack.spacing = 7 + + contentStack.children = [loadButtonNode] + + + return contentStack + } + +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index b6b0cdb55..416053b41 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -44,6 +44,10 @@ class AppContext: ObservableObject { private var documentStoreSubscription: AnyCancellable! let overrideTraitCollection = CurrentValueSubject(nil) + let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() init() { let _coreDataStack = CoreDataStack() diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index e6ccaaac0..31a7db5ba 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -9,6 +9,7 @@ import os.log import UIKit import UserNotifications import AppShared +import AsyncDisplayKit #if DEBUG import GDPerformanceView_Swift @@ -33,6 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #if DEBUG PerformanceMonitor.shared().start() + // ASDisplayNode.shouldShowRangeDebugOverlay = true #endif return true diff --git a/Mastodon/Vender/ActivityIndicatorNode.swift b/Mastodon/Vender/ActivityIndicatorNode.swift new file mode 100644 index 000000000..6d34072f3 --- /dev/null +++ b/Mastodon/Vender/ActivityIndicatorNode.swift @@ -0,0 +1,71 @@ +// ref: https://github.com/Adlai-Holler/ASDKPlaceholderTest/blob/eea9fa7cff2d16a57efb47d208422ea9b49a630a/ASDKPlaceholderTest/ASDisplayNodeSubclasses.swift + +import Foundation +import AsyncDisplayKit +import UIKit + +/** + A node that shows a `UIActivityIndicatorView`. Does not support layer backing. + Note: You must not change the style to or from `.WhiteLarge` after init, or the node's size will not update. + */ +class ActivityIndicatorNode: ASDisplayNode { + + private static let defaultSize = CGSize(width: 20, height: 20) + private static let largeSize = CGSize(width: 37, height: 37) + + init(style: UIActivityIndicatorView.Style = .medium) { + super.init() + setViewBlock { + UIActivityIndicatorView(style: style) + } + + self.style.preferredSize = style == .large ? ActivityIndicatorNode.defaultSize : ActivityIndicatorNode.largeSize + } + + var activityIndicatorView: UIActivityIndicatorView { + return view as! UIActivityIndicatorView + } + + override func didLoad() { + super.didLoad() + if animating { + activityIndicatorView.startAnimating() + } + activityIndicatorView.color = color + activityIndicatorView.hidesWhenStopped = hidesWhenStopped + } + + /// Wrapper for `UIActivityIndicatorView.hidesWhenStopped`. NOTE: You must respect thread affinity. + var hidesWhenStopped = true { + didSet { + if isNodeLoaded { + assert(Thread.isMainThread) + activityIndicatorView.hidesWhenStopped = hidesWhenStopped + } + } + } + + /// Wrapper for `UIActivityIndicatorView.color`. NOTE: You must respect thread affinity. + var color: UIColor? { + didSet { + if isNodeLoaded { + assert(Thread.isMainThread) + activityIndicatorView.color = color + } + } + } + + /// Wrapper for `UIActivityIndicatorView.animating`. NOTE: You must respect thread affinity. + var animating = false { + didSet { + if isNodeLoaded { + assert(Thread.isMainThread) + if animating { + activityIndicatorView.startAnimating() + } else { + activityIndicatorView.stopAnimating() + } + } + } + } +} diff --git a/Podfile b/Podfile index 796473d68..d888d37f4 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,8 @@ target 'Mastodon' do # UI pod 'UITextField+Shake', '~> 1.2' - + pod 'Texture', '~> 3.0.0' + # misc pod 'SwiftGen', '~> 6.4.0' pod 'DateToolsSwift', '~> 5.0.0' diff --git a/Podfile.lock b/Podfile.lock index 4e7baf347..ce8b41ff8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -4,7 +4,42 @@ PODS: - GDPerformanceView-Swift (2.1.1) - Kanna (5.2.4) - Keys (1.0.1) + - PINCache (3.0.3): + - PINCache/Arc-exception-safe (= 3.0.3) + - PINCache/Core (= 3.0.3) + - PINCache/Arc-exception-safe (3.0.3): + - PINCache/Core + - PINCache/Core (3.0.3): + - PINOperation (~> 1.2.1) + - PINOperation (1.2.1) + - PINRemoteImage/Core (3.0.3): + - PINOperation + - PINRemoteImage/iOS (3.0.3): + - PINRemoteImage/Core + - PINRemoteImage/PINCache (3.0.3): + - PINCache (~> 3.0.3) + - PINRemoteImage/Core - SwiftGen (6.4.0) + - Texture (3.0.0): + - Texture/AssetsLibrary (= 3.0.0) + - Texture/Core (= 3.0.0) + - Texture/MapKit (= 3.0.0) + - Texture/Photos (= 3.0.0) + - Texture/PINRemoteImage (= 3.0.0) + - Texture/Video (= 3.0.0) + - Texture/AssetsLibrary (3.0.0): + - Texture/Core + - Texture/Core (3.0.0) + - Texture/MapKit (3.0.0): + - Texture/Core + - Texture/Photos (3.0.0): + - Texture/Core + - Texture/PINRemoteImage (3.0.0): + - PINRemoteImage/iOS (~> 3.0.0) + - PINRemoteImage/PINCache + - Texture/Core + - Texture/Video (3.0.0): + - Texture/Core - "UITextField+Shake (1.2.1)" DEPENDENCIES: @@ -14,6 +49,7 @@ DEPENDENCIES: - Kanna (~> 5.2.2) - Keys (from `Pods/CocoaPodsKeys`) - SwiftGen (~> 6.4.0) + - Texture - "UITextField+Shake (~> 1.2)" SPEC REPOS: @@ -22,7 +58,11 @@ SPEC REPOS: - FLEX - GDPerformanceView-Swift - Kanna + - PINCache + - PINOperation + - PINRemoteImage - SwiftGen + - Texture - "UITextField+Shake" EXTERNAL SOURCES: @@ -35,9 +75,13 @@ SPEC CHECKSUMS: GDPerformanceView-Swift: 22d964fe40b19e3d914dba2586237d064de8fd77 Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 + PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 + PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 + PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 + Texture: 2f109e937850d94d1d07232041c9c7313ccddb81 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 -PODFILE CHECKSUM: 257c550231fcd1336a29f7835aa331171bb66ebd +PODFILE CHECKSUM: 464046172607e3a92ad500f8050ee34566a47c73 COCOAPODS: 1.10.1 From bac5de84918a8f878803f4c76daf70a2698396a3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 20 Jun 2021 12:27:37 +0200 Subject: [PATCH 03/18] Change public visibility icon to globe --- Mastodon/Scene/Compose/View/ComposeToolbarView.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 6acbf80ef..2296fe294 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -191,11 +191,7 @@ extension ComposeToolbarView { func image(interfaceStyle: UIUserInterfaceStyle) -> UIImage { switch self { - case .public: - switch interfaceStyle { - case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! - default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! - } + case .public: return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! // case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))! @@ -204,7 +200,7 @@ extension ComposeToolbarView { func imageNameForTimeline() -> String { switch self { - case .public: return "person.3" + case .public: return "globe" // case .unlisted: return "eye.slash" case .private: return "person.crop.circle.badge.plus" case .direct: return "at" @@ -259,7 +255,7 @@ extension ComposeToolbarView { var children: [UIMenuElement] = [] let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in guard let self = self else { return } - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibrary", ((#file as NSString).lastPathComponent), #line, #function) self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .photoLibrary) } children.append(photoLibraryAction) From 69a7517fde9dd7cc12d2ca216211861422559560 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 00:14:47 +0800 Subject: [PATCH 04/18] feat: add content for StatusNode. Migrate HTML parser from Kanna to Fuzi --- Mastodon.xcodeproj/project.pbxproj | 47 +++++++- .../xcschemes/xcschememanagement.plist | 4 +- .../xcshareddata/swiftpm/Package.resolved | 9 ++ .../MastodonStatusContent+Appearance.swift | 17 +++ .../MastodonStatusContent+ParseResult.swift | 108 +++++++++++++++++ Mastodon/Helper/MastodonStatusContent.swift | 103 +++++----------- .../StatusProvider+StatusNodeDelegate.swift | 16 +++ .../StatusProvider/StatusProvider.swift | 9 ++ .../StatusProvider/StatusProviderFacade.swift | 111 ++++++++++++------ .../HomeTimelineViewController+Provider.swift | 24 ++++ .../HomeTimelineViewController.swift | 9 ++ .../View/Node/ASMetaEditableTextNode.swift | 21 ++++ .../View/Node/{ => Status}/StatusNode.swift | 66 ++++++++++- .../TimelineBottomLoaderNode.swift | 0 .../TimelineMiddleLoaderNode.swift | 0 Mastodon/Supporting Files/AppDelegate.swift | 4 +- 16 files changed, 432 insertions(+), 116 deletions(-) create mode 100644 Mastodon/Helper/MastodonStatusContent+Appearance.swift create mode 100644 Mastodon/Helper/MastodonStatusContent+ParseResult.swift create mode 100644 Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift create mode 100644 Mastodon/Scene/Share/View/Node/ASMetaEditableTextNode.swift rename Mastodon/Scene/Share/View/Node/{ => Status}/StatusNode.swift (59%) rename Mastodon/Scene/Share/View/Node/{ => Status}/TimelineBottomLoaderNode.swift (100%) rename Mastodon/Scene/Share/View/Node/{ => Status}/TimelineMiddleLoaderNode.swift (100%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5201c5409..8fe09fe6d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; + DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */; }; DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; @@ -202,6 +203,9 @@ DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */; }; DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; }; DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; }; + DB1EE7AE267F3071000CC337 /* MastodonStatusContent+ParseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1EE7AD267F3071000CC337 /* MastodonStatusContent+ParseResult.swift */; }; + DB1EE7B0267F3088000CC337 /* MastodonStatusContent+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1EE7AF267F3088000CC337 /* MastodonStatusContent+Appearance.swift */; }; + DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1EE7B1267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift */; }; DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; }; DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; }; DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; }; @@ -416,6 +420,7 @@ DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */; }; DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */; }; DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC649D267DFE43007FE9FD /* DiffableDataSources */; }; + DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC64A0267E6D02007FE9FD /* Fuzi */; }; DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; }; DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; }; DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */; }; @@ -765,6 +770,7 @@ CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = ""; }; DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASMetaEditableTextNode.swift; sourceTree = ""; }; DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = ""; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; @@ -786,6 +792,9 @@ DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlNavigateable.swift; sourceTree = ""; }; DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = ""; }; + DB1EE7AD267F3071000CC337 /* MastodonStatusContent+ParseResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+ParseResult.swift"; sourceTree = ""; }; + DB1EE7AF267F3088000CC337 /* MastodonStatusContent+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+Appearance.swift"; sourceTree = ""; }; + DB1EE7B1267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusNodeDelegate.swift"; sourceTree = ""; }; DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = ""; }; DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSection.swift; sourceTree = ""; }; DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = ""; }; @@ -1083,6 +1092,7 @@ DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, + DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */, 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, @@ -1299,6 +1309,7 @@ 2D38F1FD25CD481700561493 /* StatusProvider.swift */, 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */, 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */, + DB1EE7B1267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift */, DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */, DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */, DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */, @@ -1654,6 +1665,16 @@ path = Onboarding; sourceTree = ""; }; + DB023296267F0ABE00031745 /* Status */ = { + isa = PBXGroup; + children = ( + DBAC6484267D0F9E007FE9FD /* StatusNode.swift */, + DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */, + DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */, + ); + path = Status; + sourceTree = ""; + }; DB084B5125CBC56300F898ED /* CoreDataStack */ = { isa = PBXGroup; children = ( @@ -2307,6 +2328,8 @@ isa = PBXGroup; children = ( 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */, + DB1EE7AD267F3071000CC337 /* MastodonStatusContent+ParseResult.swift */, + DB1EE7AF267F3088000CC337 /* MastodonStatusContent+Appearance.swift */, DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */, DB35FC2E26130172006193C9 /* MastodonField.swift */, DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */, @@ -2353,9 +2376,8 @@ DBAC6486267D0FAC007FE9FD /* Node */ = { isa = PBXGroup; children = ( - DBAC6484267D0F9E007FE9FD /* StatusNode.swift */, - DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */, - DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */, + DB023296267F0ABE00031745 /* Status */, + DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */, ); path = Node; sourceTree = ""; @@ -2553,6 +2575,7 @@ DBAEDE5E267A0B1500D25FF5 /* Nuke */, DBAC6482267D0B21007FE9FD /* DifferenceKit */, DBAC649D267DFE43007FE9FD /* DiffableDataSources */, + DBAC64A0267E6D02007FE9FD /* Fuzi */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2743,6 +2766,7 @@ DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */, DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */, DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */, + DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3210,6 +3234,7 @@ DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */, + DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */, @@ -3283,6 +3308,7 @@ DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, 2D7867192625B77500211898 /* NotificationItem.swift in Sources */, + DB1EE7AE267F3071000CC337 /* MastodonStatusContent+ParseResult.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, @@ -3299,6 +3325,7 @@ DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */, 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */, 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */, + DB1EE7B0267F3088000CC337 /* MastodonStatusContent+Appearance.swift in Sources */, 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+Provider.swift in Sources */, 0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */, 2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */, @@ -3353,6 +3380,7 @@ DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */, DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */, 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, + DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */, 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */, 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */, DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */, @@ -4186,6 +4214,14 @@ kind = branch; }; }; + DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/cezheng/Fuzi.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.1.3; + }; + }; DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/Nuke.git"; @@ -4279,6 +4315,11 @@ package = DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */; productName = DiffableDataSources; }; + DBAC64A0267E6D02007FE9FD /* Fuzi */ = { + isa = XCSwiftPackageProductDependency; + package = DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */; + productName = Fuzi; + }; DBAEDE5E267A0B1500D25FF5 /* Nuke */ = { isa = XCSwiftPackageProductDependency; package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 50a025853..d2befef16 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ AppShared.xcscheme_^#shared#^_ orderHint - 19 + 20 CoreDataStack.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 20 + 19 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index f46eff823..785af99e3 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -64,6 +64,15 @@ "version": "1.2.0" } }, + { + "package": "Fuzi", + "repositoryURL": "https://github.com/cezheng/Fuzi.git", + "state": { + "branch": null, + "revision": "f08c8323da21e985f3772610753bcfc652c2103f", + "version": "3.1.3" + } + }, { "package": "KeychainAccess", "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", diff --git a/Mastodon/Helper/MastodonStatusContent+Appearance.swift b/Mastodon/Helper/MastodonStatusContent+Appearance.swift new file mode 100644 index 000000000..f627093c6 --- /dev/null +++ b/Mastodon/Helper/MastodonStatusContent+Appearance.swift @@ -0,0 +1,17 @@ +// +// MastodonStatusContent+Appearance.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-20. +// + +import UIKit + +extension MastodonStatusContent { + 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 new file mode 100644 index 000000000..f1f02fae1 --- /dev/null +++ b/Mastodon/Helper/MastodonStatusContent+ParseResult.swift @@ -0,0 +1,108 @@ +// +// MastodonStatusContent+ParseResult.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-20. +// + +import Foundation +import ActiveLabel + +extension MastodonStatusContent { + struct ParseResult: Hashable { + let document: String + let original: String + let trimmed: String + let activeEntities: [ActiveEntity] + + 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: + } + + 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" + + 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 + } + + 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 index 5dbef4991..1e52f150c 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -5,10 +5,10 @@ // Created by MainasuK Cirno on 2021/2/1. // -import Foundation +import UIKit import Combine -import Kanna import ActiveLabel +import Fuzi enum MastodonStatusContent { @@ -125,30 +125,6 @@ extension String { } } -extension MastodonStatusContent { - struct ParseResult: Hashable { - let document: String - let original: String - let trimmed: String - let activeEntities: [ActiveEntity] - - 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: - } - - func hash(into hasher: inout Hasher) { - hasher.combine(document) - hasher.combine(original) - hasher.combine(trimmed) - hasher.combine(activeEntities.count) // FIXME: - } - } -} - - extension MastodonStatusContent { class Node { @@ -165,7 +141,7 @@ extension MastodonStatusContent { } let tagName: String? - let classNames: Set + let attributes: [String : String] let href: String? let hrefEllipsis: String? @@ -175,56 +151,47 @@ extension MastodonStatusContent { level: Int, text: Substring, tagName: String?, - className: String?, + attributes: [String : String], href: String?, hrefEllipsis: String?, children: [Node] ) { let _classNames: Set = { - guard let className = className else { return Set() } + guard let className = attributes["class"] else { return Set() } return Set(className.components(separatedBy: " ")) }() let _type: Type? = { - if tagName == "a" && !_classNames.contains("mention") { - return .url - } - - if _classNames.contains("mention") { + if tagName == "a" { if _classNames.contains("u-url") { return .mention - } else if _classNames.contains("hashtag") { + } + if _classNames.contains("hashtag") { return .hashtag } + return .url + } else { + if _classNames.contains("emoji") { + return .emoji + } + return nil } - - if _classNames.contains("emoji") { - return .emoji - } - - return nil }() self.level = level self.type = _type self.text = text self.tagName = tagName - self.classNames = _classNames + self.attributes = attributes self.href = href self.hrefEllipsis = hrefEllipsis self.children = children } static func parse(document: String) throws -> MastodonStatusContent.Node { - let html = try HTML(html: document, encoding: .utf8) - - // add `\r\n` explicit due to Kanna text missing it after convert to text - // ref: https://github.com/tid-kijyun/Kanna/issues/150 - let brNodes = html.css("br").makeIterator() - while let brNode = brNodes.next() { - brNode.addNextSibling(try! HTML(html: "\r\n", encoding: .utf8).body!) - } + 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?.text ?? "" + 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) @@ -232,8 +199,8 @@ extension MastodonStatusContent { let node = Node( level: level, text: text[...], - tagName: body?.tagName, - className: body?.className, + tagName: body?.tag, + attributes: body?.attributes ?? [:], href: nil, hrefEllipsis: nil, children: children @@ -246,13 +213,11 @@ extension MastodonStatusContent { let parent = element let scanner = Scanner(string: String(parentText)) scanner.charactersToBeSkipped = .none - - var element = parent.at_css(":first-child") + var children: [Node] = [] - - while let _element = element { - let _text = _element.text ?? "" - + for _element in parent.children { + let _text = _element.stringValue + // scan element text _ = scanner.scanUpToString(_text) let startIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string) @@ -261,27 +226,26 @@ extension MastodonStatusContent { 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..%@%@: %@", diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift new file mode 100644 index 000000000..b8734a3c8 --- /dev/null +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift @@ -0,0 +1,16 @@ +// +// StatusProvider+StatusNodeDelegate.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-20. +// + +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) + } +} diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider.swift b/Mastodon/Protocol/StatusProvider/StatusProvider.swift index 8e27a2207..78bed66c5 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider.swift @@ -9,6 +9,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import AsyncDisplayKit protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController { // async @@ -21,4 +22,12 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl var tableViewDiffableDataSource: UITableViewDiffableDataSource? { get } func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? func items(indexPaths: [IndexPath]) -> [Item] + + func status(node: ASCellNode?, indexPath: IndexPath?) -> Status? +} + +extension StatusProvider { + func status(node: ASCellNode?, indexPath: IndexPath?) -> Status? { + fatalError("Needs implement this") + } } diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index a21422159..ff5b61583 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -12,6 +12,7 @@ import CoreData import CoreDataStack import MastodonSDK import ActiveLabel +import AsyncDisplayKit enum StatusProviderFacade { } @@ -144,51 +145,85 @@ extension StatusProviderFacade { break } } - + + static func responseToStatusActiveLabelAction(provider: StatusProvider, node: ASCellNode, didSelectActiveEntityType type: ActiveEntityType) { + switch type { + case .hashtag(let text, _): + let hashtagTimelienViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text) + provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelienViewModel), 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) + } + private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell, mention: String) { - guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - let domain = activeMastodonAuthenticationBox.domain - provider.status(for: cell, indexPath: nil) .sink { [weak provider] status in guard let provider = provider else { return } - let _status: Status? = { - switch target { - case .primary: return status?.reblog ?? status - case .secondary: return status - } - }() - guard let status = _status else { return } - - // cannot continue without meta - guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { return } - - let userID = mentionMeta.id - - let profileViewModel: ProfileViewModel = { - // check if self - guard userID != activeMastodonAuthenticationBox.userID else { - return MeProfileViewModel(context: provider.context) - } - - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: userID) - let mastodonUser = provider.context.managedObjectContext.safeFetch(request).first - - if let mastodonUser = mastodonUser { - return CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser) - } else { - return RemoteProfileViewModel(context: provider.context, userID: userID) - } - }() - - DispatchQueue.main.async { - provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show) - } + guard let status = status else { return } + coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention) } .store(in: &provider.disposeBag) } + + private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, status: Status, mention: String) { + guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + let domain = activeMastodonAuthenticationBox.domain + + let status: Status = { + switch target { + case .primary: return status.reblog ?? status + case .secondary: return status + } + }() + + // cannot continue without meta + guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { return } + + let userID = mentionMeta.id + + let profileViewModel: ProfileViewModel = { + // check if self + guard userID != activeMastodonAuthenticationBox.userID else { + return MeProfileViewModel(context: provider.context) + } + + let request = MastodonUser.sortedFetchRequest + request.fetchLimit = 1 + request.predicate = MastodonUser.predicate(domain: domain, id: userID) + let mastodonUser = provider.context.managedObjectContext.safeFetch(request).first + + if let mastodonUser = mastodonUser { + return CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser) + } else { + return RemoteProfileViewModel(context: provider.context, userID: userID) + } + }() + + DispatchQueue.main.async { + provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show) + } + } } extension StatusProviderFacade { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift index 38d843114..18a96f93d 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import AsyncDisplayKit // MARK: - StatusProvider extension HomeTimelineViewController: StatusProvider { @@ -83,6 +84,29 @@ extension HomeTimelineViewController: StatusProvider { } return items } + + func status(node: ASCellNode?, indexPath: IndexPath?) -> Status? { + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + return nil + } + + guard let indexPath = indexPath ?? node.flatMap({ self.node.indexPath(for: $0) }), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { + return nil + } + + switch item { + case .homeTimelineIndex(let objectID, _): + guard let homeTimelineIndex = try? viewModel.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { + assertionFailure() + return nil + } + return homeTimelineIndex.status + default: + return nil + } + } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 68afb41e2..4502c05d1 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -576,4 +576,13 @@ extension HomeTimelineViewController: ASTableDelegate { viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) context.completeBatchFetching(true) } + + func tableNode(_ tableNode: ASTableNode, willDisplayRowWith node: ASCellNode) { + if let statusNode = node as? StatusNode { + statusNode.delegate = self + } + } } + +// MARK: - StatusNodeDelegate +extension HomeTimelineViewController: StatusNodeDelegate { } diff --git a/Mastodon/Scene/Share/View/Node/ASMetaEditableTextNode.swift b/Mastodon/Scene/Share/View/Node/ASMetaEditableTextNode.swift new file mode 100644 index 000000000..e98e81d57 --- /dev/null +++ b/Mastodon/Scene/Share/View/Node/ASMetaEditableTextNode.swift @@ -0,0 +1,21 @@ +// +// ASMetaEditableTextNode.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-20. +// + +import UIKit +import AsyncDisplayKit + +protocol ASMetaEditableTextNodeDelegate: AnyObject { + func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool +} + +final class ASMetaEditableTextNode: ASEditableTextNode, UITextViewDelegate { + weak var metaEditableTextNodeDelegate: ASMetaEditableTextNodeDelegate? + + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + return metaEditableTextNodeDelegate?.metaEditableTextNode(self, shouldInteractWith: URL, in: characterRange, interaction: interaction) ?? false + } +} diff --git a/Mastodon/Scene/Share/View/Node/StatusNode.swift b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift similarity index 59% rename from Mastodon/Scene/Share/View/Node/StatusNode.swift rename to Mastodon/Scene/Share/View/Node/Status/StatusNode.swift index 818711083..d4a3b3ba7 100644 --- a/Mastodon/Scene/Share/View/Node/StatusNode.swift +++ b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift @@ -9,20 +9,44 @@ import UIKit import Combine import AsyncDisplayKit import CoreDataStack +import ActiveLabel + +protocol StatusNodeDelegate: AnyObject { + func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) +} final class StatusNode: ASCellNode { var disposeBag = Set() + weak var delegate: StatusNodeDelegate? // needs assign on main queue 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 + ) + }() + let avatarImageNode: ASNetworkImageNode = { let node = ASNetworkImageNode() node.contentMode = .scaleAspectFill node.defaultImage = UIImage.placeholder(color: .systemFill) + node.forcedSize = StatusNode.avatarImageSize node.cornerRadius = StatusNode.avatarImageCornerRadius // node.cornerRoundingType = .precomposited + // node.shouldRenderProgressImages = true return node }() @@ -30,6 +54,11 @@ final class StatusNode: ASCellNode { let nameDotTextNode = ASTextNode() let dateTextNode = ASTextNode() let usernameTextNode = ASTextNode() + let statusContentTextNode: ASMetaEditableTextNode = { + let node = ASMetaEditableTextNode() + node.scrollEnabled = false + return node + }() init(status: Status) { super.init() @@ -39,6 +68,7 @@ final class StatusNode: ASCellNode { if let url = (status.reblog ?? status).author.avatarImageURL() { avatarImageNode.url = url } + nameTextNode.attributedText = NSAttributedString(string: status.author.displayNameWithFallback, attributes: [ .foregroundColor: Asset.Colors.Label.primary.color, .font: UIFont.systemFont(ofSize: 17, weight: .semibold) @@ -65,10 +95,29 @@ final class StatusNode: ASCellNode { // } // .store(in: &self.disposeBag) // } + usernameTextNode.attributedText = NSAttributedString(string: "@" + status.author.acct, attributes: [ .foregroundColor: Asset.Colors.Label.secondary.color, .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) + } + } + + override func didEnterDisplayState() { + super.didEnterDisplayState() + + statusContentTextNode.textView.isEditable = false + statusContentTextNode.textView.textDragInteraction?.isEnabled = false + statusContentTextNode.textView.linkTextAttributes = [ + .foregroundColor: Asset.Colors.brandBlue.color + ] } override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { @@ -99,11 +148,26 @@ final class StatusNode: ASCellNode { headerStack.children = headerStackChildren let verticalStack = ASStackLayoutSpec.vertical() + verticalStack.spacing = 10 verticalStack.children = [ - headerStack + headerStack, + statusContentTextNode, ] return verticalStack } } + +// 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 + } +} diff --git a/Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift b/Mastodon/Scene/Share/View/Node/Status/TimelineBottomLoaderNode.swift similarity index 100% rename from Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift rename to Mastodon/Scene/Share/View/Node/Status/TimelineBottomLoaderNode.swift diff --git a/Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift b/Mastodon/Scene/Share/View/Node/Status/TimelineMiddleLoaderNode.swift similarity index 100% rename from Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift rename to Mastodon/Scene/Share/View/Node/Status/TimelineMiddleLoaderNode.swift diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 31a7db5ba..71073729e 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -33,8 +33,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { application.registerForRemoteNotifications() #if DEBUG - PerformanceMonitor.shared().start() + // PerformanceMonitor.shared().start() // ASDisplayNode.shouldShowRangeDebugOverlay = true + // ASControlNode.enableHitTestDebug = true + // ASImageNode.shouldShowImageScalingOverlay = true #endif return true From 6f8666aaa8931cd76883368721f791b9fed9966b Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 00:26:23 +0800 Subject: [PATCH 05/18] feat: add timestamp updater --- .../Share/View/Node/Status/StatusNode.swift | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift index d4a3b3ba7..c4ec3e680 100644 --- a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift +++ b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift @@ -18,6 +18,8 @@ protocol StatusNodeDelegate: AnyObject { final class StatusNode: ASCellNode { var disposeBag = Set() + var timestamp: Date + var timestampSubscription: AnyCancellable? weak var delegate: StatusNodeDelegate? // needs assign on main queue static let avatarImageSize = CGSize(width: 42, height: 42) @@ -61,6 +63,7 @@ final class StatusNode: ASCellNode { }() init(status: Status) { + timestamp = (status.reblog ?? status).createdAt super.init() automaticallyManagesSubnodes = true @@ -78,23 +81,10 @@ final class StatusNode: ASCellNode { .font: UIFont.systemFont(ofSize: 13, weight: .regular) ]) // set date - let createdAt = (status.reblog ?? status).createdAt - dateTextNode.attributedText = NSAttributedString(string: createdAt.slowedTimeAgoSinceNow, attributes: [ + dateTextNode.attributedText = NSAttributedString(string: timestamp.slowedTimeAgoSinceNow, attributes: [ .foregroundColor: Asset.Colors.Label.secondary.color, .font: UIFont.systemFont(ofSize: 13, weight: .regular) ]) -// RunLoop.main.perform { [weak self] in -// guard let self = self else { return } -// AppContext.shared.timestampUpdatePublisher -// .sink { [weak self] _ in -// guard let self = self else { return } -// self.dateTextNode.attributedText = NSAttributedString(string: createdAt.slowedTimeAgoSinceNow, attributes: [ -// .foregroundColor: Asset.Colors.Label.secondary.color, -// .font: UIFont.systemFont(ofSize: 13, weight: .regular) -// ]) -// } -// .store(in: &self.disposeBag) -// } usernameTextNode.attributedText = NSAttributedString(string: "@" + status.author.acct, attributes: [ .foregroundColor: Asset.Colors.Label.secondary.color, @@ -113,6 +103,15 @@ final class StatusNode: ASCellNode { override func didEnterDisplayState() { super.didEnterDisplayState() + timestampSubscription = AppContext.shared.timestampUpdatePublisher + .sink { [weak self] _ in + guard let self = self else { return } + self.dateTextNode.attributedText = NSAttributedString(string: self.timestamp.slowedTimeAgoSinceNow, attributes: [ + .foregroundColor: Asset.Colors.Label.secondary.color, + .font: UIFont.systemFont(ofSize: 13, weight: .regular) + ]) + } + statusContentTextNode.textView.isEditable = false statusContentTextNode.textView.textDragInteraction?.isEnabled = false statusContentTextNode.textView.linkTextAttributes = [ @@ -120,6 +119,11 @@ final class StatusNode: ASCellNode { ] } + override func didExitVisibleState() { + super.didExitVisibleState() + timestampSubscription = nil + } + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let headerStack = ASStackLayoutSpec.horizontal() headerStack.alignItems = .center From 1156af3d4c5215f475283d90566fc78f976da95d Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 01:38:13 +0800 Subject: [PATCH 06/18] feat: add multiplex image nodes to StatusNode with progress loading supports --- .../Share/View/Node/Status/StatusNode.swift | 65 ++++++++++++++++++- .../ViewModel/MosaicImageViewModel.swift | 9 ++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift index c4ec3e680..9ddbcf87b 100644 --- a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift +++ b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift @@ -10,6 +10,7 @@ import Combine import AsyncDisplayKit import CoreDataStack import ActiveLabel +import func AVFoundation.AVMakeRect protocol StatusNodeDelegate: AnyObject { func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) @@ -20,6 +21,7 @@ final class StatusNode: ASCellNode { var disposeBag = Set() var timestamp: Date var timestampSubscription: AnyCancellable? + weak var delegate: StatusNodeDelegate? // needs assign on main queue static let avatarImageSize = CGSize(width: 42, height: 42) @@ -51,7 +53,6 @@ final class StatusNode: ASCellNode { // node.shouldRenderProgressImages = true return node }() - let nameTextNode = ASTextNode() let nameDotTextNode = ASTextNode() let dateTextNode = ASTextNode() @@ -62,10 +63,29 @@ final class StatusNode: ASCellNode { return node }() + let mosaicImageViewModel: MosaicImageViewModel + let mediaMultiplexImageNodes: [ASMultiplexImageNode] + init(status: Status) { timestamp = (status.reblog ?? status).createdAt + let _mosaicImageViewModel: MosaicImageViewModel = { + let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } + return MosaicImageViewModel(mediaAttachments: mediaAttachments) + }() + mosaicImageViewModel = _mosaicImageViewModel + mediaMultiplexImageNodes = { + var imageNodes: [ASMultiplexImageNode] = [] + for _ in 0..<_mosaicImageViewModel.metas.count { + let imageNode = ASMultiplexImageNode() // TODO: adapt downloader + imageNode.downloadsIntermediateImages = true + imageNode.imageIdentifiers = ["url", "previewURL"].map { $0 as NSString } // quality in descending order + imageNodes.append(imageNode) + } + return imageNodes + }() super.init() + print("meta: \(mosaicImageViewModel.metas.count), nodes: \(mediaMultiplexImageNodes.count)") automaticallyManagesSubnodes = true if let url = (status.reblog ?? status).author.avatarImageURL() { @@ -98,6 +118,10 @@ final class StatusNode: ASCellNode { ) { statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance) } + + for imageNode in mediaMultiplexImageNodes { + imageNode.dataSource = self + } } override func didEnterDisplayState() { @@ -112,6 +136,7 @@ final class StatusNode: ASCellNode { ]) } + // FIXME: needs move to other only once called callback in life cycle like: `viewDidLoad` statusContentTextNode.textView.isEditable = false statusContentTextNode.textView.textDragInteraction?.isEnabled = false statusContentTextNode.textView.linkTextAttributes = [ @@ -153,16 +178,34 @@ final class StatusNode: ASCellNode { let verticalStack = ASStackLayoutSpec.vertical() verticalStack.spacing = 10 - verticalStack.children = [ + var verticalStackChildren: [ASLayoutElement] = [ headerStack, statusContentTextNode, ] + if !mediaMultiplexImageNodes.isEmpty { + for (imageNode, meta) in zip(mediaMultiplexImageNodes, mosaicImageViewModel.metas) { + imageNode.style.preferredSize = AVMakeRect(aspectRatio: meta.size, insideRect: CGRect(origin: .zero, size: constrainedSize.max)).size + let layout = ASRatioLayoutSpec(ratio: meta.size.height / meta.size.width, child: imageNode) + verticalStackChildren.append(layout) + } + } + verticalStack.children = verticalStackChildren return verticalStack } } +//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 { @@ -175,3 +218,21 @@ extension StatusNode: ASMetaEditableTextNodeDelegate { return false } } + +// MARK: - ASMultiplexImageNodeDataSource +extension StatusNode: ASMultiplexImageNodeDataSource { + func multiplexImageNode(_ imageNode: ASMultiplexImageNode, urlForImageIdentifier imageIdentifier: ASImageIdentifier) -> URL? { + guard let imageNodeIndex = mediaMultiplexImageNodes.firstIndex(of: imageNode) else { return nil } + guard imageNodeIndex < mosaicImageViewModel.metas.count else { return nil } + let meta = mosaicImageViewModel.metas[imageNodeIndex] + switch imageIdentifier { + case "url" as NSString: + return meta.url + case "previewURL" as NSString: + return meta.priviewURL + default: + assertionFailure() + return nil + } + } +} diff --git a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift index 888d4dffe..265ce245b 100644 --- a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift @@ -16,16 +16,14 @@ struct MosaicImageViewModel { init(mediaAttachments: [Attachment]) { var metas: [MosaicMeta] = [] for element in mediaAttachments where element.type == .image { - // Display original on the iPad/Mac - guard let previewURL = element.previewURL else { continue } - let urlString = UIDevice.current.userInterfaceIdiom == .phone ? previewURL : element.url guard let meta = element.meta, let width = meta.original?.width, let height = meta.original?.height, - let url = URL(string: urlString) else { + let url = URL(string: element.url) else { continue } let mosaicMeta = MosaicMeta( + priviewURL: element.previewURL.flatMap { URL(string: $0) }, url: url, size: CGSize(width: width, height: height), blurhash: element.blurhash, @@ -40,7 +38,8 @@ struct MosaicImageViewModel { struct MosaicMeta { static let edgeMaxLength: CGFloat = 20 - + + let priviewURL: URL? let url: URL let size: CGSize let blurhash: String? From 1e67976e9f550fa58ade5caa65a1f1e375162ab8 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 13:34:49 +0800 Subject: [PATCH 07/18] feat: update compose visibility menu button icon --- Mastodon/Scene/Compose/View/ComposeToolbarView.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 2296fe294..d1ba5fdab 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -191,9 +191,13 @@ extension ComposeToolbarView { func image(interfaceStyle: UIUserInterfaceStyle) -> UIImage { switch self { - case .public: return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! + case .public: return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .medium))! // case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! - case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! + case .private: + switch interfaceStyle { + case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! + default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! + } case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))! } } @@ -202,7 +206,7 @@ extension ComposeToolbarView { switch self { case .public: return "globe" // case .unlisted: return "eye.slash" - case .private: return "person.crop.circle.badge.plus" + case .private: return "person.3" case .direct: return "at" } } From a9cce7b3e36fad19d851552817d6243a242dce69 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 16:37:28 +0800 Subject: [PATCH 08/18] fix: delete relationship missing for Status issue --- Mastodon/Service/APIService/APIService+Status.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Mastodon/Service/APIService/APIService+Status.swift b/Mastodon/Service/APIService/APIService+Status.swift index c927b05a8..01bc667ef 100644 --- a/Mastodon/Service/APIService/APIService+Status.swift +++ b/Mastodon/Service/APIService/APIService+Status.swift @@ -117,6 +117,14 @@ extension APIService { } }() if let status = oldStatus { + let homeTimelineIndexes = status.homeTimelineIndexes ?? Set() + for homeTimelineIndex in homeTimelineIndexes { + self.backgroundManagedObjectContext.delete(homeTimelineIndex) + } + let inNotifications = status.inNotifications ?? Set() + for notification in inNotifications { + self.backgroundManagedObjectContext.delete(notification) + } self.backgroundManagedObjectContext.delete(status) } } From 2dfd6168a96f6b08fe585a0d322954f0d578aac5 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 16:38:59 +0800 Subject: [PATCH 09/18] fix: handle status delete UI updater in thread scene --- .../xcschemes/xcschememanagement.plist | 4 +- .../Diffiable/Section/StatusSection.swift | 21 +++-- .../Thread/ThreadViewModel+Diffable.swift | 85 ++++++++++++++++--- Mastodon/Scene/Thread/ThreadViewModel.swift | 41 ++++++++- 4 files changed, 125 insertions(+), 26 deletions(-) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 2fb2d9806..73b68ec90 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 - 17 + 16 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 16 + 17 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index c5d0eb19b..8a3df09b1 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -47,14 +47,18 @@ extension StatusSection { // configure cell managedObjectContext.performAndWait { - let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex + let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex + // note: force check optional for status + // status maybe here when delete in thread scene + guard let status = timelineIndex?.status, + let userID = timelineIndex?.userID else { return } StatusSection.configure( cell: cell, dependency: dependency, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, - status: timelineIndex.status, - requestUserID: timelineIndex.userID, + status: status, + requestUserID: userID, statusItemAttribute: attribute ) } @@ -752,12 +756,13 @@ extension StatusSection { return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue) }() Publishers.CombineLatest( - dependency.context.blockDomainService.blockedDomains, + dependency.context.blockDomainService.blockedDomains.setFailureType(to: ManagedObjectObserver.Error.self), ManagedObjectObserver.observe(object: status.authorForUserProvider) - .assertNoFailure() - ) + ) .receive(on: RunLoop.main) - .sink { [weak dependency, weak cell] _, change in + .sink(receiveCompletion: { _ in + // do nothing + }, receiveValue: { [weak dependency, weak cell] _, change in guard let cell = cell else { return } guard let dependency = dependency else { return } switch change.changeType { @@ -769,7 +774,7 @@ extension StatusSection { break } StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status) - } + }) .store(in: &cell.disposeBag) self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status) } diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index 323a7a545..58e618f89 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -8,6 +8,8 @@ import UIKit import Combine import CoreData +import CoreDataStack +import MastodonSDK extension ThreadViewModel { @@ -41,13 +43,29 @@ extension ThreadViewModel { diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) Publishers.CombineLatest3( + rootItem.removeDuplicates(), + ancestorItems.removeDuplicates(), + descendantItems.removeDuplicates() + ) + .receive(on: RunLoop.main) + .sink { [weak self] rootItem, ancestorItems, descendantItems in + guard let self = self else { return } + var items: [Item] = [] + rootItem.flatMap { items.append($0) } + items.append(contentsOf: ancestorItems) + items.append(contentsOf: descendantItems) + self.updateDeletedStatus(for: items) + } + .store(in: &disposeBag) + + Publishers.CombineLatest4( rootItem, ancestorItems, - descendantItems + descendantItems, + existStatusFetchedResultsController.objectIDs ) - .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) // some magic to avoid jitter - .receive(on: DispatchQueue.main) - .sink { [weak self] rootItem, ancestorItems, descendantItems in + .debounce(for: .milliseconds(100), scheduler: RunLoop.main) // some magic to avoid jitter + .sink { [weak self] rootItem, ancestorItems, descendantItems, existObjectIDs in guard let self = self else { return } guard let tableView = self.tableView, let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() @@ -65,31 +83,42 @@ extension ThreadViewModel { if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) { newSnapshot.appendItems([.topLoader], toSection: .main) } + + let ancestorItems = ancestorItems.filter { item in + guard case let .reply(statusObjectID, _) = item else { return false } + return existObjectIDs.contains(statusObjectID) + } newSnapshot.appendItems(ancestorItems, toSection: .main) // root - if let rootItem = rootItem { - switch rootItem { - case .root: - newSnapshot.appendItems([rootItem], toSection: .main) - default: - break - } + if let rootItem = rootItem, + case let .root(objectID, _) = rootItem, + existObjectIDs.contains(objectID) { + newSnapshot.appendItems([rootItem], toSection: .main) } // leaf if !(currentState is LoadThreadState.NoMore) { newSnapshot.appendItems([.bottomLoader], toSection: .main) } + + let descendantItems = descendantItems.filter { item in + switch item { + case .leaf(let statusObjectID, _): + return existObjectIDs.contains(statusObjectID) + default: + return true + } + } newSnapshot.appendItems(descendantItems, toSection: .main) - // difference for first visiable item exclude .topLoader + // difference for first visible item exclude .topLoader guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { diffableDataSource.apply(newSnapshot) return } - // addtional margin for .topLoader + // additional margin for .topLoader let oldTopMargin: CGFloat = { let marginHeight = TimelineTopLoaderTableViewCell.cellHeight if oldSnapshot.itemIdentifiers.contains(.topLoader) { @@ -184,3 +213,33 @@ extension ThreadViewModel { ) } } + +extension ThreadViewModel { + private func updateDeletedStatus(for items: [Item]) { + let parentManagedObjectContext = context.managedObjectContext + let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + managedObjectContext.parent = parentManagedObjectContext + managedObjectContext.perform { + var statusIDs: [Status.ID] = [] + for item in items { + switch item { + case .root(let objectID, _): + guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } + statusIDs.append(status.id) + case .reply(let objectID, _): + guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } + statusIDs.append(status.id) + case .leaf(let objectID, _): + guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } + statusIDs.append(status.id) + default: + continue + } + } + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.existStatusFetchedResultsController.statusIDs.value = statusIDs + } + } + } +} diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index 50df678c6..febc34d17 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -16,12 +16,14 @@ import MastodonSDK class ThreadViewModel { var disposeBag = Set() + var rootItemObserver: AnyCancellable? // input let context: AppContext let rootNode: CurrentValueSubject let rootItem: CurrentValueSubject let cellFrameCache = NSCache() + let existStatusFetchedResultsController: StatusFetchedResultsController weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? weak var tableView: UITableView? @@ -49,10 +51,20 @@ class ThreadViewModel { self.context = context self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) }) 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) } ) + // bind fetcher domain + context.authenticationService.activeMastodonAuthenticationBox + .receive(on: RunLoop.main) + .sink { [weak self] box in + guard let self = self else { return } + self.existStatusFetchedResultsController.domain.value = box?.domain + } + .store(in: &disposeBag) + rootNode .receive(on: DispatchQueue.main) .sink { [weak self] rootNode in @@ -79,8 +91,32 @@ class ThreadViewModel { .store(in: &disposeBag) } - // descendantNodes - + rootItem + .receive(on: DispatchQueue.main) + .sink { [weak self] rootItem in + guard let self = self else { return } + guard case let .root(objectID, _) = rootItem else { return } + self.context.managedObjectContext.perform { + guard let status = self.context.managedObjectContext.object(with: objectID) as? Status else { + return + } + self.rootItemObserver = ManagedObjectObserver.observe(object: status) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in + // do nothing + }, receiveValue: { [weak self] change in + guard let self = self else { return } + switch change.changeType { + case .delete: + self.rootItem.value = nil + default: + break + } + }) + } + } + .store(in: &disposeBag) + ancestorNodes .receive(on: DispatchQueue.main) .compactMap { [weak self] nodes -> [Item]? in @@ -276,4 +312,3 @@ extension ThreadViewModel { } } - From 2c2aa127bfdf9de4141a14c58721167b4ff9e1f3 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 17:52:38 +0800 Subject: [PATCH 10/18] fix: wrong apostrophe for i18n issue --- Localization/app.json | 10 +++++----- Mastodon/Generated/Strings.swift | 10 +++++----- Mastodon/Resources/ar.lproj/Localizable.strings | 10 +++++----- Mastodon/Resources/en.lproj/Localizable.strings | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 90a63fc18..95410b8bb 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -187,7 +187,7 @@ "blocking_warning": "You can’t view Artbot’s profile\n until you unblock them.\nYour account looks like this to them.", "blocked_warning": "You can’t view Artbot’s profile\n until they unblock you.", "suspended_warning": "This account has been suspended.", - "user_suspended_warning": "%s's account has been suspended." + "user_suspended_warning": "%s’s account has been suspended." }, "accessibility": { "count_replies": "%s replies", @@ -290,7 +290,7 @@ }, "special": { "username_invalid": "Username must only contain alphanumeric characters and underscores", - "username_too_long": "Username is too long (can't be longer than 30 characters)", + "username_too_long": "Username is too long (can’t be longer than 30 characters)", "email_invalid": "This is not a valid e-mail address", "password_too_short": "Password is too short (must be at least 8 characters)" } @@ -299,7 +299,7 @@ "server_rules": { "title": "Some ground rules.", "subtitle": "These rules are set by the admins of %s.", - "prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.", + "prompt": "By continuing, you’re subject to the terms of service and privacy policy for %s.", "terms_of_service": "terms of service", "privacy_policy": "privacy policy", "button": { @@ -351,13 +351,13 @@ "photo_library": "Photo Library", "browse": "Browse" }, - "content_input_placeholder": "Type or paste what's on your mind", + "content_input_placeholder": "Type or paste what’s on your mind", "compose_action": "Publish", "replying_to_user": "replying to %s", "attachment": { "photo": "photo", "video": "video", - "attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon.", + "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe photo for low vision people...", "description_video": "Describe what’s happening for low vision people..." }, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 072105193..d1014b4be 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -367,7 +367,7 @@ internal enum L10n { internal static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound") /// This account has been suspended. internal static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning") - /// %@'s account has been suspended. + /// %@’s account has been suspended. internal static func userSuspendedWarning(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1)) } @@ -404,7 +404,7 @@ internal enum L10n { internal enum Compose { /// Publish internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction") - /// Type or paste what's on your mind + /// Type or paste what’s on your mind internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder") /// replying to %@ internal static func replyingToUser(_ p1: Any) -> String { @@ -435,7 +435,7 @@ internal enum L10n { internal static let removePoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.RemovePoll") } internal enum Attachment { - /// This %@ is broken and can't be\nuploaded to Mastodon. + /// This %@ is broken and can’t be\nuploaded to Mastodon. internal static func attachmentBroken(_ p1: Any) -> String { return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1)) } @@ -756,7 +756,7 @@ internal enum L10n { internal static let passwordTooShort = L10n.tr("Localizable", "Scene.Register.Error.Special.PasswordTooShort") /// Username must only contain alphanumeric characters and underscores internal static let usernameInvalid = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameInvalid") - /// Username is too long (can't be longer than 30 characters) + /// Username is too long (can’t be longer than 30 characters) internal static let usernameTooLong = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameTooLong") } } @@ -918,7 +918,7 @@ internal enum L10n { internal enum ServerRules { /// privacy policy internal static let privacyPolicy = L10n.tr("Localizable", "Scene.ServerRules.PrivacyPolicy") - /// By continuing, you're subject to the terms of service and privacy policy for %@. + /// By continuing, you’re subject to the terms of service and privacy policy for %@. internal static func prompt(_ p1: Any) -> String { return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1)) } diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 3d976d72a..ad7e4dd8d 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -128,7 +128,7 @@ Please check your internet connection."; Your account looks like this to them."; "Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found"; "Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended."; -"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended."; "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; @@ -145,7 +145,7 @@ Your account looks like this to them."; "Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu"; "Scene.Compose.Accessibility.RemovePoll" = "Remove poll"; -"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be +"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be uploaded to Mastodon."; "Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people..."; "Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people..."; @@ -154,7 +154,7 @@ uploaded to Mastodon."; "Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking"; "Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking"; "Scene.Compose.ComposeAction" = "Publish"; -"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind"; +"Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind"; "Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; "Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@"; "Scene.Compose.Keyboard.DiscardPost" = "Discard Post"; @@ -249,7 +249,7 @@ tap the link to confirm your account."; "Scene.Register.Error.Special.EmailInvalid" = "This is not a valid e-mail address"; "Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)"; "Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores"; -"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)"; +"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can’t be longer than 30 characters)"; "Scene.Register.Input.Avatar.Delete" = "Delete"; "Scene.Register.Input.DisplayName.Placeholder" = "display name"; "Scene.Register.Input.Email.Placeholder" = "email"; @@ -308,7 +308,7 @@ tap the link to confirm your account."; any server."; "Scene.ServerRules.Button.Confirm" = "I Agree"; "Scene.ServerRules.PrivacyPolicy" = "privacy policy"; -"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; +"Scene.ServerRules.Prompt" = "By continuing, you’re subject to the terms of service and privacy policy for %@."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.TermsOfService" = "terms of service"; "Scene.ServerRules.Title" = "Some ground rules."; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 3d976d72a..ad7e4dd8d 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -128,7 +128,7 @@ Please check your internet connection."; Your account looks like this to them."; "Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found"; "Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended."; -"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended."; "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; @@ -145,7 +145,7 @@ Your account looks like this to them."; "Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu"; "Scene.Compose.Accessibility.RemovePoll" = "Remove poll"; -"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be +"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be uploaded to Mastodon."; "Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people..."; "Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people..."; @@ -154,7 +154,7 @@ uploaded to Mastodon."; "Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking"; "Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking"; "Scene.Compose.ComposeAction" = "Publish"; -"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind"; +"Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind"; "Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; "Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@"; "Scene.Compose.Keyboard.DiscardPost" = "Discard Post"; @@ -249,7 +249,7 @@ tap the link to confirm your account."; "Scene.Register.Error.Special.EmailInvalid" = "This is not a valid e-mail address"; "Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)"; "Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores"; -"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)"; +"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can’t be longer than 30 characters)"; "Scene.Register.Input.Avatar.Delete" = "Delete"; "Scene.Register.Input.DisplayName.Placeholder" = "display name"; "Scene.Register.Input.Email.Placeholder" = "email"; @@ -308,7 +308,7 @@ tap the link to confirm your account."; any server."; "Scene.ServerRules.Button.Confirm" = "I Agree"; "Scene.ServerRules.PrivacyPolicy" = "privacy policy"; -"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; +"Scene.ServerRules.Prompt" = "By continuing, you’re subject to the terms of service and privacy policy for %@."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.TermsOfService" = "terms of service"; "Scene.ServerRules.Title" = "Some ground rules."; From e753da5ca47741e73ae99cb717083648e57fca1e Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 17:53:06 +0800 Subject: [PATCH 11/18] fix: content offset not take effect issue in compose scene --- Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift index 7a7eba12b..0b8d3e8f1 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift @@ -69,6 +69,9 @@ extension ComposeViewModel { snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status) } diffableDataSource.apply(snapshot, animatingDifferences: false) + + // some magic fix modal presentation animation issue + collectionView.dataSource = diffableDataSource } func setupCustomEmojiPickerDiffableDataSource( From 9fca59d40d61381e0bfb1faa1284e05199ddcd97 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 17:58:16 +0800 Subject: [PATCH 12/18] fix: set dismiss key board on drag for compose scene --- Mastodon/Scene/Compose/ComposeViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 6dfb77d3c..8eeff5512 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -55,6 +55,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self)) collectionView.backgroundColor = Asset.Scene.Compose.background.color collectionView.alwaysBounceVertical = true + collectionView.keyboardDismissMode = .onDrag return collectionView }() From b76f918a99b9ed63c30c6551248fb852560a8ec4 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 18:06:10 +0800 Subject: [PATCH 13/18] fix: set character count label font to monospace --- Mastodon/Scene/Compose/ComposeViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 8eeff5512..0c1bf3a34 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -381,11 +381,11 @@ extension ComposeViewController { self.composeToolbarView.characterCountLabel.text = "\(count)" switch count { case _ where count < 0: - self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold) + self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold) self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitExceedsCount(abs(count)) default: - self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular) + self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular) self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(count) } From f826cacccfbd5dab7cac895648d678e3c870869c Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 18:30:24 +0800 Subject: [PATCH 14/18] fix: add interact hint label and separator line for emoji auto complete in compose scene --- Localization/app.json | 3 ++- Mastodon/Diffiable/Section/AutoCompleteSection.swift | 6 ++++-- Mastodon/Generated/Strings.swift | 2 ++ Mastodon/Resources/ar.lproj/Localizable.strings | 1 + Mastodon/Resources/en.lproj/Localizable.strings | 1 + .../AutoComplete/Cell/AutoCompleteTableViewCell.swift | 11 +++++++++++ 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 95410b8bb..f370022ab 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -382,7 +382,8 @@ }, "auto_complete": { "single_people_talking": "%ld people talking", - "multiple_people_talking": "%ld people talking" + "multiple_people_talking": "%ld people talking", + "space_to_add": "Space to add" }, "accessibility": { "append_attachment": "Append attachment", diff --git a/Mastodon/Diffiable/Section/AutoCompleteSection.swift b/Mastodon/Diffiable/Section/AutoCompleteSection.swift index 39aa6e9cc..8de32a284 100644 --- a/Mastodon/Diffiable/Section/AutoCompleteSection.swift +++ b/Mastodon/Diffiable/Section/AutoCompleteSection.swift @@ -33,7 +33,7 @@ extension AutoCompleteSection { return cell case .emoji(let emoji): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AutoCompleteTableViewCell.self), for: indexPath) as! AutoCompleteTableViewCell - configureEmoji(cell: cell, emoji: emoji) + configureEmoji(cell: cell, emoji: emoji, isFirst: indexPath.row == 0) return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell @@ -80,8 +80,10 @@ extension AutoCompleteSection { cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: account.avatar))) } - private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji) { + private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji, isFirst: Bool) { cell.titleLabel.text = ":" + emoji.shortcode + ":" + // FIXME: handle spacer enter to complete emoji + // cell.subtitleLabel.text = isFirst ? L10n.Scene.Compose.AutoComplete.spaceToAdd : " " cell.subtitleLabel.text = " " cell.avatarImageView.isHidden = false cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: emoji.url))) diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index d1014b4be..07cab3974 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -457,6 +457,8 @@ internal enum L10n { internal static func singlePeopleTalking(_ p1: Int) -> String { return L10n.tr("Localizable", "Scene.Compose.AutoComplete.SinglePeopleTalking", p1) } + /// Space to add + internal static let spaceToAdd = L10n.tr("Localizable", "Scene.Compose.AutoComplete.SpaceToAdd") } internal enum ContentWarning { /// Write an accurate warning here... diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index ad7e4dd8d..e6bb7b217 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -153,6 +153,7 @@ uploaded to Mastodon."; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking"; "Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking"; +"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; "Scene.Compose.ComposeAction" = "Publish"; "Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind"; "Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index ad7e4dd8d..e6bb7b217 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -153,6 +153,7 @@ uploaded to Mastodon."; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking"; "Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking"; +"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; "Scene.Compose.ComposeAction" = "Publish"; "Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind"; "Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; diff --git a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift index 324f5354e..c9f0a55d1 100644 --- a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift +++ b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift @@ -45,6 +45,8 @@ final class AutoCompleteTableViewCell: UITableViewCell { return label }() + let separatorLine = UIView.separatorLine + override func prepareForReuse() { super.prepareForReuse() avatarImageView.af.cancelImageRequest() @@ -118,6 +120,15 @@ extension AutoCompleteTableViewCell { bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0), ]) + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separatorLine) + NSLayoutConstraint.activate([ + separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.defaultHigh), + ]) } } From b4c4153aaa40de35da9d8160d9cef7c754882357 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 18:58:58 +0800 Subject: [PATCH 15/18] feat: scroll to top when tap title view in home scene --- .../HomeTimelineViewController.swift | 4 +++ .../HomeTimelineNavigationBarTitleView.swift | 29 ++++++++++++------- ...eTimelineNavigationBarTitleViewModel.swift | 6 ++-- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 529f2d81c..b40a7f3ad 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -526,6 +526,10 @@ extension HomeTimelineViewController: StatusTableViewCellDelegate { // MARK: - HomeTimelineNavigationBarTitleViewDelegate extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate { + func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton) { + scrollToTop(animated: true) + } + func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) { switch titleView.state { case .newPostButton: diff --git a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift index 91020f12a..ac39ce1ae 100644 --- a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift +++ b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift @@ -9,6 +9,7 @@ import os.log import UIKit protocol HomeTimelineNavigationBarTitleViewDelegate: AnyObject { + func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton) func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) } @@ -16,7 +17,7 @@ final class HomeTimelineNavigationBarTitleView: UIView { let containerView = UIStackView() - let imageView = UIImageView() + let logoButton = HighlightDimmableButton() let button = RoundedEdgesButton() let label = UILabel() @@ -25,7 +26,7 @@ final class HomeTimelineNavigationBarTitleView: UIView { weak var delegate: HomeTimelineNavigationBarTitleViewDelegate? // output - private(set) var state: HomeTimelineNavigationBarTitleViewModel.State = .logoImage + private(set) var state: HomeTimelineNavigationBarTitleViewModel.State = .logo override init(frame: CGRect) { super.init(frame: frame) @@ -50,7 +51,7 @@ extension HomeTimelineNavigationBarTitleView { containerView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) - containerView.addArrangedSubview(imageView) + containerView.addArrangedSubview(logoButton) button.translatesAutoresizingMaskIntoConstraints = false containerView.addArrangedSubview(button) NSLayoutConstraint.activate([ @@ -58,12 +59,18 @@ extension HomeTimelineNavigationBarTitleView { ]) containerView.addArrangedSubview(label) - configure(state: .logoImage) + configure(state: .logo) + logoButton.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.logoButtonDidPressed(_:)), for: .touchUpInside) button.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.buttonDidPressed(_:)), for: .touchUpInside) } } extension HomeTimelineNavigationBarTitleView { + @objc private func logoButtonDidPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.homeTimelineNavigationBarTitleView(self, logoButtonDidPressed: sender) + } + @objc private func buttonDidPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) delegate?.homeTimelineNavigationBarTitleView(self, buttonDidPressed: sender) @@ -73,7 +80,7 @@ extension HomeTimelineNavigationBarTitleView { extension HomeTimelineNavigationBarTitleView { func resetContainer() { - imageView.isHidden = true + logoButton.isHidden = true button.isHidden = true label.isHidden = true } @@ -90,11 +97,11 @@ extension HomeTimelineNavigationBarTitleView { resetContainer() switch state { - case .logoImage: - imageView.tintColor = Asset.Colors.Label.primary.color - imageView.image = Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate) - imageView.contentMode = .center - imageView.isHidden = false + case .logo: + logoButton.tintColor = Asset.Colors.Label.primary.color + logoButton.setImage(Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate), for: .normal) + logoButton.contentMode = .center + logoButton.isHidden = false case .newPostButton: configureButton( title: L10n.Scene.HomeTimeline.NavigationBarState.newPosts, @@ -173,7 +180,7 @@ struct HomeTimelineNavigationBarTitleView_Previews: PreviewProvider { Group { UIViewPreview(width: 375) { let titleView = HomeTimelineNavigationBarTitleView() - titleView.configure(state: .logoImage) + titleView.configure(state: .logo) return titleView } .previewLayout(.fixed(width: 375, height: 44)) diff --git a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift index e1fc3174e..71b4dda8b 100644 --- a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift @@ -22,7 +22,7 @@ final class HomeTimelineNavigationBarTitleViewModel { var networkErrorPublisher = PassthroughSubject() // output - let state = CurrentValueSubject(.logoImage) + let state = CurrentValueSubject(.logo) let hasNewPosts = CurrentValueSubject(false) let isOffline = CurrentValueSubject(false) let isPublishingPost = CurrentValueSubject(false) @@ -75,7 +75,7 @@ final class HomeTimelineNavigationBarTitleViewModel { guard !isPublishingPost else { return .publishingPostLabel } guard !isOffline else { return .offlineButton } guard !hasNewPosts else { return .newPostButton } - return .logoImage + return .logo } .receive(on: DispatchQueue.main) .assign(to: \.value, on: state) @@ -100,7 +100,7 @@ final class HomeTimelineNavigationBarTitleViewModel { extension HomeTimelineNavigationBarTitleViewModel { // state order by priority from low to high enum State: String { - case logoImage + case logo case newPostButton case offlineButton case publishingPostLabel From 579d45b6759c44ba348c4cfe4ab8dcb8d3fddaf1 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 20:27:43 +0800 Subject: [PATCH 16/18] chore: add async entry --- Mastodon.xcodeproj/project.pbxproj | 4 ++++ Mastodon/Coordinator/SceneCoordinator.swift | 6 ++++++ .../Preference/HomeTimelinePreference.swift | 20 +++++++++++++++++++ ...meTimelineViewController+DebugAction.swift | 4 ++++ .../AsyncHomeTimelineViewController.swift | 16 ++++++++++++++- ...meTimelineViewController+DebugAction.swift | 4 ++++ .../HomeTimelineViewController.swift | 14 +++++++++++++ .../Scene/MainTab/MainTabBarController.swift | 2 +- .../Share/View/Node/Status/StatusNode.swift | 1 - Mastodon/State/AppContext.swift | 10 +++++++--- 10 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 Mastodon/Preference/HomeTimelinePreference.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e3365f6f6..61211f900 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -465,6 +465,7 @@ DBCBCC072680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC062680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift */; }; DBCBCC092680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC082680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift */; }; DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC0A2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift */; }; + DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */; }; DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; }; DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */; }; DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; }; @@ -1057,6 +1058,7 @@ DBCBCC062680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadLatestState.swift"; sourceTree = ""; }; DBCBCC082680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; DBCBCC0A2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; + DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelinePreference.swift; sourceTree = ""; }; DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = ""; }; DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = ""; }; DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = ""; }; @@ -1894,6 +1896,7 @@ DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */, DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */, DB1D842F26566512000346B3 /* KeyboardPreference.swift */, + DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */, ); path = Preference; sourceTree = ""; @@ -3346,6 +3349,7 @@ DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */, DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */, + DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */, DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */, DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 11053e660..f0ffcc892 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -48,6 +48,9 @@ extension SceneCoordinator { case mastodonResendEmail(viewModel: MastodonResendEmailViewModel) case mastodonWebView(viewModel:WebViewModel) + // ASDK + case asyncHome + // compose case compose(viewModel: ComposeViewModel) @@ -236,6 +239,9 @@ private extension SceneCoordinator { let _viewController = WebViewController() _viewController.viewModel = viewModel viewController = _viewController + case .asyncHome: + let _viewController = AsyncHomeTimelineViewController() + viewController = _viewController case .compose(let viewModel): let _viewController = ComposeViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Preference/HomeTimelinePreference.swift b/Mastodon/Preference/HomeTimelinePreference.swift new file mode 100644 index 000000000..123692db5 --- /dev/null +++ b/Mastodon/Preference/HomeTimelinePreference.swift @@ -0,0 +1,20 @@ +// +// HomeTimelinePreference.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-21. +// + +import UIKit + +extension UserDefaults { + + @objc dynamic var preferAsyncHomeTimeline: Bool { + get { + register(defaults: [#function: false]) + return bool(forKey: #function) + } + set { self[#function] = newValue } + } + +} diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+DebugAction.swift index 0362d6fe3..2e2304ac3 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+DebugAction.swift @@ -25,6 +25,10 @@ extension AsyncHomeTimelineViewController { guard let self = self else { return } self.showFLEXAction(action) }), + UIAction(title: "Toggle Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in + guard let self = self else { return } + self.context.toggleHomePreference(action) + }), moveMenu, dropMenu, UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift index 37f884255..93a0436f5 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift @@ -119,8 +119,22 @@ extension AsyncHomeTimelineViewController { #else settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) + settingBarButtonItem.action = #selector(AsyncHomeTimelineViewController.settingBarButtonItemPressed(_:)) #endif + settingBarButtonItem.menu = UIMenu(title: "Toggle Home", image: nil, identifier: nil, options: [], children: [ + UIAction(title: "Setting", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in + guard let self = self else { return } + self.settingBarButtonItemPressed(self.settingBarButtonItem) + }), + UIAction(title: "Toggle Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in + guard let self = self else { return } + self.context.toggleHomePreference(action) + let alertController = UIAlertController(title: "Please Restart App", message: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + }) + ]) navigationItem.rightBarButtonItem = composeBarButtonItem composeBarButtonItem.target = self diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 69eff4a82..588f90aeb 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -25,6 +25,10 @@ extension HomeTimelineViewController { guard let self = self else { return } self.showFLEXAction(action) }), + UIAction(title: "Toggle Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in + guard let self = self else { return } + self.context.toggleHomePreference(action) + }), moveMenu, dropMenu, UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index b40a7f3ad..34c5622c0 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -108,6 +108,20 @@ extension HomeTimelineViewController { settingBarButtonItem.target = self settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) #endif + settingBarButtonItem.menu = UIMenu(title: "Toggle Home", image: nil, identifier: nil, options: [], children: [ + UIAction(title: "Setting", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in + guard let self = self else { return } + self.settingBarButtonItemPressed(self.settingBarButtonItem) + }), + UIAction(title: "Show Async Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in + guard let self = self else { return } + self.context.toggleHomePreference(action) + let alertController = UIAlertController(title: "Please Restart App", message: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + }) + ]) navigationItem.rightBarButtonItem = composeBarButtonItem composeBarButtonItem.target = self diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index cd56e08c5..c0c65a203 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -45,7 +45,7 @@ class MainTabBarController: UITabBarController { let viewController: UIViewController switch self { case .home: - let _viewController = HomeTimelineViewController() + let _viewController: NeedsDependency & UIViewController = UserDefaults.shared.preferAsyncHomeTimeline ? AsyncHomeTimelineViewController() : HomeTimelineViewController() _viewController.context = context _viewController.coordinator = coordinator viewController = _viewController diff --git a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift index 9ddbcf87b..739babe39 100644 --- a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift +++ b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift @@ -85,7 +85,6 @@ final class StatusNode: ASCellNode { }() super.init() - print("meta: \(mosaicImageViewModel.metas.count), nodes: \(mediaMultiplexImageNodes.count)") automaticallyManagesSubnodes = true if let url = (status.reblog ?? status).author.avatarImageURL() { diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 416053b41..3989ea1b3 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -209,7 +209,11 @@ extension AppContext { } .eraseToAnyPublisher() } -// -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: purge temporary directory success", ((#file as NSString).lastPathComponent), #line, #function) -// } + +} + +extension AppContext { + @objc func toggleHomePreference(_ action: UIAction) { + UserDefaults.shared.preferAsyncHomeTimeline.toggle() + } } From 5b74763c232c64cfec54e45c7ead8994791a7f92 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 20:37:35 +0800 Subject: [PATCH 17/18] chore: update version to 0.7.1 (15) --- Mastodon.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 61211f900..c488f53c5 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3758,7 +3758,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3766,7 +3766,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.7.0; + MARKETING_VERSION = 0.7.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3785,7 +3785,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3793,7 +3793,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.7.0; + MARKETING_VERSION = 0.7.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4048,7 +4048,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4056,7 +4056,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.7.0; + MARKETING_VERSION = 0.7.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4071,7 +4071,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4079,7 +4079,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.7.0; + MARKETING_VERSION = 0.7.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; From b2070d3c3455b43aa6acee2f64b50d77619d1f4c Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 20:38:00 +0800 Subject: [PATCH 18/18] chore: add ASDK compiler flag. Update version to 0.7.1 (16) --- Mastodon.xcodeproj/project.pbxproj | 259 +++++++++++++++++- .../xcschemes/xcschememanagement.plist | 9 +- .../HomeTimelineViewController.swift | 10 +- .../Scene/MainTab/MainTabBarController.swift | 4 + 4 files changed, 271 insertions(+), 11 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c488f53c5..91a63516d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -758,6 +758,7 @@ 5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; 5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = ""; }; + 5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk.xcconfig"; sourceTree = ""; }; 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = ""; }; 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = ""; }; 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; @@ -769,12 +770,14 @@ 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = ""; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; + 819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk.xcconfig"; sourceTree = ""; }; 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = ""; }; 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = ""; }; 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; }; 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.debug.xcconfig"; sourceTree = ""; }; A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A67FD038ECDA0E411AF8DB4D /* Pods-Mastodon-MastodonUITests.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk.xcconfig"; sourceTree = ""; }; B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.release.xcconfig"; sourceTree = ""; }; B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = ""; }; BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; @@ -1090,6 +1093,8 @@ DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; sourceTree = ""; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = ""; }; ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.release.xcconfig"; sourceTree = ""; }; + EE13214BC0246BE5210CCC10 /* Pods-AppShared.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.asdk.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.asdk.xcconfig"; sourceTree = ""; }; + F31E7502A7E3945B98C6CBAF /* Pods-NotificationService.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.asdk.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.asdk.xcconfig"; sourceTree = ""; }; F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -1261,6 +1266,11 @@ ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */, 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */, 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */, + EE13214BC0246BE5210CCC10 /* Pods-AppShared.asdk.xcconfig */, + 819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */, + A67FD038ECDA0E411AF8DB4D /* Pods-Mastodon-MastodonUITests.asdk.xcconfig */, + 5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.xcconfig */, + F31E7502A7E3945B98C6CBAF /* Pods-NotificationService.asdk.xcconfig */, ); path = Pods; sourceTree = ""; @@ -3758,7 +3768,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3785,7 +3795,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4042,13 +4052,246 @@ }; name = Release; }; + DBCBCC0E2680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ASDK; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = ASDK; + }; + DBCBCC0F2680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 16; + DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + INFOPLIST_FILE = Mastodon/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.7.1; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ASDK; + }; + DBCBCC102680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + INFOPLIST_FILE = MastodonTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.MastodonTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; + }; + name = ASDK; + }; + DBCBCC112680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A67FD038ECDA0E411AF8DB4D /* Pods-Mastodon-MastodonUITests.asdk.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + INFOPLIST_FILE = MastodonUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.MastodonUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Mastodon; + }; + name = ASDK; + }; + DBCBCC122680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = CoreDataStack/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStack; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ASDK; + }; + DBCBCC132680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + INFOPLIST_FILE = CoreDataStackTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStackTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; + }; + name = ASDK; + }; + DBCBCC142680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F31E7502A7E3945B98C6CBAF /* Pods-NotificationService.asdk.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 16; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + INFOPLIST_FILE = NotificationService/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 0.7.1; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ASDK; + }; + DBCBCC152680BE3E000F5B51 /* ASDK */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EE13214BC0246BE5210CCC10 /* Pods-AppShared.asdk.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = AppShared/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ASDK; + }; DBF8AE1C263293E400C9C23C /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4071,7 +4314,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4095,6 +4338,7 @@ isa = XCConfigurationList; buildConfigurations = ( DB427DFA25BAA00100D1B89D /* Debug */, + DBCBCC0E2680BE3E000F5B51 /* ASDK */, DB427DFB25BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -4104,6 +4348,7 @@ isa = XCConfigurationList; buildConfigurations = ( DB427DFD25BAA00100D1B89D /* Debug */, + DBCBCC0F2680BE3E000F5B51 /* ASDK */, DB427DFE25BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -4113,6 +4358,7 @@ isa = XCConfigurationList; buildConfigurations = ( DB427E0025BAA00100D1B89D /* Debug */, + DBCBCC102680BE3E000F5B51 /* ASDK */, DB427E0125BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -4122,6 +4368,7 @@ isa = XCConfigurationList; buildConfigurations = ( DB427E0325BAA00100D1B89D /* Debug */, + DBCBCC112680BE3E000F5B51 /* ASDK */, DB427E0425BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -4131,6 +4378,7 @@ isa = XCConfigurationList; buildConfigurations = ( DB6804892637CD4C00430867 /* Debug */, + DBCBCC152680BE3E000F5B51 /* ASDK */, DB68048A2637CD4C00430867 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -4140,6 +4388,7 @@ isa = XCConfigurationList; buildConfigurations = ( DB89BA0625C10FD0008580ED /* Debug */, + DBCBCC122680BE3E000F5B51 /* ASDK */, DB89BA0725C10FD0008580ED /* Release */, ); defaultConfigurationIsVisible = 0; @@ -4149,6 +4398,7 @@ isa = XCConfigurationList; buildConfigurations = ( DB89BA0A25C10FD0008580ED /* Debug */, + DBCBCC132680BE3E000F5B51 /* ASDK */, DB89BA0B25C10FD0008580ED /* Release */, ); defaultConfigurationIsVisible = 0; @@ -4158,6 +4408,7 @@ isa = XCConfigurationList; buildConfigurations = ( DBF8AE1C263293E400C9C23C /* Debug */, + DBCBCC142680BE3E000F5B51 /* ASDK */, DBF8AE1D263293E400C9C23C /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 5de48646b..87ae76985 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,12 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 26 + 31 + + Mastodon - ASDK.xcscheme_^#shared#^_ + + orderHint + 3 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 27 + 30 SuppressBuildableAutocreation diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 34c5622c0..e6417f06c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -103,11 +103,7 @@ extension HomeTimelineViewController { // long press to trigger debug menu settingBarButtonItem.menu = debugMenu PerformanceMonitor.shared().delegate = self - - #else - settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) - #endif + #elseif ASDK settingBarButtonItem.menu = UIMenu(title: "Toggle Home", image: nil, identifier: nil, options: [], children: [ UIAction(title: "Setting", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in guard let self = self else { return } @@ -122,6 +118,10 @@ extension HomeTimelineViewController { self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) }) ]) + #else + settingBarButtonItem.target = self + settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) + #endif navigationItem.rightBarButtonItem = composeBarButtonItem composeBarButtonItem.target = self diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index c0c65a203..e16ebc202 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -45,7 +45,11 @@ class MainTabBarController: UITabBarController { let viewController: UIViewController switch self { case .home: + #if ASDK let _viewController: NeedsDependency & UIViewController = UserDefaults.shared.preferAsyncHomeTimeline ? AsyncHomeTimelineViewController() : HomeTimelineViewController() + #else + let _viewController = HomeTimelineViewController() + #endif _viewController.context = context _viewController.coordinator = coordinator viewController = _viewController