chore: add bottom loader
This commit is contained in:
parent
6e10efc490
commit
90803fc544
|
@ -20,10 +20,13 @@
|
||||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
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 */; };
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||||
|
2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A0A261D5A5600B44727 /* Array.swift */; };
|
||||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
||||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
||||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
||||||
|
2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */; };
|
||||||
|
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */; };
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
||||||
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
|
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||||
|
@ -97,7 +100,7 @@
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
||||||
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
||||||
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; };
|
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; };
|
||||||
2DE0FAC12615F04D00CDF649 /* RecomendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */; };
|
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */; };
|
||||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */; };
|
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */; };
|
||||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */; };
|
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */; };
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
||||||
|
@ -368,10 +371,13 @@
|
||||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
||||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||||
|
2D0B7A0A261D5A5600B44727 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||||
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
||||||
|
2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBottomLoader.swift; sourceTree = "<group>"; };
|
||||||
|
2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
||||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
||||||
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
||||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||||
|
@ -442,7 +448,7 @@
|
||||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||||
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
||||||
2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecomendHashTagSection.swift; sourceTree = "<group>"; };
|
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = "<group>"; };
|
||||||
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = "<group>"; };
|
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = "<group>"; };
|
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = "<group>"; };
|
||||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -949,7 +955,7 @@
|
||||||
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
||||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||||
2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */,
|
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
||||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||||
|
@ -1039,6 +1045,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */,
|
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */,
|
||||||
|
2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */,
|
||||||
);
|
);
|
||||||
path = TableViewCell;
|
path = TableViewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1470,6 +1477,7 @@
|
||||||
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
|
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
|
||||||
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */,
|
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */,
|
||||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
|
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
|
||||||
|
2D0B7A0A261D5A5600B44727 /* Array.swift */,
|
||||||
DB68A06225E905E000CFDF14 /* UIApplication.swift */,
|
DB68A06225E905E000CFDF14 /* UIApplication.swift */,
|
||||||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
||||||
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
||||||
|
@ -1532,6 +1540,7 @@
|
||||||
2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.swift */,
|
2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.swift */,
|
||||||
2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */,
|
2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */,
|
||||||
2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */,
|
2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */,
|
||||||
|
2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */,
|
||||||
2D34D9E026149C550081BFC0 /* CollectionViewCell */,
|
2D34D9E026149C550081BFC0 /* CollectionViewCell */,
|
||||||
);
|
);
|
||||||
path = Search;
|
path = Search;
|
||||||
|
@ -2097,6 +2106,7 @@
|
||||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
||||||
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
|
2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */,
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
||||||
|
@ -2135,7 +2145,7 @@
|
||||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
2DE0FAC12615F04D00CDF649 /* RecomendHashTagSection.swift in Sources */,
|
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
|
||||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||||
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
|
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
|
||||||
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
|
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
|
||||||
|
@ -2179,6 +2189,7 @@
|
||||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
|
2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */,
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||||
|
@ -2245,6 +2256,7 @@
|
||||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||||
|
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */,
|
||||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||||
|
|
|
@ -12,6 +12,8 @@ enum SearchResultItem {
|
||||||
case hashTag(tag: Mastodon.Entity.Tag)
|
case hashTag(tag: Mastodon.Entity.Tag)
|
||||||
|
|
||||||
case account(account: Mastodon.Entity.Account)
|
case account(account: Mastodon.Entity.Account)
|
||||||
|
|
||||||
|
case bottomLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultItem: Equatable {
|
extension SearchResultItem: Equatable {
|
||||||
|
@ -19,8 +21,10 @@ extension SearchResultItem: Equatable {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.hashTag(let tagLeft), .hashTag(let tagRight)):
|
case (.hashTag(let tagLeft), .hashTag(let tagRight)):
|
||||||
return tagLeft == tagRight
|
return tagLeft == tagRight
|
||||||
case (.account(let accountLeft), account(let accountRight)):
|
case (.account(let accountLeft), .account(let accountRight)):
|
||||||
return accountLeft == accountRight
|
return accountLeft == accountRight
|
||||||
|
case (.bottomLoader, .bottomLoader):
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -34,6 +38,8 @@ extension SearchResultItem: Hashable {
|
||||||
hasher.combine(account)
|
hasher.combine(account)
|
||||||
case .hashTag(let tag):
|
case .hashTag(let tag):
|
||||||
hasher.combine(tag)
|
hasher.combine(tag)
|
||||||
|
case .bottomLoader:
|
||||||
|
hasher.combine(String(describing: SearchResultItem.bottomLoader.self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// RecomendHashTagSection.swift
|
// RecommendHashTagSection.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by sxiaojian on 2021/4/1.
|
// Created by sxiaojian on 2021/4/1.
|
||||||
|
@ -9,14 +9,14 @@ import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
enum RecomendHashTagSection: Equatable, Hashable {
|
enum RecommendHashTagSection: Equatable, Hashable {
|
||||||
case main
|
case main
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RecomendHashTagSection {
|
extension RecommendHashTagSection {
|
||||||
static func collectionViewDiffableDataSource(
|
static func collectionViewDiffableDataSource(
|
||||||
for collectionView: UICollectionView
|
for collectionView: UICollectionView
|
||||||
) -> UICollectionViewDiffableDataSource<RecomendHashTagSection, Mastodon.Entity.Tag> {
|
) -> UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag> {
|
||||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, tag -> UICollectionViewCell? in
|
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, tag -> UICollectionViewCell? in
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self), for: indexPath) as! SearchRecommendTagsCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self), for: indexPath) as! SearchRecommendTagsCollectionViewCell
|
||||||
cell.config(with: tag)
|
cell.config(with: tag)
|
|
@ -12,6 +12,7 @@ import UIKit
|
||||||
enum SearchResultSection: Equatable, Hashable {
|
enum SearchResultSection: Equatable, Hashable {
|
||||||
case account
|
case account
|
||||||
case hashTag
|
case hashTag
|
||||||
|
case bottomLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultSection {
|
extension SearchResultSection {
|
||||||
|
@ -19,14 +20,20 @@ extension SearchResultSection {
|
||||||
for tableView: UITableView
|
for tableView: UITableView
|
||||||
) -> UITableViewDiffableDataSource<SearchResultSection, SearchResultItem> {
|
) -> UITableViewDiffableDataSource<SearchResultSection, SearchResultItem> {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, result) -> UITableViewCell? in
|
UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, result) -> UITableViewCell? in
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell
|
|
||||||
switch result {
|
switch result {
|
||||||
case .account(let account):
|
case .account(let account):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell
|
||||||
cell.config(with: account)
|
cell.config(with: account)
|
||||||
|
return cell
|
||||||
case .hashTag(let tag):
|
case .hashTag(let tag):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell
|
||||||
cell.config(with: tag)
|
cell.config(with: tag)
|
||||||
|
return cell
|
||||||
|
case .bottomLoader:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader
|
||||||
|
cell.startAnimating()
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
return cell
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// Array.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/7.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension Array where Element: Equatable {
|
||||||
|
|
||||||
|
func removeDuplicate() -> Array {
|
||||||
|
return self.enumerated().filter { (index,value) -> Bool in
|
||||||
|
return self.firstIndex(of: value) == index
|
||||||
|
}.map { (_, value) in
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,22 +25,6 @@ extension SearchViewController {
|
||||||
hashTagCollectionView.constrain([
|
hashTagCollectionView.constrain([
|
||||||
hashTagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130)
|
hashTagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130)
|
||||||
])
|
])
|
||||||
|
|
||||||
viewModel.requestRecommendHashTags()
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
if !self.viewModel.recommendHashTags.isEmpty {
|
|
||||||
let dataSource = RecomendHashTagSection.collectionViewDiffableDataSource(for: self.hashTagCollectionView)
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<RecomendHashTagSection, Mastodon.Entity.Tag>()
|
|
||||||
snapshot.appendSections([.main])
|
|
||||||
snapshot.appendItems(self.viewModel.recommendHashTags, toSection: .main)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
|
||||||
self.hashTagDiffableDataSource = dataSource
|
|
||||||
}
|
|
||||||
} receiveValue: { _ in
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupAccountsCollectionView() {
|
func setupAccountsCollectionView() {
|
||||||
|
@ -57,22 +41,6 @@ extension SearchViewController {
|
||||||
accountsCollectionView.constrain([
|
accountsCollectionView.constrain([
|
||||||
accountsCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 202)
|
accountsCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 202)
|
||||||
])
|
])
|
||||||
|
|
||||||
viewModel.requestRecommendAccounts()
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
if !self.viewModel.recommendAccounts.isEmpty {
|
|
||||||
let dataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: self.accountsCollectionView)
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, Mastodon.Entity.Account>()
|
|
||||||
snapshot.appendSections([.main])
|
|
||||||
snapshot.appendItems(self.viewModel.recommendAccounts, toSection: .main)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
|
||||||
self.accountDiffableDataSource = dataSource
|
|
||||||
}
|
|
||||||
} receiveValue: { _ in
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension SearchViewController {
|
extension SearchViewController {
|
||||||
func setupSearchingTableView() {
|
func setupSearchingTableView() {
|
||||||
searchingTableView.delegate = self
|
searchingTableView.delegate = self
|
||||||
searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self))
|
searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self))
|
||||||
|
searchingTableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self))
|
||||||
view.addSubview(searchingTableView)
|
view.addSubview(searchingTableView)
|
||||||
searchingTableView.constrain([
|
searchingTableView.constrain([
|
||||||
searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
|
@ -20,35 +22,11 @@ extension SearchViewController {
|
||||||
searchingTableView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
searchingTableView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||||
searchingTableView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
searchingTableView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||||
])
|
])
|
||||||
|
searchingTableView.tableFooterView = UIView()
|
||||||
viewModel.isSearching
|
viewModel.isSearching
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink {[weak self] isSearching in
|
.sink { [weak self] isSearching in
|
||||||
self?.searchingTableView.isHidden = !isSearching
|
self?.searchingTableView.isHidden = !isSearching
|
||||||
if !isSearching {
|
|
||||||
self?.searchResultDiffableDataSource = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
viewModel.searchResult
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] searchResult in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let dataSource = SearchResultSection.tableViewDiffableDataSource(for: self.searchingTableView)
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<SearchResultSection, SearchResultItem>()
|
|
||||||
if let accounts = searchResult?.accounts {
|
|
||||||
snapshot.appendSections([.account])
|
|
||||||
let items = accounts.compactMap { SearchResultItem.account(account: $0) }
|
|
||||||
snapshot.appendItems(items, toSection: .account)
|
|
||||||
}
|
|
||||||
if let tags = searchResult?.hashtags {
|
|
||||||
snapshot.appendSections([.hashTag])
|
|
||||||
let items = tags.compactMap { SearchResultItem.hashTag(tag: $0) }
|
|
||||||
snapshot.appendItems(items, toSection: .hashTag)
|
|
||||||
}
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
|
||||||
self.searchResultDiffableDataSource = dataSource
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
@ -60,8 +38,10 @@ extension SearchViewController: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
66
|
66
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
66
|
66
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||||
searchBar.setImage(micImage, for: .bookmark, state: .normal)
|
searchBar.setImage(micImage, for: .bookmark, state: .normal)
|
||||||
searchBar.showsBookmarkButton = true
|
searchBar.showsBookmarkButton = true
|
||||||
searchBar.showsScopeBar = false
|
searchBar.showsScopeBar = false
|
||||||
searchBar.scopeButtonTitles = [L10n.Scene.Search.Searching.Segment.all, L10n.Scene.Search.Searching.Segment.people,L10n.Scene.Search.Searching.Segment.hashtags]
|
searchBar.scopeButtonTitles = [L10n.Scene.Search.Searching.Segment.all, L10n.Scene.Search.Searching.Segment.people, L10n.Scene.Search.Searching.Segment.hashtags]
|
||||||
return searchBar
|
return searchBar
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -59,9 +60,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var hashTagDiffableDataSource: UICollectionViewDiffableDataSource<RecomendHashTagSection, Mastodon.Entity.Tag>?
|
|
||||||
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, Mastodon.Entity.Account>?
|
|
||||||
|
|
||||||
let accountsCollectionView: UICollectionView = {
|
let accountsCollectionView: UICollectionView = {
|
||||||
let flowLayout = UICollectionViewFlowLayout()
|
let flowLayout = UICollectionViewFlowLayout()
|
||||||
|
@ -83,7 +81,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||||
tableView.backgroundColor = .white
|
tableView.backgroundColor = .white
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
var searchResultDiffableDataSource: UITableViewDiffableDataSource<SearchResultSection, SearchResultItem>?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchViewController {
|
extension SearchViewController {
|
||||||
|
@ -97,6 +94,7 @@ extension SearchViewController {
|
||||||
setupHashTagCollectionView()
|
setupHashTagCollectionView()
|
||||||
setupAccountsCollectionView()
|
setupAccountsCollectionView()
|
||||||
setupSearchingTableView()
|
setupSearchingTableView()
|
||||||
|
setupDataSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupScrollView() {
|
func setupScrollView() {
|
||||||
|
@ -118,6 +116,20 @@ extension SearchViewController {
|
||||||
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
|
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupDataSource() {
|
||||||
|
viewModel.hashTagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashTagCollectionView)
|
||||||
|
viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView)
|
||||||
|
viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SearchViewController: UIScrollViewDelegate {
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if scrollView == searchingTableView {
|
||||||
|
handleScrollViewDidScroll(scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchViewController: UISearchBarDelegate {
|
extension SearchViewController: UISearchBarDelegate {
|
||||||
|
@ -150,16 +162,24 @@ extension SearchViewController: UISearchBarDelegate {
|
||||||
case 0:
|
case 0:
|
||||||
viewModel.searchScope.value = ""
|
viewModel.searchScope.value = ""
|
||||||
case 1:
|
case 1:
|
||||||
viewModel.searchScope.value = "accounts"
|
viewModel.searchScope.value = Mastodon.API.Search.Scope.accounts.rawValue
|
||||||
case 2:
|
case 2:
|
||||||
viewModel.searchScope.value = "hashtags"
|
viewModel.searchScope.value = Mastodon.API.Search.Scope.hashTags.rawValue
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {}
|
func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SearchViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
typealias BottomLoaderTableViewCell = SearchBottomLoader
|
||||||
|
typealias LoadingState = SearchViewModel.LoadOldestState.Loading
|
||||||
|
var loadMoreConfigurableTableView: UITableView { searchingTableView }
|
||||||
|
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||||
|
}
|
||||||
|
|
||||||
#if canImport(SwiftUI) && DEBUG
|
#if canImport(SwiftUI) && DEBUG
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
//
|
||||||
|
// SearchViewModel+LoadOldestState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/6.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
extension SearchViewModel {
|
||||||
|
class LoadOldestState: GKState {
|
||||||
|
weak var viewModel: SearchViewModel?
|
||||||
|
|
||||||
|
init(viewModel: SearchViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, debugDescription, previousState.debugDescription)
|
||||||
|
viewModel?.loadOldestStateMachinePublisher.send(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SearchViewModel.LoadOldestState {
|
||||||
|
class Initial: SearchViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
guard let viewModel = viewModel else { return false }
|
||||||
|
guard viewModel.searchResult.value != nil else { return false }
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: SearchViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
assertionFailure()
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let oldSearchResult = viewModel.searchResult.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var offset = 0
|
||||||
|
switch viewModel.searchScope.value {
|
||||||
|
case Mastodon.API.Search.Scope.accounts.rawValue:
|
||||||
|
offset = oldSearchResult.accounts.count
|
||||||
|
case Mastodon.API.Search.Scope.hashTags.rawValue:
|
||||||
|
offset = oldSearchResult.hashtags.count
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let query = Mastodon.API.Search.Query(accountID: nil,
|
||||||
|
maxID: nil,
|
||||||
|
minID: nil,
|
||||||
|
type: viewModel.searchScope.value,
|
||||||
|
excludeUnreviewed: nil,
|
||||||
|
q: viewModel.searchText.value,
|
||||||
|
resolve: nil,
|
||||||
|
limit: nil,
|
||||||
|
offset: offset,
|
||||||
|
following: nil)
|
||||||
|
viewModel.context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: load oldest search failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { result in
|
||||||
|
switch viewModel.searchScope.value {
|
||||||
|
case Mastodon.API.Search.Scope.accounts.rawValue:
|
||||||
|
if result.value.accounts.isEmpty {
|
||||||
|
stateMachine.enter(NoMore.self)
|
||||||
|
} else {
|
||||||
|
var newAccounts = [Mastodon.Entity.Account]()
|
||||||
|
newAccounts.append(contentsOf: oldSearchResult.accounts)
|
||||||
|
newAccounts.append(contentsOf: result.value.accounts)
|
||||||
|
viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: newAccounts.removeDuplicate(), statuses: oldSearchResult.statuses, hashtags: oldSearchResult.hashtags)
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
case Mastodon.API.Search.Scope.hashTags.rawValue:
|
||||||
|
if result.value.hashtags.isEmpty {
|
||||||
|
stateMachine.enter(NoMore.self)
|
||||||
|
} else {
|
||||||
|
var newTags = [Mastodon.Entity.Tag]()
|
||||||
|
newTags.append(contentsOf: oldSearchResult.hashtags)
|
||||||
|
newTags.append(contentsOf: result.value.hashtags)
|
||||||
|
viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: oldSearchResult.accounts, statuses: oldSearchResult.statuses, hashtags: newTags.removeDuplicate())
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: SearchViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Loading.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: SearchViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoMore: SearchViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// reset state if needs
|
||||||
|
stateClass == Idle.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
guard let diffableDataSource = viewModel.searchResultDiffableDataSource else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var snapshot = diffableDataSource.snapshot()
|
||||||
|
snapshot.deleteItems([.bottomLoader])
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import OSLog
|
import OSLog
|
||||||
import UIKit
|
import UIKit
|
||||||
|
@ -28,6 +29,26 @@ final class SearchViewModel {
|
||||||
var recommendHashTags = [Mastodon.Entity.Tag]()
|
var recommendHashTags = [Mastodon.Entity.Tag]()
|
||||||
var recommendAccounts = [Mastodon.Entity.Account]()
|
var recommendAccounts = [Mastodon.Entity.Account]()
|
||||||
|
|
||||||
|
var hashTagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
|
||||||
|
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, Mastodon.Entity.Account>?
|
||||||
|
var searchResultDiffableDataSource: UITableViewDiffableDataSource<SearchResultSection, SearchResultItem>?
|
||||||
|
|
||||||
|
// bottom loader
|
||||||
|
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
LoadOldestState.Initial(viewModel: self),
|
||||||
|
LoadOldestState.Loading(viewModel: self),
|
||||||
|
LoadOldestState.Fail(viewModel: self),
|
||||||
|
LoadOldestState.Idle(viewModel: self),
|
||||||
|
LoadOldestState.NoMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(LoadOldestState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
|
||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
@ -60,10 +81,66 @@ final class SearchViewModel {
|
||||||
isSearching
|
isSearching
|
||||||
.sink { [weak self] isSearching in
|
.sink { [weak self] isSearching in
|
||||||
if !isSearching {
|
if !isSearching {
|
||||||
self?.searchResult.value == nil
|
self?.searchResult.value = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
requestRecommendHashTags()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !self.recommendHashTags.isEmpty {
|
||||||
|
guard let dataSource = self.hashTagDiffableDataSource else { return }
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<RecommendHashTagSection, Mastodon.Entity.Tag>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
snapshot.appendItems(self.recommendHashTags, toSection: .main)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
|
}
|
||||||
|
} receiveValue: { _ in
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
requestRecommendAccounts()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !self.recommendAccounts.isEmpty {
|
||||||
|
guard let dataSource = self.accountDiffableDataSource else { return }
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, Mastodon.Entity.Account>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
snapshot.appendItems(self.recommendAccounts, toSection: .main)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
|
}
|
||||||
|
} receiveValue: { _ in
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
searchResult
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] searchResult in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let dataSource = self.searchResultDiffableDataSource else { return }
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<SearchResultSection, SearchResultItem>()
|
||||||
|
if let accounts = searchResult?.accounts {
|
||||||
|
snapshot.appendSections([.account])
|
||||||
|
let items = accounts.compactMap { SearchResultItem.account(account: $0) }
|
||||||
|
snapshot.appendItems(items, toSection: .account)
|
||||||
|
if self.searchScope.value == Mastodon.API.Search.Scope.accounts.rawValue {
|
||||||
|
snapshot.appendItems([.bottomLoader], toSection: .account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let tags = searchResult?.hashtags {
|
||||||
|
snapshot.appendSections([.hashTag])
|
||||||
|
let items = tags.compactMap { SearchResultItem.hashTag(tag: $0) }
|
||||||
|
snapshot.appendItems(items, toSection: .hashTag)
|
||||||
|
if self.searchScope.value == Mastodon.API.Search.Scope.hashTags.rawValue {
|
||||||
|
snapshot.appendItems([.bottomLoader], toSection: .hashTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestRecommendHashTags() -> Future<Void, Error> {
|
func requestRecommendHashTags() -> Future<Void, Error> {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
//
|
||||||
|
// SearchBottomLoader.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/6.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class SearchBottomLoader: UITableViewCell {
|
||||||
|
let activityIndicatorView: UIActivityIndicatorView = {
|
||||||
|
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||||
|
activityIndicatorView.tintColor = Asset.Colors.Label.primary.color
|
||||||
|
activityIndicatorView.hidesWhenStopped = true
|
||||||
|
return activityIndicatorView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startAnimating() {
|
||||||
|
activityIndicatorView.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAnimating() {
|
||||||
|
activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
func _init() {
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = Asset.Colors.lightWhite.color
|
||||||
|
contentView.addSubview(activityIndicatorView)
|
||||||
|
activityIndicatorView.constrainToCenter()
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,8 +6,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
|
||||||
final class SearchingTableViewCell: UITableViewCell {
|
final class SearchingTableViewCell: UITableViewCell {
|
||||||
let _imageView: UIImageView = {
|
let _imageView: UIImageView = {
|
||||||
|
@ -50,7 +50,7 @@ final class SearchingTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
extension SearchingTableViewCell {
|
extension SearchingTableViewCell {
|
||||||
private func configure() {
|
private func configure() {
|
||||||
self.selectionStyle = .none
|
selectionStyle = .none
|
||||||
contentView.addSubview(_imageView)
|
contentView.addSubview(_imageView)
|
||||||
_imageView.pin(toSize: CGSize(width: 42, height: 42))
|
_imageView.pin(toSize: CGSize(width: 42, height: 42))
|
||||||
_imageView.constrain([
|
_imageView.constrain([
|
||||||
|
@ -65,28 +65,28 @@ extension SearchingTableViewCell {
|
||||||
_subTitleLabel.pin(top: 34, left: 75, bottom: nil, right: 0)
|
_subTitleLabel.pin(top: 34, left: 75, bottom: nil, right: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with account:Mastodon.Entity.Account) {
|
func config(with account: Mastodon.Entity.Account) {
|
||||||
self._imageView.af.setImage(
|
_imageView.af.setImage(
|
||||||
withURL: URL(string: account.avatar)!,
|
withURL: URL(string: account.avatar)!,
|
||||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
imageTransition: .crossDissolve(0.2)
|
imageTransition: .crossDissolve(0.2)
|
||||||
)
|
)
|
||||||
self._titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
||||||
self._subTitleLabel.text = account.acct
|
_subTitleLabel.text = account.acct
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with tag:Mastodon.Entity.Tag) {
|
func config(with tag: Mastodon.Entity.Tag) {
|
||||||
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
|
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
|
||||||
self._imageView.image = image
|
_imageView.image = image
|
||||||
self._titleLabel.text = "# " + tag.name
|
_titleLabel.text = "# " + tag.name
|
||||||
guard let historys = tag.history else {
|
guard let historys = tag.history else {
|
||||||
self._subTitleLabel.text = ""
|
_subTitleLabel.text = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let recentHistory = historys[0...2]
|
let recentHistory = historys[0 ... 2]
|
||||||
let peopleAreTalking = recentHistory.compactMap({ Int($0.accounts) }).reduce(0, +)
|
let peopleAreTalking = recentHistory.compactMap { Int($0.accounts) }.reduce(0, +)
|
||||||
let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking))
|
let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking))
|
||||||
self._subTitleLabel.text = string
|
_subTitleLabel.text = string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,8 @@ extension Mastodon.API.Search {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.API.Search {
|
public extension Mastodon.API.Search {
|
||||||
public struct Query: Codable, GetQuery {
|
struct Query: Codable, GetQuery {
|
||||||
public init(accountID: Mastodon.Entity.Account.ID?, maxID: Mastodon.Entity.Status.ID?, minID: Mastodon.Entity.Status.ID?, type: String?, excludeUnreviewed: Bool?, q: String, resolve: Bool?, limit: Int?, offset: Int?, following: Bool?) {
|
public init(accountID: Mastodon.Entity.Account.ID?, maxID: Mastodon.Entity.Status.ID?, minID: Mastodon.Entity.Status.ID?, type: String?, excludeUnreviewed: Bool?, q: String, resolve: Bool?, limit: Int?, offset: Int?, following: Bool?) {
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.maxID = maxID
|
self.maxID = maxID
|
||||||
|
@ -93,3 +93,19 @@ extension Mastodon.API.Search {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Mastodon.API.Search {
|
||||||
|
enum Scope: String {
|
||||||
|
case accounts
|
||||||
|
case hashTags
|
||||||
|
|
||||||
|
public var rawValue: String {
|
||||||
|
switch self {
|
||||||
|
case .accounts:
|
||||||
|
return "accounts"
|
||||||
|
case .hashTags:
|
||||||
|
return "hashtags"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
extension Mastodon.Entity {
|
extension Mastodon.Entity {
|
||||||
public struct SearchResult: Codable {
|
public struct SearchResult: Codable {
|
||||||
|
public init(accounts: [Mastodon.Entity.Account], statuses: [Mastodon.Entity.Status], hashtags: [Mastodon.Entity.Tag]) {
|
||||||
|
self.accounts = accounts
|
||||||
|
self.statuses = statuses
|
||||||
|
self.hashtags = hashtags
|
||||||
|
}
|
||||||
|
|
||||||
public let accounts: [Mastodon.Entity.Account]
|
public let accounts: [Mastodon.Entity.Account]
|
||||||
public let statuses: [Mastodon.Entity.Status]
|
public let statuses: [Mastodon.Entity.Status]
|
||||||
public let hashtags: [Mastodon.Entity.Tag]
|
public let hashtags: [Mastodon.Entity.Tag]
|
||||||
|
|
Loading…
Reference in New Issue