Merge pull request #1115 from mastodon/141-improve-search-flow
Better Search Workflow (IOS-141)
This commit is contained in:
commit
40f20641bc
|
@ -651,16 +651,15 @@
|
|||
}
|
||||
},
|
||||
"searching": {
|
||||
"segment": {
|
||||
"all": "All",
|
||||
"people": "People",
|
||||
"hashtags": "Hashtags",
|
||||
"posts": "Posts"
|
||||
},
|
||||
"posts": "Posts with \"%@\"",
|
||||
"people": "People with \"%@\"",
|
||||
"profile": "Go to @%@@%@",
|
||||
"url": "Open Link",
|
||||
"empty_state": {
|
||||
"no_results": "No results"
|
||||
},
|
||||
"recent_search": "Recent searches",
|
||||
"clear_all": "Clear all",
|
||||
"clear": "Clear"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -651,15 +651,18 @@
|
|||
}
|
||||
},
|
||||
"searching": {
|
||||
"segment": {
|
||||
"all": "All",
|
||||
"people": "People",
|
||||
"hashtags": "Hashtags",
|
||||
"posts": "Posts"
|
||||
},
|
||||
"posts": "Posts matching \"%@\"",
|
||||
"people": "People matching \"%@\"",
|
||||
"hashtag": "Go to #%@",
|
||||
"profile": "Go to @%@@%@",
|
||||
"url": "Open Link",
|
||||
"empty_state": {
|
||||
"no_results": "No results"
|
||||
},
|
||||
"no_user": {
|
||||
"title": "No User Account Found",
|
||||
"message": "There's no Useraccount \"%@\" on %@"
|
||||
}
|
||||
"recent_search": "Recent searches",
|
||||
"clear": "Clear"
|
||||
}
|
||||
|
|
|
@ -139,6 +139,9 @@
|
|||
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; };
|
||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
||||
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; };
|
||||
D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */; };
|
||||
D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */; };
|
||||
D81A227B2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */; };
|
||||
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; };
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; };
|
||||
|
@ -148,6 +151,8 @@
|
|||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; };
|
||||
D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */; };
|
||||
D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; };
|
||||
D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; };
|
||||
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; };
|
||||
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; };
|
||||
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; };
|
||||
|
@ -194,7 +199,6 @@
|
|||
DB0FCB842796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB832796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift */; };
|
||||
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB852796BDA1006C02E2 /* SearchSection.swift */; };
|
||||
DB0FCB882796BDA9006C02E2 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB872796BDA9006C02E2 /* SearchItem.swift */; };
|
||||
DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */; };
|
||||
DB0FCB8E2796C0B7006C02E2 /* TrendCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB8D2796C0B7006C02E2 /* TrendCollectionViewCell.swift */; };
|
||||
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB912796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift */; };
|
||||
DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */; };
|
||||
|
@ -253,7 +257,6 @@
|
|||
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4AA6B227BA34B6009EC082 /* CellFrameCacheContainer.swift */; };
|
||||
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */; };
|
||||
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */; };
|
||||
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; };
|
||||
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */; };
|
||||
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */; };
|
||||
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */; };
|
||||
|
@ -298,7 +301,6 @@
|
|||
DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; };
|
||||
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; };
|
||||
DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */; };
|
||||
DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */; };
|
||||
DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */; };
|
||||
DB63F764279A5E3C00455B82 /* NotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */; };
|
||||
DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */; };
|
||||
|
@ -776,6 +778,9 @@
|
|||
D8099077294BC8A30050219F /* PrivacyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewController.swift; sourceTree = "<group>"; };
|
||||
D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = "<group>"; };
|
||||
D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverviewTableViewController.swift; sourceTree = "<group>"; };
|
||||
D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewSection.swift; sourceTree = "<group>"; };
|
||||
D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultDefaultSectionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D82463522A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||
D82463532A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
|
||||
D82463542A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
|
@ -799,6 +804,8 @@
|
|||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = "<group>"; };
|
||||
D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = "<group>"; };
|
||||
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
|
||||
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||
D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = "<group>"; };
|
||||
|
@ -847,7 +854,6 @@
|
|||
DB0FCB832796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
|
||||
DB0FCB852796BDA1006C02E2 /* SearchSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSection.swift; sourceTree = "<group>"; };
|
||||
DB0FCB872796BDA9006C02E2 /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = "<group>"; };
|
||||
DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB0FCB8D2796C0B7006C02E2 /* TrendCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB0FCB912796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendSectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
|
||||
DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
|
@ -929,7 +935,6 @@
|
|||
DB4B779626CA50BA00B087B3 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Intents.stringsdict; sourceTree = "<group>"; };
|
||||
DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultViewController.swift; sourceTree = "<group>"; };
|
||||
DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultViewModel.swift; sourceTree = "<group>"; };
|
||||
DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryTableHeaderView.swift; sourceTree = "<group>"; };
|
||||
DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewModel.swift; sourceTree = "<group>"; };
|
||||
DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySection.swift; sourceTree = "<group>"; };
|
||||
|
@ -989,7 +994,6 @@
|
|||
DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
|
||||
DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+SearchHistory.swift"; sourceTree = "<group>"; };
|
||||
DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryUserCollectionViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
|
||||
DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1795,6 +1799,26 @@
|
|||
path = Privacy;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D81A22732AB4641F00905D71 /* Search Results Overview */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D81A22792AB47B8400905D71 /* Cells */,
|
||||
D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */,
|
||||
D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */,
|
||||
D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */,
|
||||
);
|
||||
path = "Search Results Overview";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D81A22792AB47B8400905D71 /* Cells */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */,
|
||||
D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */,
|
||||
);
|
||||
path = Cells;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6AB68291C50F3003AB663 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2101,6 +2125,8 @@
|
|||
DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */,
|
||||
DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */,
|
||||
DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */,
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
||||
);
|
||||
path = SearchResult;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2110,10 +2136,6 @@
|
|||
children = (
|
||||
DB0FCB852796BDA1006C02E2 /* SearchSection.swift */,
|
||||
DB0FCB872796BDA9006C02E2 /* SearchItem.swift */,
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
||||
DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */,
|
||||
DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2136,14 +2158,6 @@
|
|||
path = Status;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4F098026A0475500D62E92 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4FFC2D269EC39C00D62E92 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2282,7 +2296,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */,
|
||||
DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */,
|
||||
DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
|
@ -2905,6 +2918,7 @@
|
|||
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D81A22732AB4641F00905D71 /* Search Results Overview */,
|
||||
DB4F0964269ED06700D62E92 /* SearchResult */,
|
||||
DBF1D252269DB01700C1C08A /* SearchHistory */,
|
||||
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */,
|
||||
|
@ -2917,11 +2931,12 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB63F7502799449300455B82 /* Cell */,
|
||||
DB4F098026A0475500D62E92 /* View */,
|
||||
DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */,
|
||||
DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */,
|
||||
DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */,
|
||||
DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */,
|
||||
DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */,
|
||||
DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */,
|
||||
);
|
||||
path = SearchHistory;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2933,7 +2948,6 @@
|
|||
2DE0FAC62615F5D200CDF649 /* View */,
|
||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */,
|
||||
2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */,
|
||||
DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3544,8 +3558,8 @@
|
|||
DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */,
|
||||
D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */,
|
||||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||
DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */,
|
||||
DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */,
|
||||
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
|
||||
5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */,
|
||||
|
@ -3593,7 +3607,6 @@
|
|||
DB0FCB9C27980AB6006C02E2 /* HashtagTimelineViewController+DataSourceProvider.swift in Sources */,
|
||||
DB63F76F279A7D1100455B82 /* NotificationTableViewCell.swift in Sources */,
|
||||
2A1BF99529F7E68400FA1BA5 /* DataSourceFacade+UserView.swift in Sources */,
|
||||
DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */,
|
||||
DBEFCD76282A143F00C0ABEA /* ReportStatusViewController.swift in Sources */,
|
||||
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,
|
||||
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
|
||||
|
@ -3698,8 +3711,8 @@
|
|||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
||||
6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */,
|
||||
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */,
|
||||
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */,
|
||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||
D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */,
|
||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */,
|
||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */,
|
||||
|
@ -3796,8 +3809,10 @@
|
|||
DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */,
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||
D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */,
|
||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */,
|
||||
D81A227B2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift in Sources */,
|
||||
6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */,
|
||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
||||
|
@ -3878,6 +3893,7 @@
|
|||
DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */,
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
||||
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
|
||||
D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */,
|
||||
2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */,
|
||||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
|
||||
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
||||
|
|
|
@ -9,10 +9,13 @@ import UIKit
|
|||
import MastodonCore
|
||||
|
||||
protocol NeedsDependency: AnyObject {
|
||||
//FIXME: Get rid of ! ~@zeitschlag
|
||||
var context: AppContext! { get set }
|
||||
var coordinator: SceneCoordinator! { get set }
|
||||
}
|
||||
|
||||
typealias ViewControllerWithDependencies = NeedsDependency & UIViewController
|
||||
|
||||
extension UISceneSession {
|
||||
private struct AssociatedKeys {
|
||||
static var sceneCoordinator = "SceneCoordinator"
|
||||
|
|
|
@ -153,6 +153,7 @@ extension SceneCoordinator {
|
|||
|
||||
// search
|
||||
case searchDetail(viewModel: SearchDetailViewModel)
|
||||
case searchResult(viewModel: SearchResultViewModel)
|
||||
|
||||
// compose
|
||||
case compose(viewModel: ComposeViewModel)
|
||||
|
@ -376,159 +377,169 @@ private extension SceneCoordinator {
|
|||
let viewController: UIViewController?
|
||||
|
||||
switch scene {
|
||||
case .welcome:
|
||||
let _viewController = WelcomeViewController()
|
||||
viewController = _viewController
|
||||
case .mastodonPickServer(let viewModel):
|
||||
let _viewController = MastodonPickServerViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonRegister(let viewModel):
|
||||
let _viewController = MastodonRegisterViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonServerRules(let viewModel):
|
||||
let _viewController = MastodonServerRulesViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonConfirmEmail(let viewModel):
|
||||
let _viewController = MastodonConfirmEmailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonLogin:
|
||||
let loginViewController = MastodonLoginViewController(appContext: appContext,
|
||||
authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false),
|
||||
sceneCoordinator: self)
|
||||
loginViewController.delegate = self
|
||||
case .welcome:
|
||||
let _viewController = WelcomeViewController()
|
||||
viewController = _viewController
|
||||
case .mastodonPickServer(let viewModel):
|
||||
let _viewController = MastodonPickServerViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonRegister(let viewModel):
|
||||
let _viewController = MastodonRegisterViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonServerRules(let viewModel):
|
||||
let _viewController = MastodonServerRulesViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonConfirmEmail(let viewModel):
|
||||
let _viewController = MastodonConfirmEmailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonLogin:
|
||||
let loginViewController = MastodonLoginViewController(appContext: appContext,
|
||||
authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false),
|
||||
sceneCoordinator: self)
|
||||
loginViewController.delegate = self
|
||||
|
||||
viewController = loginViewController
|
||||
case .mastodonPrivacyPolicies(let viewModel):
|
||||
let privacyViewController = PrivacyTableViewController(context: appContext, coordinator: self, viewModel: viewModel)
|
||||
viewController = privacyViewController
|
||||
case .mastodonResendEmail(let viewModel):
|
||||
let _viewController = MastodonResendEmailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonWebView(let viewModel):
|
||||
let _viewController = WebViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .searchDetail(let viewModel):
|
||||
let _viewController = SearchDetailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .compose(let viewModel):
|
||||
let _viewController = ComposeViewController(viewModel: viewModel)
|
||||
viewController = _viewController
|
||||
case .thread(let viewModel):
|
||||
let _viewController = ThreadViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .editHistory(let viewModel):
|
||||
let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel)
|
||||
viewController = editHistoryViewController
|
||||
case .hashtagTimeline(let viewModel):
|
||||
let _viewController = HashtagTimelineViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .accountList(let viewModel):
|
||||
let _viewController = AccountListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .profile(let viewModel):
|
||||
let _viewController = ProfileViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .bookmark(let viewModel):
|
||||
let _viewController = BookmarkViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .followedTags(let viewModel):
|
||||
let _viewController = FollowedTagsViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .favorite(let viewModel):
|
||||
let _viewController = FavoriteViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .follower(let viewModel):
|
||||
let _viewController = FollowerListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .following(let viewModel):
|
||||
let _viewController = FollowingListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .familiarFollowers(let viewModel):
|
||||
let _viewController = FamiliarFollowersViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .rebloggedBy(let viewModel):
|
||||
let _viewController = RebloggedByViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .favoritedBy(let viewModel):
|
||||
let _viewController = FavoritedByViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .report(let viewModel):
|
||||
let _viewController = ReportViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportServerRules(let viewModel):
|
||||
let _viewController = ReportServerRulesViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportStatus(let viewModel):
|
||||
let _viewController = ReportStatusViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportSupplementary(let viewModel):
|
||||
let _viewController = ReportSupplementaryViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportResult(let viewModel):
|
||||
let _viewController = ReportResultViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .suggestionAccount(let viewModel):
|
||||
let _viewController = SuggestionAccountViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mediaPreview(let viewModel):
|
||||
let _viewController = MediaPreviewViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .safari(let url):
|
||||
guard let scheme = url.scheme?.lowercased(),
|
||||
scheme == "http" || scheme == "https" else {
|
||||
return nil
|
||||
}
|
||||
let _viewController = SFSafariViewController(url: url)
|
||||
_viewController.preferredBarTintColor = ThemeService.shared.currentTheme.value.navigationBarBackgroundColor
|
||||
_viewController.preferredControlTintColor = Asset.Colors.Brand.blurple.color
|
||||
viewController = _viewController
|
||||
viewController = loginViewController
|
||||
case .mastodonPrivacyPolicies(let viewModel):
|
||||
let privacyViewController = PrivacyTableViewController(context: appContext, coordinator: self, viewModel: viewModel)
|
||||
viewController = privacyViewController
|
||||
case .mastodonResendEmail(let viewModel):
|
||||
let _viewController = MastodonResendEmailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonWebView(let viewModel):
|
||||
let _viewController = WebViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
|
||||
case .alertController(let alertController):
|
||||
if let popoverPresentationController = alertController.popoverPresentationController {
|
||||
assert(
|
||||
popoverPresentationController.sourceView != nil ||
|
||||
popoverPresentationController.sourceRect != .zero ||
|
||||
popoverPresentationController.barButtonItem != nil
|
||||
)
|
||||
}
|
||||
viewController = alertController
|
||||
case .activityViewController(let activityViewController, let sourceView, let barButtonItem):
|
||||
activityViewController.popoverPresentationController?.sourceView = sourceView
|
||||
activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
|
||||
viewController = activityViewController
|
||||
case .settings(let viewModel):
|
||||
let _viewController = SettingsViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .editStatus(let viewModel):
|
||||
let composeViewController = ComposeViewController(viewModel: viewModel)
|
||||
viewController = composeViewController
|
||||
|
||||
case .searchDetail(let viewModel):
|
||||
let _viewController = SearchDetailViewController(appContext: appContext, sceneCoordinator: self, authContext: viewModel.authContext)
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .searchResult(let viewModel):
|
||||
let searchResultViewController = SearchResultViewController()
|
||||
searchResultViewController.context = appContext
|
||||
searchResultViewController.coordinator = self
|
||||
searchResultViewController.viewModel = viewModel
|
||||
viewController = searchResultViewController
|
||||
|
||||
|
||||
case .compose(let viewModel):
|
||||
let _viewController = ComposeViewController(viewModel: viewModel)
|
||||
viewController = _viewController
|
||||
case .thread(let viewModel):
|
||||
let _viewController = ThreadViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .editHistory(let viewModel):
|
||||
let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel)
|
||||
viewController = editHistoryViewController
|
||||
case .hashtagTimeline(let viewModel):
|
||||
let _viewController = HashtagTimelineViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .accountList(let viewModel):
|
||||
let _viewController = AccountListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .profile(let viewModel):
|
||||
let _viewController = ProfileViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .bookmark(let viewModel):
|
||||
let _viewController = BookmarkViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .followedTags(let viewModel):
|
||||
let _viewController = FollowedTagsViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .favorite(let viewModel):
|
||||
let _viewController = FavoriteViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .follower(let viewModel):
|
||||
let _viewController = FollowerListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .following(let viewModel):
|
||||
let _viewController = FollowingListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .familiarFollowers(let viewModel):
|
||||
let _viewController = FamiliarFollowersViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .rebloggedBy(let viewModel):
|
||||
let _viewController = RebloggedByViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .favoritedBy(let viewModel):
|
||||
let _viewController = FavoritedByViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .report(let viewModel):
|
||||
let _viewController = ReportViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportServerRules(let viewModel):
|
||||
let _viewController = ReportServerRulesViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportStatus(let viewModel):
|
||||
let _viewController = ReportStatusViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportSupplementary(let viewModel):
|
||||
let _viewController = ReportSupplementaryViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportResult(let viewModel):
|
||||
let _viewController = ReportResultViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .suggestionAccount(let viewModel):
|
||||
let _viewController = SuggestionAccountViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mediaPreview(let viewModel):
|
||||
let _viewController = MediaPreviewViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .safari(let url):
|
||||
guard let scheme = url.scheme?.lowercased(),
|
||||
scheme == "http" || scheme == "https" else {
|
||||
return nil
|
||||
}
|
||||
let _viewController = SFSafariViewController(url: url)
|
||||
_viewController.preferredBarTintColor = ThemeService.shared.currentTheme.value.navigationBarBackgroundColor
|
||||
_viewController.preferredControlTintColor = Asset.Colors.Brand.blurple.color
|
||||
viewController = _viewController
|
||||
|
||||
case .alertController(let alertController):
|
||||
if let popoverPresentationController = alertController.popoverPresentationController {
|
||||
assert(
|
||||
popoverPresentationController.sourceView != nil ||
|
||||
popoverPresentationController.sourceRect != .zero ||
|
||||
popoverPresentationController.barButtonItem != nil
|
||||
)
|
||||
}
|
||||
viewController = alertController
|
||||
case .activityViewController(let activityViewController, let sourceView, let barButtonItem):
|
||||
activityViewController.popoverPresentationController?.sourceView = sourceView
|
||||
activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
|
||||
viewController = activityViewController
|
||||
case .settings(let viewModel):
|
||||
let _viewController = SettingsViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .editStatus(let viewModel):
|
||||
let composeViewController = ComposeViewController(viewModel: viewModel)
|
||||
viewController = composeViewController
|
||||
}
|
||||
|
||||
setupDependency(for: viewController as? NeedsDependency)
|
||||
|
|
|
@ -39,30 +39,31 @@ extension UserSection {
|
|||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .user(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: UserTableViewCell.ViewModel(value: .user(user),
|
||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
|
||||
),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.startAnimating()
|
||||
return cell
|
||||
case .bottomHeader(let text):
|
||||
case .user(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: UserTableViewCell.ViewModel(
|
||||
user: user,
|
||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
|
||||
),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.startAnimating()
|
||||
return cell
|
||||
case .bottomHeader(let text):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell
|
||||
cell.messageLabel.text = text
|
||||
return cell
|
||||
|
|
|
@ -26,7 +26,7 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func coordinateToHashtagScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
tag: Mastodon.Entity.Tag
|
||||
) async {
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(
|
||||
|
|
|
@ -33,7 +33,7 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func coordinateToProfileScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async {
|
||||
guard let user = user.object(in: provider.context.managedObjectContext) else {
|
||||
|
@ -127,227 +127,6 @@ extension DataSourceFacade {
|
|||
let barButtonItem: UIBarButtonItem?
|
||||
}
|
||||
|
||||
// @MainActor
|
||||
// static func createProfileActionMenu(
|
||||
// dependency: NeedsDependency,
|
||||
// user: ManagedObjectRecord<MastodonUser>
|
||||
// ) -> UIMenu {
|
||||
// var children: [UIMenuElement] = []
|
||||
// let name = mastodonUser.displayNameWithFallback
|
||||
//
|
||||
// if let shareUser = shareUser {
|
||||
// let shareAction = UIAction(
|
||||
// title: L10n.Common.Controls.Actions.shareUser(name),
|
||||
// image: UIImage(systemName: "square.and.arrow.up"),
|
||||
// identifier: nil,
|
||||
// discoverabilityTitle: nil,
|
||||
// attributes: [],
|
||||
// state: .off
|
||||
// ) { [weak provider, weak sourceView, weak barButtonItem] _ in
|
||||
// guard let provider = provider else { return }
|
||||
// let activityViewController = createActivityViewControllerForMastodonUser(mastodonUser: shareUser, dependency: provider)
|
||||
// provider.coordinator.present(
|
||||
// scene: .activityViewController(
|
||||
// activityViewController: activityViewController,
|
||||
// sourceView: sourceView,
|
||||
// barButtonItem: barButtonItem
|
||||
// ),
|
||||
// from: provider,
|
||||
// transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
// )
|
||||
// }
|
||||
// children.append(shareAction)
|
||||
// }
|
||||
//
|
||||
// if let shareStatus = shareStatus {
|
||||
// let shareAction = UIAction(
|
||||
// title: L10n.Common.Controls.Actions.sharePost,
|
||||
// image: UIImage(systemName: "square.and.arrow.up"),
|
||||
// identifier: nil,
|
||||
// discoverabilityTitle: nil,
|
||||
// attributes: [],
|
||||
// state: .off
|
||||
// ) { [weak provider, weak sourceView, weak barButtonItem] _ in
|
||||
// guard let provider = provider else { return }
|
||||
// let activityViewController = createActivityViewControllerForMastodonUser(status: shareStatus, dependency: provider)
|
||||
// provider.coordinator.present(
|
||||
// scene: .activityViewController(
|
||||
// activityViewController: activityViewController,
|
||||
// sourceView: sourceView,
|
||||
// barButtonItem: barButtonItem
|
||||
// ),
|
||||
// from: provider,
|
||||
// transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
// )
|
||||
// }
|
||||
// children.append(shareAction)
|
||||
// }
|
||||
//
|
||||
// if !isMyself {
|
||||
// // mute
|
||||
// let muteAction = UIAction(
|
||||
// title: isMuting ? L10n.Common.Controls.Friendship.unmuteUser(name) : L10n.Common.Controls.Friendship.mute,
|
||||
// image: isMuting ? UIImage(systemName: "speaker") : UIImage(systemName: "speaker.slash"),
|
||||
// discoverabilityTitle: isMuting ? nil : L10n.Common.Controls.Friendship.muteUser(name),
|
||||
// attributes: isMuting ? [] : .destructive,
|
||||
// state: .off
|
||||
// ) { [weak provider, weak cell] _ in
|
||||
// guard let provider = provider else { return }
|
||||
//
|
||||
// UserProviderFacade.toggleUserMuteRelationship(
|
||||
// provider: provider,
|
||||
// cell: cell
|
||||
// )
|
||||
// .sink { _ in
|
||||
// // do nothing
|
||||
// } receiveValue: { _ in
|
||||
// // do nothing
|
||||
// }
|
||||
// .store(in: &provider.context.disposeBag)
|
||||
// }
|
||||
// if isMuting {
|
||||
// children.append(muteAction)
|
||||
// } else {
|
||||
// let muteMenu = UIMenu(title: L10n.Common.Controls.Friendship.muteUser(name), image: UIImage(systemName: "speaker.slash"), options: [], children: [muteAction])
|
||||
// children.append(muteMenu)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if !isMyself {
|
||||
// // block
|
||||
// let blockAction = UIAction(
|
||||
// title: isBlocking ? L10n.Common.Controls.Friendship.unblockUser(name) : L10n.Common.Controls.Friendship.block,
|
||||
// image: isBlocking ? UIImage(systemName: "hand.raised.slash") : UIImage(systemName: "hand.raised"),
|
||||
// discoverabilityTitle: isBlocking ? nil : L10n.Common.Controls.Friendship.blockUser(name),
|
||||
// attributes: isBlocking ? [] : .destructive,
|
||||
// state: .off
|
||||
// ) { [weak provider, weak cell] _ in
|
||||
// guard let provider = provider else { return }
|
||||
//
|
||||
// UserProviderFacade.toggleUserBlockRelationship(
|
||||
// provider: provider,
|
||||
// cell: cell
|
||||
// )
|
||||
// .sink { _ in
|
||||
// // do nothing
|
||||
// } receiveValue: { _ in
|
||||
// // do nothing
|
||||
// }
|
||||
// .store(in: &provider.context.disposeBag)
|
||||
// }
|
||||
// if isBlocking {
|
||||
// children.append(blockAction)
|
||||
// } else {
|
||||
// let blockMenu = UIMenu(title: L10n.Common.Controls.Friendship.blockUser(name), image: UIImage(systemName: "hand.raised"), options: [], children: [blockAction])
|
||||
// children.append(blockMenu)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if !isMyself {
|
||||
// let reportAction = UIAction(
|
||||
// title: L10n.Common.Controls.Actions.reportUser(name),
|
||||
// image: UIImage(systemName: "flag"),
|
||||
// identifier: nil,
|
||||
// discoverabilityTitle: nil,
|
||||
// attributes: [],
|
||||
// state: .off
|
||||
// ) { [weak provider] _ in
|
||||
// guard let provider = provider else { return }
|
||||
// guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
// return
|
||||
// }
|
||||
// let viewModel = ReportViewModel(
|
||||
// context: provider.context,
|
||||
// domain: authenticationBox.domain,
|
||||
// user: mastodonUser,
|
||||
// status: nil
|
||||
// )
|
||||
// provider.coordinator.present(
|
||||
// scene: .report(viewModel: viewModel),
|
||||
// from: provider,
|
||||
// transition: .modal(animated: true, completion: nil)
|
||||
// )
|
||||
// }
|
||||
// children.append(reportAction)
|
||||
// }
|
||||
//
|
||||
// if !isInSameDomain {
|
||||
// if isDomainBlocking {
|
||||
// let unblockDomainAction = UIAction(
|
||||
// title: L10n.Common.Controls.Actions.unblockDomain(mastodonUser.domainFromAcct),
|
||||
// image: UIImage(systemName: "nosign"),
|
||||
// identifier: nil,
|
||||
// discoverabilityTitle: nil,
|
||||
// attributes: [],
|
||||
// state: .off
|
||||
// ) { [weak provider, weak cell] _ in
|
||||
// guard let provider = provider else { return }
|
||||
// provider.context.blockDomainService.unblockDomain(userProvider: provider, cell: cell)
|
||||
// }
|
||||
// children.append(unblockDomainAction)
|
||||
// } else {
|
||||
// let blockDomainAction = UIAction(
|
||||
// title: L10n.Common.Controls.Actions.blockDomain(mastodonUser.domainFromAcct),
|
||||
// image: UIImage(systemName: "nosign"),
|
||||
// identifier: nil,
|
||||
// discoverabilityTitle: nil,
|
||||
// attributes: [],
|
||||
// state: .off
|
||||
// ) { [weak provider, weak cell] _ in
|
||||
// guard let provider = provider else { return }
|
||||
//
|
||||
// let alertController = UIAlertController(title: L10n.Common.Alerts.BlockDomain.title(mastodonUser.domainFromAcct), message: nil, preferredStyle: .alert)
|
||||
// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in }
|
||||
// alertController.addAction(cancelAction)
|
||||
// let blockDomainAction = UIAlertAction(title: L10n.Common.Alerts.BlockDomain.blockEntireDomain, style: .destructive) { [weak provider, weak cell] _ in
|
||||
// guard let provider = provider else { return }
|
||||
// provider.context.blockDomainService.blockDomain(userProvider: provider, cell: cell)
|
||||
// }
|
||||
// alertController.addAction(blockDomainAction)
|
||||
// provider.present(alertController, animated: true, completion: nil)
|
||||
// }
|
||||
// children.append(blockDomainAction)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if let status = shareStatus, isMyself {
|
||||
// let deleteAction = UIAction(
|
||||
// title: L10n.Common.Controls.Actions.delete,
|
||||
// image: UIImage(systemName: "delete.left"),
|
||||
// identifier: nil,
|
||||
// discoverabilityTitle: nil,
|
||||
// attributes: [.destructive],
|
||||
// state: .off
|
||||
// ) { [weak provider] _ in
|
||||
// guard let provider = provider else { return }
|
||||
//
|
||||
// let alertController = UIAlertController(title: L10n.Common.Alerts.DeletePost.title, message: nil, preferredStyle: .alert)
|
||||
// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in }
|
||||
// alertController.addAction(cancelAction)
|
||||
// let deleteAction = UIAlertAction(title: L10n.Common.Alerts.DeletePost.delete, style: .destructive) { [weak provider] _ in
|
||||
// guard let provider = provider else { return }
|
||||
// guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
// provider.context.apiService.deleteStatus(
|
||||
// domain: activeMastodonAuthenticationBox.domain,
|
||||
// statusID: status.id,
|
||||
// authorizationBox: activeMastodonAuthenticationBox
|
||||
// )
|
||||
// .sink { _ in
|
||||
// // do nothing
|
||||
// } receiveValue: { _ in
|
||||
// // do nothing
|
||||
// }
|
||||
// .store(in: &provider.context.disposeBag)
|
||||
// }
|
||||
// alertController.addAction(deleteAction)
|
||||
// provider.present(alertController, animated: true, completion: nil)
|
||||
// }
|
||||
// children.append(deleteAction)
|
||||
// }
|
||||
//
|
||||
// return UIMenu(title: "", options: [], children: children)
|
||||
// }
|
||||
|
||||
static func createActivityViewController(
|
||||
dependency: NeedsDependency,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import UIKit
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func responseToCreateSearchHistory(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
item: DataSourceItem
|
||||
) async {
|
||||
switch item {
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
// Created by MainasuK on 2022-1-17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func coordinateToStatusThreadScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
target: StatusTarget,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async {
|
||||
|
@ -40,7 +40,7 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func coordinateToStatusThreadScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
root: StatusItem.Thread
|
||||
) async {
|
||||
let threadViewModel = ThreadViewModel(
|
||||
|
|
|
@ -352,10 +352,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
choices: [choice],
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choice) success")
|
||||
} catch {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll fail: \(error.localizedDescription)")
|
||||
|
||||
// restore voting state
|
||||
try await managedObjectContext.performChanges {
|
||||
guard
|
||||
|
@ -411,10 +408,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
choices: choices,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choices) success")
|
||||
} catch {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll fail: \(error.localizedDescription)")
|
||||
|
||||
// restore voting state
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let poll = poll.object(in: managedObjectContext) else { return }
|
||||
|
|
|
@ -15,7 +15,6 @@ import MastodonLocalization
|
|||
extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)")
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath)
|
||||
guard let item = await item(from: source) else {
|
||||
|
@ -77,7 +76,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableV
|
|||
contextMenuConfigurationForRowAt
|
||||
indexPath: IndexPath, point: CGPoint
|
||||
) -> UIContextMenuConfiguration? {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return nil }
|
||||
|
||||
|
@ -238,7 +236,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableV
|
|||
willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
|
||||
animator: UIContextMenuInteractionCommitAnimating
|
||||
) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return }
|
||||
guard let indexPath = configuration.indexPath, let index = configuration.index else { return }
|
||||
|
|
|
@ -44,7 +44,6 @@ extension DataSourceItem {
|
|||
}
|
||||
}
|
||||
|
||||
protocol DataSourceProvider: NeedsDependency & UIViewController {
|
||||
var logger: Logger { get }
|
||||
protocol DataSourceProvider: ViewControllerWithDependencies {
|
||||
func item(from source: DataSourceItem.Source) async -> DataSourceItem?
|
||||
}
|
||||
|
|
|
@ -41,13 +41,8 @@ extension HomeTimelineViewModel {
|
|||
.sink { [weak self] records in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): incoming \(records.count) objects")
|
||||
|
||||
Task { @MainActor in
|
||||
let start = CACurrentMediaTime()
|
||||
defer {
|
||||
let end = CACurrentMediaTime()
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cost \(end - start, format: .fixed(precision: 4))s to process \(records.count) feeds")
|
||||
}
|
||||
let oldSnapshot = diffableDataSource.snapshot()
|
||||
var newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, StatusItem> = {
|
||||
let newItems = records.map { record in
|
||||
|
@ -92,11 +87,8 @@ extension HomeTimelineViewModel {
|
|||
|
||||
let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers
|
||||
if !hasChanges && !self.hasPendingStatusEditReload {
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes")
|
||||
self.didLoadLatest.send()
|
||||
return
|
||||
} else {
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes")
|
||||
}
|
||||
|
||||
guard let difference = self.calculateReloadSnapshotDifference(
|
||||
|
@ -106,7 +98,6 @@ extension HomeTimelineViewModel {
|
|||
) else {
|
||||
self.updateSnapshotUsingReloadData(snapshot: newSnapshot)
|
||||
self.didLoadLatest.send()
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -116,7 +107,6 @@ extension HomeTimelineViewModel {
|
|||
contentOffset.y = tableView.contentOffset.y - difference.sourceDistanceToTableViewTopEdge
|
||||
tableView.setContentOffset(contentOffset, animated: false)
|
||||
self.didLoadLatest.send()
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot")
|
||||
self.hasPendingStatusEditReload = false
|
||||
} // end Task
|
||||
}
|
||||
|
|
|
@ -21,9 +21,6 @@ final class HeightFixedSearchBar: UISearchBar {
|
|||
}
|
||||
|
||||
final class SearchViewController: UIViewController, NeedsDependency {
|
||||
|
||||
let logger = Logger(subsystem: "SearchViewController", category: "ViewController")
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -37,16 +34,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
let titleViewContainer = UIView()
|
||||
let searchBar = HeightFixedSearchBar()
|
||||
|
||||
// let collectionView: UICollectionView = {
|
||||
// var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||
// configuration.backgroundColor = .clear
|
||||
// configuration.headerMode = .supplementary
|
||||
// let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
// collectionView.backgroundColor = .clear
|
||||
// return collectionView
|
||||
// }()
|
||||
|
||||
// value is the initial search text to set
|
||||
let searchBarTapPublisher = PassthroughSubject<String, Never>()
|
||||
|
||||
|
@ -62,11 +49,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
)
|
||||
return viewController
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchViewController {
|
||||
|
@ -85,30 +67,12 @@ extension SearchViewController {
|
|||
title = L10n.Scene.Search.title
|
||||
|
||||
setupSearchBar()
|
||||
|
||||
// collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.addSubview(collectionView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
// collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
// collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
// collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// collectionView.delegate = self
|
||||
// viewModel.setupDiffableDataSource(
|
||||
// collectionView: collectionView
|
||||
// )
|
||||
|
||||
guard let discoveryViewController = self.discoveryViewController else { return }
|
||||
|
||||
addChild(discoveryViewController)
|
||||
discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(discoveryViewController.view)
|
||||
discoveryViewController.view.pinToParent()
|
||||
|
||||
// discoveryViewController.view.isHidden = true
|
||||
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -171,7 +135,6 @@ extension SearchViewController {
|
|||
// MARK: - UISearchBarDelegate
|
||||
extension SearchViewController: UISearchBarDelegate {
|
||||
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
searchBarTapPublisher.send("")
|
||||
return false
|
||||
}
|
||||
|
@ -184,12 +147,8 @@ extension SearchViewController: UISearchBarDelegate {
|
|||
// MARK: - UISearchControllerDelegate
|
||||
extension SearchViewController: UISearchControllerDelegate {
|
||||
func willDismissSearchController(_ searchController: UISearchController) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
searchController.isActive = true
|
||||
}
|
||||
func didPresentSearchController(_ searchController: UISearchController) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ScrollViewContainer
|
||||
|
@ -201,23 +160,3 @@ extension SearchViewController: ScrollViewContainer {
|
|||
discoveryViewController?.scrollToTop(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate
|
||||
//extension SearchViewController: UICollectionViewDelegate {
|
||||
// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select item at: \(indexPath.debugDescription)")
|
||||
//
|
||||
// defer {
|
||||
// collectionView.deselectItem(at: indexPath, animated: true)
|
||||
// }
|
||||
//
|
||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
//
|
||||
// switch item {
|
||||
// case .trend(let hashtag):
|
||||
// let viewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name)
|
||||
// coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: self, transition: .show)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
//
|
||||
// SearchViewModel+Diffable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-18.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
|
||||
//extension SearchViewModel {
|
||||
//
|
||||
// func setupDiffableDataSource(
|
||||
// collectionView: UICollectionView
|
||||
// ) {
|
||||
// diffableDataSource = SearchSection.diffableDataSource(
|
||||
// collectionView: collectionView,
|
||||
// context: context
|
||||
// )
|
||||
//
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>()
|
||||
// snapshot.appendSections([.trend])
|
||||
// diffableDataSource?.apply(snapshot)
|
||||
//
|
||||
// $hashtags
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] hashtags in
|
||||
// guard let self = self else { return }
|
||||
// guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
//
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>()
|
||||
// snapshot.appendSections([.trend])
|
||||
//
|
||||
// let trendItems = hashtags.map { SearchItem.trend($0) }
|
||||
// snapshot.appendItems(trendItems, toSection: .trend)
|
||||
//
|
||||
// diffableDataSource.apply(snapshot)
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -31,32 +31,5 @@ final class SearchViewModel: NSObject {
|
|||
self.context = context
|
||||
self.authContext = authContext
|
||||
super.init()
|
||||
|
||||
// Publishers.CombineLatest(
|
||||
// context.authenticationService.activeMastodonAuthenticationBox,
|
||||
// viewDidAppeared
|
||||
// )
|
||||
// .compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in
|
||||
// return authenticationBox
|
||||
// }
|
||||
// .throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
|
||||
// .asyncMap { authenticationBox in
|
||||
// try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
|
||||
// }
|
||||
// .retry(3)
|
||||
// .map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
|
||||
// .catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) }
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] result in
|
||||
// guard let self = self else { return }
|
||||
// switch result {
|
||||
// case .success(let response):
|
||||
// self.hashtags = response.value
|
||||
// case .failure:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
|
||||
class SearchResultDefaultSectionTableViewCell: UITableViewCell {
|
||||
static let reuseIdentifier = "SearchResultDefaultSectionTableViewCell"
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
backgroundColor = .secondarySystemGroupedBackground
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func configure(item: SearchResultOverviewItem.DefaultSectionEntry) {
|
||||
var content = UIListContentConfiguration.cell()
|
||||
content.image = item.icon
|
||||
content.text = item.title
|
||||
content.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
contentConfiguration = content
|
||||
}
|
||||
|
||||
func configure(item: SearchResultOverviewItem.SuggestionSectionEntry) {
|
||||
var content = UIListContentConfiguration.cell()
|
||||
content.image = item.icon
|
||||
content.text = item.title
|
||||
content.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
contentConfiguration = content
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonUI
|
||||
|
||||
class SearchResultsProfileTableViewCell: UITableViewCell {
|
||||
static let reuseIdentifier = "SearchResultsProfileTableViewCell"
|
||||
|
||||
let condensedUserView: CondensedUserView
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
|
||||
condensedUserView = CondensedUserView(frame: .zero)
|
||||
condensedUserView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
contentView.addSubview(condensedUserView)
|
||||
condensedUserView.pinToParent()
|
||||
backgroundColor = .secondarySystemGroupedBackground
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
condensedUserView.prepareForReuse()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
protocol Coordinator {
|
||||
func start()
|
||||
}
|
||||
|
||||
class SearchResultOverviewCoordinator: Coordinator {
|
||||
|
||||
let overviewViewController: SearchResultsOverviewTableViewController
|
||||
let sceneCoordinator: SceneCoordinator
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
|
||||
var activeTask: Task<Void, Never>?
|
||||
|
||||
init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) {
|
||||
self.sceneCoordinator = sceneCoordinator
|
||||
self.context = appContext
|
||||
self.authContext = authContext
|
||||
|
||||
overviewViewController = SearchResultsOverviewTableViewController(appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator)
|
||||
}
|
||||
|
||||
func start() {
|
||||
overviewViewController.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControllerDelegate {
|
||||
@MainActor
|
||||
func searchForPosts(_ viewController: SearchResultsOverviewTableViewController, withSearchText searchText: String) {
|
||||
let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts, searchText: searchText)
|
||||
|
||||
sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show)
|
||||
}
|
||||
|
||||
func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag) {
|
||||
Task {
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: viewController,
|
||||
tag: tag
|
||||
)
|
||||
|
||||
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
|
||||
item: .hashtag(tag: .entity(tag)))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func searchForPeople(_ viewController: SearchResultsOverviewTableViewController, withName searchText: String) {
|
||||
let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people, searchText: searchText)
|
||||
|
||||
sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show)
|
||||
}
|
||||
|
||||
func goTo(_ viewController: SearchResultsOverviewTableViewController, urlString: String) {
|
||||
|
||||
let query = Mastodon.API.V2.Search.Query(
|
||||
q: urlString,
|
||||
type: .default,
|
||||
resolve: true
|
||||
)
|
||||
|
||||
let authContext = self.authContext
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
|
||||
Task {
|
||||
let searchResult = try await context.apiService.search(
|
||||
query: query,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
if let account = searchResult.accounts.first {
|
||||
showProfile(viewController, for: account)
|
||||
} else if let status = searchResult.statuses.first {
|
||||
|
||||
let status = try await managedObjectContext.perform {
|
||||
return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext(
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
entity: status,
|
||||
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user,
|
||||
statusCache: nil,
|
||||
userCache: nil,
|
||||
networkDate: Date()))
|
||||
}
|
||||
|
||||
guard let status else { return }
|
||||
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: viewController,
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status.asRecord
|
||||
)
|
||||
} else if let url = URL(string: urlString) {
|
||||
let prefixedURL: URL?
|
||||
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
|
||||
if components.scheme == nil {
|
||||
components.scheme = "https"
|
||||
}
|
||||
prefixedURL = components.url
|
||||
} else {
|
||||
prefixedURL = url
|
||||
}
|
||||
|
||||
guard let prefixedURL else { return }
|
||||
|
||||
await sceneCoordinator.present(scene: .safari(url: prefixedURL), transition: .safariPresent(animated: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) {
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
|
||||
Task {
|
||||
let user = try await managedObjectContext.perform {
|
||||
return Persistence.MastodonUser.fetch(in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: domain,
|
||||
entity: account,
|
||||
cache: nil,
|
||||
networkDate: Date()
|
||||
))
|
||||
}
|
||||
|
||||
if let user {
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: viewController,
|
||||
user: user.asRecord)
|
||||
|
||||
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
|
||||
item: .user(record: user.asRecord))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchForPerson(_ viewController: SearchResultsOverviewTableViewController, username: String, domain: String) {
|
||||
let acct = "\(username)@\(domain)"
|
||||
let query = Mastodon.API.V2.Search.Query(
|
||||
q: acct,
|
||||
type: .accounts,
|
||||
resolve: true
|
||||
)
|
||||
|
||||
Task {
|
||||
let searchResult = try await context.apiService.search(
|
||||
query: query,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
if let account = searchResult.accounts.first(where: { $0.acctWithDomainIfMissing(domain).lowercased() == acct.lowercased() }) {
|
||||
showProfile(viewController, for: account)
|
||||
} else {
|
||||
await MainActor.run {
|
||||
let alertTitle = L10n.Scene.Search.Searching.NoUser.title
|
||||
let alertMessage = L10n.Scene.Search.Searching.NoUser.message(username, domain)
|
||||
|
||||
let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
|
||||
alertController.addAction(okAction)
|
||||
sceneCoordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
import CoreDataStack
|
||||
|
||||
enum SearchResultOverviewSection: Hashable {
|
||||
case `default`
|
||||
case suggestions
|
||||
}
|
||||
|
||||
enum SearchResultOverviewItem: Hashable {
|
||||
case `default`(DefaultSectionEntry)
|
||||
case suggestion(SuggestionSectionEntry)
|
||||
|
||||
enum DefaultSectionEntry: Hashable {
|
||||
case showHashtag(hashtag: String)
|
||||
case posts(matching: String)
|
||||
case people(matching: String)
|
||||
case showProfile(username: String, domain: String)
|
||||
case openLink(String)
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .posts(let text):
|
||||
return L10n.Scene.Search.Searching.posts(text)
|
||||
case .people(let username):
|
||||
return L10n.Scene.Search.Searching.people(username)
|
||||
case .showProfile(let username, let instanceName):
|
||||
return L10n.Scene.Search.Searching.profile(username, instanceName)
|
||||
case .openLink(_):
|
||||
return L10n.Scene.Search.Searching.url
|
||||
case .showHashtag(let hashtag):
|
||||
return L10n.Scene.Search.Searching.hashtag(hashtag)
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
switch self {
|
||||
case .posts(_):
|
||||
return UIImage(systemName: "magnifyingglass")
|
||||
case .people(_):
|
||||
return UIImage(systemName: "person.2")
|
||||
case .showProfile(_, _):
|
||||
return UIImage(systemName: "person.crop.circle")
|
||||
case .openLink(_):
|
||||
return UIImage(systemName: "link")
|
||||
case .showHashtag(_):
|
||||
return UIImage(systemName: "number")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SuggestionSectionEntry: Hashable {
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case profile(user: Mastodon.Entity.Account)
|
||||
|
||||
var title: String? {
|
||||
if case let .hashtag(tag) = self {
|
||||
return tag.name
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
if case .hashtag(_) = self {
|
||||
return UIImage(systemName: "number")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
protocol SearchResultsOverviewTableViewControllerDelegate: AnyObject {
|
||||
func goTo(_ viewController: SearchResultsOverviewTableViewController, urlString: String)
|
||||
func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag)
|
||||
func searchForPosts(_ viewController: SearchResultsOverviewTableViewController, withSearchText searchText: String)
|
||||
func searchForPeople(_ viewController: SearchResultsOverviewTableViewController, withName searchText: String)
|
||||
func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account)
|
||||
func searchForPerson(_ viewController: SearchResultsOverviewTableViewController, username: String, domain: String)
|
||||
}
|
||||
|
||||
class SearchResultsOverviewTableViewController: UIViewController, NeedsDependency, AuthContextProvider {
|
||||
let authContext: AuthContext
|
||||
var context: AppContext!
|
||||
var coordinator: SceneCoordinator!
|
||||
|
||||
private let tableView: UITableView
|
||||
var dataSource: UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>?
|
||||
|
||||
weak var delegate: SearchResultsOverviewTableViewControllerDelegate?
|
||||
|
||||
var activeTask: Task<Void, Never>?
|
||||
|
||||
init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) {
|
||||
|
||||
self.authContext = authContext
|
||||
self.context = appContext
|
||||
self.coordinator = sceneCoordinator
|
||||
|
||||
tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.backgroundColor = .systemGroupedBackground
|
||||
tableView.separatorInset.left = 62
|
||||
tableView.register(SearchResultDefaultSectionTableViewCell.self, forCellReuseIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier)
|
||||
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCell.reuseIdentifier)
|
||||
tableView.register(HashtagTableViewCell.self, forCellReuseIdentifier: HashtagTableViewCell.reuseIdentifier)
|
||||
tableView.register(SearchResultsProfileTableViewCell.self, forCellReuseIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier)
|
||||
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
let dataSource = UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>(tableView: tableView) { tableView, indexPath, itemIdentifier in
|
||||
|
||||
switch itemIdentifier {
|
||||
case .default(let item):
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
|
||||
|
||||
cell.configure(item: item)
|
||||
|
||||
return cell
|
||||
|
||||
case .suggestion(let suggestion):
|
||||
switch suggestion {
|
||||
case .hashtag(let hashtag):
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
|
||||
|
||||
cell.configure(item: .hashtag(tag: hashtag))
|
||||
return cell
|
||||
|
||||
case .profile(let profile):
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultsProfileTableViewCell else { fatalError() }
|
||||
|
||||
cell.condensedUserView.configure(with: profile)
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tableView.dataSource = dataSource
|
||||
tableView.delegate = self
|
||||
self.dataSource = dataSource
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.pinToParent()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchResultOverviewSection, SearchResultOverviewItem>()
|
||||
snapshot.appendSections([.default, .suggestions])
|
||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func showStandardSearch(for searchText: String) {
|
||||
guard let dataSource else { return }
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .default))
|
||||
|
||||
if searchText.lowercased().starts(with: "https://") && (searchText.contains(" ") == false) {
|
||||
if URL(string: searchText)?.isValidURL() ?? false {
|
||||
snapshot.appendItems([.default(.openLink(searchText))], toSection: .default)
|
||||
}
|
||||
}
|
||||
|
||||
if searchText.starts(with: "#") && searchText.length > 1 {
|
||||
snapshot.appendItems([.default(.showHashtag(hashtag: searchText.replacingOccurrences(of: "#", with: "")))],
|
||||
toSection: .default)
|
||||
} else if searchText.length > 1, let hashtagRegex = try? NSRegularExpression(pattern: MastodonRegex.Search.hashtag, options: .caseInsensitive), hashtagRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.length-1)) != nil {
|
||||
snapshot.appendItems([.default(.showHashtag(hashtag: searchText.replacingOccurrences(of: "#", with: "")))],
|
||||
toSection: .default)
|
||||
}
|
||||
|
||||
if searchText.length > 1,
|
||||
let usernameRegex = try? NSRegularExpression(pattern: MastodonRegex.Search.username, options: .caseInsensitive),
|
||||
usernameRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.length-1)) != nil {
|
||||
let components = searchText.split(separator: "@")
|
||||
if components.count == 2 {
|
||||
let username = String(components[0]).replacingOccurrences(of: "@", with: "")
|
||||
|
||||
let domain = String(components[1])
|
||||
if domain.split(separator: ".").count >= 2 {
|
||||
snapshot.appendItems([.default(.showProfile(username: username, domain: domain))], toSection: .default)
|
||||
} else {
|
||||
snapshot.appendItems([.default(.showProfile(username: username, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default)
|
||||
}
|
||||
} else {
|
||||
snapshot.appendItems([.default(.showProfile(username: searchText, domain: authContext.mastodonAuthenticationBox.domain))], toSection: .default)
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.appendItems([.default(.posts(matching: searchText)),
|
||||
.default(.people(matching: searchText))], toSection: .default)
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func searchForSuggestions(for searchText: String) {
|
||||
|
||||
activeTask?.cancel()
|
||||
|
||||
guard let dataSource else { return }
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .suggestions))
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
guard searchText.isNotEmpty else { return }
|
||||
|
||||
let query = Mastodon.API.V2.Search.Query(
|
||||
q: searchText,
|
||||
type: .default,
|
||||
resolve: true
|
||||
)
|
||||
|
||||
let searchTask = Task {
|
||||
do {
|
||||
let searchResult = try await context.apiService.search(
|
||||
query: query,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
let firstThreeHashtags = searchResult.hashtags.prefix(3)
|
||||
let firstThreeUsers = searchResult.accounts.prefix(3)
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
|
||||
if firstThreeHashtags.isNotEmpty {
|
||||
snapshot.appendItems(firstThreeHashtags.map { .suggestion(.hashtag(tag: $0)) }, toSection: .suggestions )
|
||||
}
|
||||
|
||||
if firstThreeUsers.isNotEmpty {
|
||||
snapshot.appendItems(firstThreeUsers.map { .suggestion(.profile(user: $0)) }, toSection: .suggestions )
|
||||
}
|
||||
|
||||
guard Task.isCancelled == false else { return }
|
||||
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
} catch {
|
||||
// do nothing
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
activeTask = searchTask
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: UITableViewDelegate
|
||||
extension SearchResultsOverviewTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
guard let snapshot = dataSource?.snapshot() else { return }
|
||||
let section = snapshot.sectionIdentifiers[indexPath.section]
|
||||
let item = snapshot.itemIdentifiers(inSection: section)[indexPath.row]
|
||||
|
||||
switch item {
|
||||
case .default(let defaultSectionEntry):
|
||||
switch defaultSectionEntry {
|
||||
case .posts(let searchText):
|
||||
delegate?.searchForPosts(self, withSearchText: searchText)
|
||||
case .people(let searchText):
|
||||
delegate?.searchForPeople(self, withName: searchText)
|
||||
case .showProfile(let username, let domain):
|
||||
delegate?.searchForPerson(self, username: username, domain: domain)
|
||||
case .openLink(let urlString):
|
||||
delegate?.goTo(self, urlString: urlString)
|
||||
case .showHashtag(let hashtagText):
|
||||
let tag = Mastodon.Entity.Tag(name: hashtagText, url: "")
|
||||
delegate?.showPosts(self, tag: tag)
|
||||
}
|
||||
case .suggestion(let suggestionSectionEntry):
|
||||
switch suggestionSectionEntry {
|
||||
|
||||
case .hashtag(let tag):
|
||||
delegate?.showPosts(self, tag: tag)
|
||||
case .profile(let account):
|
||||
delegate?.showProfile(self, for: account)
|
||||
}
|
||||
}
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultsOverviewTableViewController: UserTableViewCellDelegate {}
|
|
@ -8,7 +8,6 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import Pageboy
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
@ -23,22 +22,20 @@ final class CustomSearchController: UISearchController {
|
|||
|
||||
// Fake search bar not works on iPad with UISplitViewController
|
||||
// check device and fallback to standard UISearchController
|
||||
final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||
|
||||
let logger = Logger(subsystem: "SearchDetail", category: "UI")
|
||||
final class SearchDetailViewController: UIViewController, NeedsDependency {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
let searchResultOverviewCoordinator: SearchResultOverviewCoordinator
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
||||
let isPhoneDevice: Bool = {
|
||||
return UIDevice.current.userInterfaceIdiom == .phone
|
||||
}()
|
||||
|
||||
var viewModel: SearchDetailViewModel!
|
||||
var viewControllers: [SearchResultViewController]!
|
||||
|
||||
let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||
let navigationBarBackgroundView = UIView()
|
||||
|
@ -73,9 +70,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
|||
searchController.searchBar.setShowsScope(true, animated: false)
|
||||
}
|
||||
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
|
||||
searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle }
|
||||
searchBar.sizeToFit()
|
||||
searchBar.scopeBarBackgroundImage = UIImage()
|
||||
return searchBar
|
||||
}()
|
||||
|
||||
|
@ -86,11 +81,30 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
|||
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
|
||||
return searchHistoryViewController
|
||||
}()
|
||||
}
|
||||
|
||||
extension SearchDetailViewController {
|
||||
private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = {
|
||||
return searchResultOverviewCoordinator.overviewViewController
|
||||
}()
|
||||
|
||||
//MARK: - init
|
||||
|
||||
init(appContext: AppContext, sceneCoordinator: SceneCoordinator, authContext: AuthContext) {
|
||||
self.context = appContext
|
||||
self.coordinator = sceneCoordinator
|
||||
|
||||
self.searchResultOverviewCoordinator = SearchResultOverviewCoordinator(appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
//MARK: - UIViewController
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
searchResultOverviewCoordinator.start()
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
|
||||
|
@ -119,91 +133,30 @@ extension SearchDetailViewController {
|
|||
searchHistoryViewController.view.pinToParent()
|
||||
}
|
||||
|
||||
transition = Transition(style: .fade, duration: 0.1)
|
||||
isScrollEnabled = false
|
||||
|
||||
viewControllers = viewModel.searchScopes.map { scope in
|
||||
let searchResultViewController = SearchResultViewController()
|
||||
searchResultViewController.context = context
|
||||
searchResultViewController.coordinator = coordinator
|
||||
searchResultViewController.viewModel = SearchResultViewModel(context: context, authContext: viewModel.authContext, searchScope: scope)
|
||||
|
||||
// bind searchText
|
||||
viewModel.searchText
|
||||
.assign(to: \.value, on: searchResultViewController.viewModel.searchText)
|
||||
.store(in: &searchResultViewController.disposeBag)
|
||||
|
||||
// bind navigationBarFrame
|
||||
viewModel.navigationBarFrame
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: searchResultViewController.viewModel.navigationBarFrame)
|
||||
.store(in: &searchResultViewController.disposeBag)
|
||||
return searchResultViewController
|
||||
addChild(searchResultsOverviewViewController)
|
||||
searchResultsOverviewViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(searchResultsOverviewViewController.view)
|
||||
searchResultsOverviewViewController.didMove(toParent: self)
|
||||
if isPhoneDevice {
|
||||
NSLayoutConstraint.activate([
|
||||
searchResultsOverviewViewController.view.topAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor),
|
||||
searchResultsOverviewViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
searchResultsOverviewViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
searchResultsOverviewViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
} else {
|
||||
searchResultsOverviewViewController.view.pinToParent()
|
||||
}
|
||||
|
||||
// set initial items from "all" search scope for non-appeared lists
|
||||
if let allSearchScopeViewController = viewControllers.first(where: { $0.viewModel.searchScope == .all }) {
|
||||
allSearchScopeViewController.viewModel.$items
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] items in
|
||||
guard let self = self else { return }
|
||||
guard self.currentViewController === allSearchScopeViewController else { return }
|
||||
for viewController in self.viewControllers where viewController != allSearchScopeViewController {
|
||||
// do not change appeared list
|
||||
guard !viewController.viewModel.viewDidAppear.value else { continue }
|
||||
// set initial items
|
||||
switch viewController.viewModel.searchScope {
|
||||
case .all:
|
||||
assertionFailure()
|
||||
break
|
||||
case .people:
|
||||
viewController.viewModel.userFetchedResultsController.userIDs = allSearchScopeViewController.viewModel.userFetchedResultsController.userIDs
|
||||
case .hashtags:
|
||||
viewController.viewModel.hashtags = allSearchScopeViewController.viewModel.hashtags
|
||||
case .posts:
|
||||
viewController.viewModel.statusFetchedResultsController.statusIDs = allSearchScopeViewController.viewModel.statusFetchedResultsController.statusIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &allSearchScopeViewController.disposeBag)
|
||||
}
|
||||
|
||||
dataSource = self
|
||||
delegate = self
|
||||
|
||||
// bind search bar scope
|
||||
viewModel.selectedSearchScope
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] searchScope in
|
||||
guard let self = self else { return }
|
||||
if let index = self.viewModel.searchScopes.firstIndex(of: searchScope) {
|
||||
self.searchBar.selectedScopeButtonIndex = index
|
||||
self.scrollToPage(.at(index: index), animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind search trigger
|
||||
viewModel.searchText
|
||||
.removeDuplicates()
|
||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { [weak self] searchText in
|
||||
guard let self = self else { return }
|
||||
guard let searchResultViewController = self.currentViewController as? SearchResultViewController else {
|
||||
return
|
||||
}
|
||||
self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): trigger search \(searchText)")
|
||||
searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind search history display
|
||||
viewModel.searchText
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] searchText in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.searchHistoryViewController.view.isHidden = !searchText.isEmpty
|
||||
self.searchResultsOverviewViewController.view.isHidden = searchText.isEmpty
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -253,7 +206,6 @@ extension SearchDetailViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchDetailViewController {
|
||||
|
@ -292,7 +244,6 @@ extension SearchDetailViewController {
|
|||
searchController.searchBar.sizeToFit()
|
||||
}
|
||||
|
||||
searchBar.text = viewModel.searchText.value
|
||||
searchBar.delegate = self
|
||||
}
|
||||
|
||||
|
@ -305,18 +256,19 @@ extension SearchDetailViewController {
|
|||
// MARK: - UISearchBarDelegate
|
||||
extension SearchDetailViewController: UISearchBarDelegate {
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||
viewModel.selectedSearchScope.value = viewModel.searchScopes[selectedScope]
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
viewModel.searchText.value = trimmedSearchText
|
||||
|
||||
searchResultsOverviewViewController.showStandardSearch(for: trimmedSearchText)
|
||||
searchResultsOverviewViewController.searchForSuggestions(for: trimmedSearchText)
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): searchTest \(searchText)")
|
||||
viewModel.searchText.value = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
// dismiss or pop
|
||||
if isModal {
|
||||
dismiss(animated: true, completion: nil)
|
||||
|
@ -324,77 +276,4 @@ extension SearchDetailViewController: UISearchBarDelegate {
|
|||
navigationController?.popViewController(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PageboyViewControllerDataSource
|
||||
extension SearchDetailViewController: PageboyViewControllerDataSource {
|
||||
|
||||
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
||||
return viewControllers.count
|
||||
}
|
||||
|
||||
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
||||
guard index < viewControllers.count else { return nil }
|
||||
return viewControllers[index]
|
||||
}
|
||||
|
||||
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
|
||||
return .first
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PageboyViewControllerDelegate
|
||||
extension SearchDetailViewController: PageboyViewControllerDelegate {
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
willScrollToPageAt index: PageboyViewController.PageIndex,
|
||||
direction: PageboyViewController.NavigationDirection,
|
||||
animated: Bool
|
||||
) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didScrollTo position: CGPoint,
|
||||
direction: PageboyViewController.NavigationDirection,
|
||||
animated: Bool
|
||||
) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didCancelScrollToPageAt index: PageboyViewController.PageIndex,
|
||||
returnToPageAt previousIndex: PageboyViewController.PageIndex
|
||||
) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didScrollToPageAt index: PageboyViewController.PageIndex,
|
||||
direction: PageboyViewController.NavigationDirection,
|
||||
animated: Bool
|
||||
) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): index \(index)")
|
||||
|
||||
let searchResultViewController = viewControllers[index]
|
||||
viewModel.selectedSearchScope.value = searchResultViewController.viewModel.searchScope
|
||||
|
||||
// trigger fetch
|
||||
searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
|
||||
}
|
||||
|
||||
|
||||
func pageboyViewController(
|
||||
_ pageboyViewController: PageboyViewController,
|
||||
didReloadWith currentViewController: UIViewController,
|
||||
currentPageIndex: PageboyViewController.PageIndex
|
||||
) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// Created by MainasuK Cirno on 2021-7-13.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import Combine
|
||||
|
@ -15,53 +14,37 @@ import MastodonAsset
|
|||
import MastodonLocalization
|
||||
|
||||
final class SearchDetailViewModel {
|
||||
|
||||
|
||||
// input
|
||||
let authContext: AuthContext
|
||||
var needsBecomeFirstResponder = false
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
let navigationBarFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||
|
||||
|
||||
// output
|
||||
let searchScopes = SearchScope.allCases
|
||||
let selectedSearchScope = CurrentValueSubject<SearchScope, Never>(.all)
|
||||
let searchText: CurrentValueSubject<String, Never>
|
||||
let searchActionPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
|
||||
init(authContext: AuthContext, initialSearchText: String = "") {
|
||||
self.authContext = authContext
|
||||
self.searchText = CurrentValueSubject(initialSearchText)
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchDetailViewModel {
|
||||
enum SearchScope: CaseIterable {
|
||||
case all
|
||||
case people
|
||||
case hashtags
|
||||
case posts
|
||||
|
||||
var segmentedControlTitle: String {
|
||||
switch self {
|
||||
case .all: return L10n.Scene.Search.Searching.Segment.all
|
||||
case .people: return L10n.Scene.Search.Searching.Segment.people
|
||||
case .hashtags: return L10n.Scene.Search.Searching.Segment.hashtags
|
||||
case .posts: return L10n.Scene.Search.Searching.Segment.posts
|
||||
}
|
||||
}
|
||||
|
||||
var searchType: Mastodon.API.V2.Search.SearchType {
|
||||
switch self {
|
||||
enum SearchScope: CaseIterable {
|
||||
case all
|
||||
case people
|
||||
case hashtags
|
||||
case posts
|
||||
|
||||
var searchType: Mastodon.API.V2.Search.SearchType {
|
||||
switch self {
|
||||
case .all: return .default
|
||||
case .people: return .accounts
|
||||
case .hashtags: return .hashtags
|
||||
case .posts: return .statuses
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,14 +11,12 @@ import MastodonAsset
|
|||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
protocol SearchHistorySectionHeaderCollectionReusableViewDelegate: AnyObject, UserViewDelegate {
|
||||
protocol SearchHistorySectionHeaderCollectionReusableViewDelegate: AnyObject {
|
||||
func searchHistorySectionHeaderCollectionReusableView(_ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView, clearButtonDidPressed button: UIButton)
|
||||
}
|
||||
|
||||
final class SearchHistorySectionHeaderCollectionReusableView: UICollectionReusableView {
|
||||
|
||||
let logger = Logger(subsystem: "SearchHistorySectionHeaderCollectionReusableView", category: "View")
|
||||
|
||||
|
||||
weak var delegate: SearchHistorySectionHeaderCollectionReusableViewDelegate?
|
||||
|
||||
let primaryLabel: UILabel = {
|
||||
|
@ -32,8 +30,9 @@ final class SearchHistorySectionHeaderCollectionReusableView: UICollectionReusab
|
|||
|
||||
let clearButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
|
||||
button.tintColor = Asset.Colors.Label.secondary.color
|
||||
|
||||
button.setTitle(L10n.Scene.Search.Searching.clearAll, for: .normal)
|
||||
button.tintColor = Asset.Colors.Brand.blurple.color
|
||||
button.accessibilityLabel = L10n.Scene.Search.Searching.clear
|
||||
|
||||
return button
|
||||
|
@ -49,9 +48,6 @@ final class SearchHistorySectionHeaderCollectionReusableView: UICollectionReusab
|
|||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchHistorySectionHeaderCollectionReusableView {
|
||||
private func _init() {
|
||||
primaryLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(primaryLabel)
|
||||
|
@ -74,11 +70,8 @@ extension SearchHistorySectionHeaderCollectionReusableView {
|
|||
|
||||
clearButton.addTarget(self, action: #selector(SearchHistorySectionHeaderCollectionReusableView.clearButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistorySectionHeaderCollectionReusableView {
|
||||
@objc private func clearButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
delegate?.searchHistorySectionHeaderCollectionReusableView(self, clearButtonDidPressed: sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
//
|
||||
// SearchHistoryUserCollectionViewCell+ViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonUI
|
||||
import Combine
|
||||
|
||||
extension SearchHistoryUserCollectionViewCell {
|
||||
final class ViewModel {
|
||||
let value: MastodonUser
|
||||
|
||||
let followedUsers: AnyPublisher<[String], Never>
|
||||
let blockedUsers: AnyPublisher<[String], Never>
|
||||
let followRequestedUsers: AnyPublisher<[String], Never>
|
||||
|
||||
init(value: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
|
||||
self.value = value
|
||||
self.followedUsers = followedUsers
|
||||
self.followRequestedUsers = followRequestedUsers
|
||||
self.blockedUsers = blockedUsers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryUserCollectionViewCell {
|
||||
func configure(
|
||||
me: MastodonUser?,
|
||||
viewModel: ViewModel,
|
||||
delegate: UserViewDelegate?
|
||||
) {
|
||||
let user = viewModel.value
|
||||
|
||||
userView.configure(user: user, delegate: delegate)
|
||||
|
||||
guard let me = me else {
|
||||
return userView.setButtonState(.none)
|
||||
}
|
||||
|
||||
if user == me {
|
||||
userView.setButtonState(.none)
|
||||
} else {
|
||||
userView.setButtonState(.loading)
|
||||
}
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
viewModel.followedUsers,
|
||||
viewModel.followRequestedUsers,
|
||||
viewModel.blockedUsers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] followed, requested, blocked in
|
||||
if blocked.contains(user.id) {
|
||||
self?.userView.setButtonState(.blocked)
|
||||
} else if followed.contains(user.id) {
|
||||
self?.userView.setButtonState(.unfollow)
|
||||
} else if requested.contains(user.id) {
|
||||
self?.userView.setButtonState(.pending)
|
||||
} else if user.locked {
|
||||
self?.userView.setButtonState(.request)
|
||||
} else if user != me {
|
||||
self?.userView.setButtonState(.follow)
|
||||
}
|
||||
}
|
||||
.store(in: &_disposeBag)
|
||||
|
||||
}
|
||||
}
|
|
@ -1,74 +1,45 @@
|
|||
//
|
||||
// SearchHistoryUserCollectionViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-20.
|
||||
//
|
||||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonCore
|
||||
|
||||
class SearchHistoryUserCollectionViewCell: UICollectionViewCell {
|
||||
static let reuseIdentifier = "SearchHistoryUserCollectionViewCell"
|
||||
|
||||
let condensedUserView: CondensedUserView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
condensedUserView = CondensedUserView(frame: .zero)
|
||||
condensedUserView.translatesAutoresizingMaskIntoConstraints = false
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(condensedUserView)
|
||||
condensedUserView.pinToParent()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
final class SearchHistoryUserCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
var _disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let userView = UserView()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
userView.prepareForReuse()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchHistoryUserCollectionViewCell {
|
||||
|
||||
private func _init() {
|
||||
ThemeService.shared.currentTheme
|
||||
.map { $0.secondarySystemGroupedBackgroundColor }
|
||||
.sink { [weak self] backgroundColor in
|
||||
guard let self = self else { return }
|
||||
self.backgroundColor = backgroundColor
|
||||
self.setNeedsUpdateConfiguration()
|
||||
}
|
||||
.store(in: &_disposeBag)
|
||||
|
||||
userView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(userView)
|
||||
NSLayoutConstraint.activate([
|
||||
userView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
userView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
contentView.trailingAnchor.constraint(equalTo: userView.trailingAnchor, constant: 16),
|
||||
userView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
userView.accessibilityTraits.insert(.button)
|
||||
condensedUserView.prepareForReuse()
|
||||
}
|
||||
|
||||
|
||||
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||
super.updateConfiguration(using: state)
|
||||
|
||||
|
||||
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
|
||||
backgroundConfiguration.backgroundColorTransformer = .init { _ in
|
||||
if state.isHighlighted || state.isSelected {
|
||||
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
|
||||
} else {
|
||||
return .secondarySystemGroupedBackground
|
||||
}
|
||||
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
|
||||
}
|
||||
|
||||
self.backgroundConfiguration = backgroundConfiguration
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
|
||||
enum SearchHistorySection: Hashable {
|
||||
case main
|
||||
|
@ -30,16 +31,7 @@ extension SearchHistorySection {
|
|||
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { cell, indexPath, item in
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = item.object(in: context.managedObjectContext) else { return }
|
||||
cell.configure(
|
||||
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user,
|
||||
viewModel: SearchHistoryUserCollectionViewCell.ViewModel(
|
||||
value: user,
|
||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
|
||||
),
|
||||
delegate: configuration.searchHistorySectionHeaderCollectionReusableViewDelegate
|
||||
)
|
||||
cell.condensedUserView.configure(with: user)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,6 +39,8 @@ extension SearchHistorySection {
|
|||
context.managedObjectContext.performAndWait {
|
||||
guard let hashtag = item.object(in: context.managedObjectContext) else { return }
|
||||
var contentConfiguration = cell.defaultContentConfiguration()
|
||||
contentConfiguration.image = UIImage(systemName: "magnifyingglass")
|
||||
contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
|
||||
contentConfiguration.text = "#" + hashtag.name
|
||||
cell.contentConfiguration = contentConfiguration
|
||||
}
|
||||
|
@ -54,13 +48,13 @@ extension SearchHistorySection {
|
|||
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
|
||||
backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in
|
||||
guard let state = cell?.configurationState else {
|
||||
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
|
||||
return .secondarySystemGroupedBackground
|
||||
}
|
||||
|
||||
if state.isHighlighted || state.isSelected {
|
||||
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
|
||||
}
|
||||
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
|
||||
return .secondarySystemGroupedBackground
|
||||
}
|
||||
cell.backgroundConfiguration = backgroundConfiguration
|
||||
}
|
||||
|
@ -78,13 +72,8 @@ extension SearchHistorySection {
|
|||
}
|
||||
}
|
||||
|
||||
let trendHeaderRegister = UICollectionView.SupplementaryRegistration<SearchHistorySectionHeaderCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { [weak dataSource] supplementaryView, elementKind, indexPath in
|
||||
let trendHeaderRegister = UICollectionView.SupplementaryRegistration<SearchHistorySectionHeaderCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
|
||||
supplementaryView.delegate = configuration.searchHistorySectionHeaderCollectionReusableViewDelegate
|
||||
|
||||
guard let _ = dataSource else { return }
|
||||
// let sections = dataSource.snapshot().sectionIdentifiers
|
||||
// guard indexPath.section < sections.count else { return }
|
||||
// let section = sections[indexPath.section]
|
||||
}
|
||||
|
||||
dataSource.supplementaryViewProvider = { (collectionView: UICollectionView, elementKind: String, indexPath: IndexPath) in
|
|
@ -14,8 +14,6 @@ import MastodonUI
|
|||
|
||||
final class SearchHistoryViewController: UIViewController, NeedsDependency {
|
||||
|
||||
let logger = Logger(subsystem: "SearchHistoryViewController", category: "ViewController")
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -24,6 +22,8 @@ final class SearchHistoryViewController: UIViewController, NeedsDependency {
|
|||
|
||||
let collectionView: UICollectionView = {
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||
configuration.separatorConfiguration.bottomSeparatorInsets.leading = 62
|
||||
configuration.separatorConfiguration.topSeparatorInsets.leading = 62
|
||||
configuration.backgroundColor = .clear
|
||||
configuration.headerMode = .supplementary
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
|
@ -68,8 +68,6 @@ extension SearchHistoryViewController {
|
|||
extension SearchHistoryViewController: UICollectionViewDelegate {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select item at: \(indexPath.debugDescription)")
|
||||
|
||||
defer {
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
}
|
||||
|
@ -116,14 +114,14 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa
|
|||
_ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView,
|
||||
clearButtonDidPressed button: UIButton
|
||||
) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
Task {
|
||||
try await DataSourceFacade.responseToDeleteSearchHistory(
|
||||
provider: self
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
button.isEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryViewController: UserTableViewCellDelegate {}
|
||||
|
|
|
@ -37,26 +37,25 @@ extension SearchHistoryViewModel {
|
|||
do {
|
||||
let managedObjectContext = self.context.managedObjectContext
|
||||
let items: [SearchHistoryItem] = try await managedObjectContext.perform {
|
||||
var users: [SearchHistoryItem] = []
|
||||
var hashtags: [SearchHistoryItem] = []
|
||||
|
||||
var items: [SearchHistoryItem] = []
|
||||
|
||||
for record in records {
|
||||
guard let searchHistory = record.object(in: managedObjectContext) else { continue }
|
||||
if let user = searchHistory.account {
|
||||
users.append(.user(.init(objectID: user.objectID)))
|
||||
items.append(.user(.init(objectID: user.objectID)))
|
||||
} else if let hashtag = searchHistory.hashtag {
|
||||
hashtags.append(.hashtag(.init(objectID: hashtag.objectID)))
|
||||
} else {
|
||||
continue
|
||||
items.append(.hashtag(.init(objectID: hashtag.objectID)))
|
||||
}
|
||||
}
|
||||
|
||||
return users + hashtags
|
||||
return items
|
||||
}
|
||||
|
||||
let mostRecentItems = Array(items.prefix(10))
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchHistorySection, SearchHistoryItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
await diffableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
snapshot.appendItems(mostRecentItems, toSection: .main)
|
||||
await diffableDataSource.apply(snapshot, animatingDifferences: true)
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
|
|
@ -32,71 +32,3 @@ final class SearchHistoryViewModel {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
//extension SearchHistoryViewModel {
|
||||
// func persistSearchHistory(for item: SearchHistoryItem) {
|
||||
// guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
// let property = SearchHistory.Property(domain: box.domain, userID: box.userID)
|
||||
//
|
||||
// switch item {
|
||||
// case .account(let objectID):
|
||||
// let managedObjectContext = context.backgroundManagedObjectContext
|
||||
// managedObjectContext.performChanges {
|
||||
// guard let user = try? managedObjectContext.existingObject(with: objectID) as? MastodonUser else { return }
|
||||
// if let searchHistory = user.findSearchHistory(domain: box.domain, userID: box.userID) {
|
||||
// searchHistory.update(updatedAt: Date())
|
||||
// } else {
|
||||
// SearchHistory.insert(into: managedObjectContext, property: property, account: user)
|
||||
// }
|
||||
// }
|
||||
// .sink { result in
|
||||
// switch result {
|
||||
// case .failure(let error):
|
||||
// assertionFailure(error.localizedDescription)
|
||||
// case .success:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// .store(in: &context.disposeBag)
|
||||
//
|
||||
// case .hashtag(let objectID):
|
||||
// let managedObjectContext = context.backgroundManagedObjectContext
|
||||
// managedObjectContext.performChanges {
|
||||
// guard let hashtag = try? managedObjectContext.existingObject(with: objectID) as? Tag else { return }
|
||||
// if let searchHistory = hashtag.findSearchHistory(domain: box.domain, userID: box.userID) {
|
||||
// searchHistory.update(updatedAt: Date())
|
||||
// } else {
|
||||
// _ = SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
|
||||
// }
|
||||
// }
|
||||
// .sink { result in
|
||||
// switch result {
|
||||
// case .failure(let error):
|
||||
// assertionFailure(error.localizedDescription)
|
||||
// case .success:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// .store(in: &context.disposeBag)
|
||||
//
|
||||
// case .status:
|
||||
// // FIXME:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func clearSearchHistory() {
|
||||
// let managedObjectContext = context.backgroundManagedObjectContext
|
||||
// managedObjectContext.performChanges {
|
||||
// let request = SearchHistory.sortedFetchRequest
|
||||
// let searchHistories = managedObjectContext.safeFetch(request)
|
||||
// for searchHistory in searchHistories {
|
||||
// managedObjectContext.delete(searchHistory)
|
||||
// }
|
||||
// }
|
||||
// .sink { result in
|
||||
// // do nothing
|
||||
// }
|
||||
// .store(in: &context.disposeBag)
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
//
|
||||
// SearchHistoryTableHeaderView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-14.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
protocol SearchHistoryTableHeaderViewDelegate: AnyObject {
|
||||
func searchHistoryTableHeaderView(_ searchHistoryTableHeaderView: SearchHistoryTableHeaderView, clearSearchHistoryButtonDidPressed button: UIButton)
|
||||
}
|
||||
|
||||
final class SearchHistoryTableHeaderView: UIView {
|
||||
|
||||
let logger = Logger(subsystem: "SearchHistory", category: "UI")
|
||||
|
||||
weak var delegate: SearchHistoryTableHeaderViewDelegate?
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let recentSearchesLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = L10n.Scene.Search.Searching.recentSearch
|
||||
return label
|
||||
}()
|
||||
|
||||
let clearSearchHistoryButton: HighlightDimmableButton = {
|
||||
let button = HighlightDimmableButton(type: .custom)
|
||||
button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
|
||||
button.setTitleColor(Asset.Colors.Brand.blurple.color, for: .normal)
|
||||
button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchHistoryTableHeaderView {
|
||||
private func _init() {
|
||||
preservesSuperviewLayoutMargins = true
|
||||
|
||||
recentSearchesLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(recentSearchesLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
recentSearchesLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
||||
recentSearchesLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
bottomAnchor.constraint(equalTo: recentSearchesLabel.bottomAnchor, constant: 16),
|
||||
])
|
||||
|
||||
clearSearchHistoryButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(clearSearchHistoryButton)
|
||||
NSLayoutConstraint.activate([
|
||||
clearSearchHistoryButton.centerYAnchor.constraint(equalTo: recentSearchesLabel.centerYAnchor),
|
||||
clearSearchHistoryButton.leadingAnchor.constraint(equalTo: recentSearchesLabel.trailingAnchor),
|
||||
clearSearchHistoryButton.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
])
|
||||
clearSearchHistoryButton.setContentHuggingPriority(.defaultHigh + 10, for: .horizontal)
|
||||
|
||||
clearSearchHistoryButton.addTarget(self, action: #selector(SearchHistoryTableHeaderView.clearSearchHistoryButtonDidPressed(_:)), for: .touchUpInside)
|
||||
|
||||
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] theme in
|
||||
guard let self = self else { return }
|
||||
self.setupBackgroundColor(theme: theme)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryTableHeaderView {
|
||||
@objc private func clearSearchHistoryButtonDidPressed(_ sender: UIButton) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
delegate?.searchHistoryTableHeaderView(self, clearSearchHistoryButtonDidPressed: sender)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryTableHeaderView {
|
||||
private func setupBackgroundColor(theme: Theme) {
|
||||
backgroundColor = theme.systemGroupedBackgroundColor
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ import UIKit
|
|||
import MetaTextKit
|
||||
|
||||
final class HashtagTableViewCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "HashtagTableViewCell"
|
||||
|
||||
let primaryLabel = MetaLabel(style: .statusName)
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@ enum SearchResultSection: Hashable {
|
|||
|
||||
extension SearchResultSection {
|
||||
|
||||
static let logger = Logger(subsystem: "SearchResultSection", category: "logic")
|
||||
|
||||
struct Configuration {
|
||||
let authContext: AuthContext
|
||||
weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate?
|
||||
|
@ -45,7 +43,7 @@ extension SearchResultSection {
|
|||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .user(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
|
@ -53,7 +51,7 @@ extension SearchResultSection {
|
|||
authContext: authContext,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: UserTableViewCell.ViewModel(value: .user(user),
|
||||
viewModel: UserTableViewCell.ViewModel(user: user,
|
||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()),
|
|
@ -40,7 +40,6 @@ extension SearchResultViewController: DataSourceProvider {
|
|||
|
||||
extension SearchResultViewController {
|
||||
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)")
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath)
|
||||
guard let item = await item(from: source) else {
|
||||
|
@ -72,6 +71,8 @@ extension SearchResultViewController {
|
|||
case .notification:
|
||||
assertionFailure()
|
||||
} // end switch
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
} // end Task
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -11,11 +11,10 @@ import Combine
|
|||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonAsset
|
||||
|
||||
final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
let logger = Logger(subsystem: "SearchResultViewController", category: "ViewController")
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -39,21 +38,13 @@ extension SearchResultViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] theme in
|
||||
guard let self = self else { return }
|
||||
self.setupBackgroundColor(theme: theme)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
view.backgroundColor = Asset.Theme.System.systemGroupedBackground.color
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
tableView.pinToParent()
|
||||
|
||||
tableView.delegate = self
|
||||
// tableView.prefetchDataSource = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
tableView: tableView,
|
||||
statusTableViewCellDelegate: self,
|
||||
|
@ -71,83 +62,14 @@ extension SearchResultViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// listen keyboard events and set content inset
|
||||
let keyboardEventPublishers = Publishers.CombineLatest3(
|
||||
KeyboardResponderService.shared.isShow,
|
||||
KeyboardResponderService.shared.state,
|
||||
KeyboardResponderService.shared.endFrame
|
||||
)
|
||||
Publishers.CombineLatest3(
|
||||
keyboardEventPublishers,
|
||||
viewModel.viewDidAppear,
|
||||
viewModel.didDataSourceUpdate
|
||||
)
|
||||
.sink(receiveValue: { [weak self] keyboardEvents, _, _ in
|
||||
guard let self = self else { return }
|
||||
let (isShow, state, endFrame) = keyboardEvents
|
||||
|
||||
// update keyboard background color
|
||||
guard isShow, state == .dock else {
|
||||
self.tableView.contentInset.bottom = 0
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = 0
|
||||
return
|
||||
}
|
||||
// isShow AND dock state
|
||||
|
||||
// adjust inset for tableView
|
||||
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
|
||||
let padding = contentFrame.maxY - endFrame.minY
|
||||
guard padding > 0 else {
|
||||
self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom
|
||||
return
|
||||
}
|
||||
|
||||
self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
//
|
||||
// works for already onscreen page
|
||||
viewModel.navigationBarFrame
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] frame in
|
||||
guard let self = self else { return }
|
||||
guard self.viewModel.viewDidAppear.value else { return }
|
||||
self.tableView.contentInset.top = frame.height
|
||||
self.tableView.verticalScrollIndicatorInsets.top = frame.height
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// works for appearing page
|
||||
if !viewModel.viewDidAppear.value {
|
||||
tableView.contentInset.top = viewModel.navigationBarFrame.value.height
|
||||
tableView.contentOffset.y = -viewModel.navigationBarFrame.value.height
|
||||
}
|
||||
|
||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||
title = viewModel.searchText
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
viewModel.viewDidAppear.value = true
|
||||
viewModel.stateMachine.enter(SearchResultViewModel.State.Initial.self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchResultViewController {
|
||||
private func setupBackgroundColor(theme: Theme) {
|
||||
view.backgroundColor = theme.systemGroupedBackgroundColor
|
||||
// tableView.backgroundColor = theme.systemBackgroundColor
|
||||
// searchHeader.backgroundColor = theme.systemGroupedBackgroundColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
|
@ -180,81 +102,9 @@ extension SearchResultViewController: UITableViewDelegate, AutoGenerateTableView
|
|||
func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
|
||||
}
|
||||
|
||||
// sourcery:end
|
||||
|
||||
// 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) {
|
||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
//
|
||||
// viewModel.persistSearchHistory(for: item)
|
||||
//
|
||||
// switch item {
|
||||
// case .account(let account):
|
||||
// let profileViewModel = RemoteProfileViewModel(context: context, userID: account.id)
|
||||
// coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show)
|
||||
// case .hashtag(let hashtag):
|
||||
// let hashtagViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name)
|
||||
// coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: self, transition: .show)
|
||||
// case .status:
|
||||
// aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
// case .bottomLoader:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
// aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
// aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
// aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
// aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
//extension SearchResultViewController: UITableViewDataSourcePrefetching {
|
||||
// func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
// aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
// aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
//extension SearchResultViewController: AVPlayerViewControllerDelegate {
|
||||
// func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
// handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
// }
|
||||
//
|
||||
// func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
// handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
extension SearchResultViewController: StatusTableViewCellDelegate { }
|
||||
|
||||
|
|
|
@ -65,8 +65,7 @@ extension SearchResultViewModel {
|
|||
if let currentState = self.stateMachine.currentState {
|
||||
switch currentState {
|
||||
case is State.Loading,
|
||||
is State.Fail,
|
||||
is State.Idle:
|
||||
is State.Fail:
|
||||
let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: false)
|
||||
snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main)
|
||||
case is State.NoMore:
|
||||
|
@ -74,6 +73,9 @@ extension SearchResultViewModel {
|
|||
let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: true)
|
||||
snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main)
|
||||
}
|
||||
case is State.Idle:
|
||||
// do nothing
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// Created by MainasuK Cirno on 2021-7-14.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
@ -14,8 +13,6 @@ import MastodonCore
|
|||
extension SearchResultViewModel {
|
||||
class State: GKState {
|
||||
|
||||
let logger = Logger(subsystem: "SearchResultViewModel.State", category: "StateMachine")
|
||||
|
||||
let id = UUID()
|
||||
|
||||
weak var viewModel: SearchResultViewModel?
|
||||
|
@ -24,46 +21,37 @@ extension SearchResultViewModel {
|
|||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
let from = previousState.flatMap { String(describing: $0) } ?? "nil"
|
||||
let to = String(describing: self)
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func enter(state: State.Type) {
|
||||
public func enter(state: State.Type) {
|
||||
stateMachine?.enter(state)
|
||||
}
|
||||
|
||||
deinit {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultViewModel.State {
|
||||
class Initial: SearchResultViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
guard let viewModel = viewModel else { return false }
|
||||
return stateClass == Loading.self && !viewModel.searchText.value.isEmpty
|
||||
return stateClass == Loading.self
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
guard let viewModel else { return }
|
||||
|
||||
viewModel.items = [.bottomLoader(attribute: .init(isEmptyResult: false))]
|
||||
}
|
||||
}
|
||||
|
||||
class Loading: SearchResultViewModel.State {
|
||||
|
||||
var previousSearchText = ""
|
||||
var offset: Int? = nil
|
||||
var latestLoadingToken = UUID()
|
||||
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
guard let viewModel = self.viewModel else { return false }
|
||||
switch stateClass {
|
||||
case is Fail.Type, is Idle.Type, is NoMore.Type:
|
||||
return true
|
||||
case is Loading.Type:
|
||||
return viewModel.searchText.value != previousSearchText
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -71,12 +59,11 @@ extension SearchResultViewModel.State {
|
|||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
guard let viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
let searchText = viewModel.searchText.value
|
||||
let searchType = viewModel.searchScope.searchType
|
||||
|
||||
if previousState is NoMore && previousSearchText == searchText {
|
||||
if previousState is NoMore {
|
||||
// same searchText from NoMore
|
||||
// break the loading and resume NoMore state
|
||||
stateMachine.enter(NoMore.self)
|
||||
|
@ -86,17 +73,12 @@ extension SearchResultViewModel.State {
|
|||
// viewModel.items.value = viewModel.items.value
|
||||
}
|
||||
|
||||
guard !searchText.isEmpty else {
|
||||
guard viewModel.searchText.isEmpty == false else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
if searchText != previousSearchText {
|
||||
previousSearchText = searchText
|
||||
offset = nil
|
||||
} else {
|
||||
offset = viewModel.items.count
|
||||
}
|
||||
offset = viewModel.items.count
|
||||
|
||||
// not set offset for all case
|
||||
// and assert other cases the items are all the same type elements
|
||||
|
@ -108,7 +90,7 @@ extension SearchResultViewModel.State {
|
|||
}()
|
||||
|
||||
let query = Mastodon.API.V2.Search.Query(
|
||||
q: searchText,
|
||||
q: viewModel.searchText,
|
||||
type: searchType,
|
||||
accountID: nil,
|
||||
maxID: nil,
|
||||
|
@ -130,8 +112,6 @@ extension SearchResultViewModel.State {
|
|||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
// discard result when search text is outdated
|
||||
guard searchText == self.previousSearchText else { return }
|
||||
// discard result when request not the latest one
|
||||
guard id == self.latestLoadingToken else { return }
|
||||
// discard result when state is not Loading
|
||||
|
@ -165,7 +145,6 @@ extension SearchResultViewModel.State {
|
|||
viewModel.hashtags = hashtags
|
||||
|
||||
} catch {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): search \(searchText) fail: \(error.localizedDescription)")
|
||||
await enter(state: Fail.self)
|
||||
}
|
||||
} // end Task
|
||||
|
|
|
@ -20,14 +20,13 @@ final class SearchResultViewModel {
|
|||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let searchScope: SearchDetailViewModel.SearchScope
|
||||
let searchText = CurrentValueSubject<String, Never>("")
|
||||
let searchScope: SearchScope
|
||||
let searchText: String
|
||||
@Published var hashtags: [Mastodon.Entity.Tag] = []
|
||||
let userFetchedResultsController: UserFetchedResultsController
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
|
||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
var navigationBarFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||
|
||||
|
@ -43,15 +42,16 @@ final class SearchResultViewModel {
|
|||
State.Idle(viewModel: self),
|
||||
State.NoMore(viewModel: self),
|
||||
])
|
||||
stateMachine.enter(State.Initial.self)
|
||||
return stateMachine
|
||||
}()
|
||||
let didDataSourceUpdate = PassthroughSubject<Void, Never>()
|
||||
|
||||
init(context: AppContext, authContext: AuthContext, searchScope: SearchDetailViewModel.SearchScope) {
|
||||
init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all, searchText: String) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.searchScope = searchScope
|
||||
self.searchText = searchText
|
||||
|
||||
self.userFetchedResultsController = UserFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
|
@ -62,139 +62,5 @@ final class SearchResultViewModel {
|
|||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
additionalTweetPredicate: nil
|
||||
)
|
||||
|
||||
// Publishers.CombineLatest(
|
||||
// items,
|
||||
// statusFetchedResultsController.objectIDs.removeDuplicates()
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] items, statusObjectIDs in
|
||||
// guard let self = self else { return }
|
||||
// guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
//
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<SearchResultSection, SearchResultItem>()
|
||||
// snapshot.appendSections([.main])
|
||||
//
|
||||
// // append account & hashtag items
|
||||
//
|
||||
// var items = items
|
||||
// if self.searchScope == .all {
|
||||
// // all search scope not paging. it's safe sort on whole dataset
|
||||
// items.sort(by: { ($0.sortKey ?? "") < ($1.sortKey ?? "")})
|
||||
// }
|
||||
// snapshot.appendItems(items, toSection: .main)
|
||||
//
|
||||
// var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
||||
// let oldSnapshot = diffableDataSource.snapshot()
|
||||
// for item in oldSnapshot.itemIdentifiers {
|
||||
// guard case let .status(objectID, attribute) = item else { continue }
|
||||
// oldSnapshotAttributeDict[objectID] = attribute
|
||||
// }
|
||||
//
|
||||
// // append statuses
|
||||
// var statusItems: [SearchResultItem] = []
|
||||
// for objectID in statusObjectIDs {
|
||||
// let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute()
|
||||
// statusItems.append(.status(statusObjectID: objectID, attribute: attribute))
|
||||
// }
|
||||
// snapshot.appendItems(statusItems, toSection: .main)
|
||||
//
|
||||
// if let currentState = self.stateMachine.currentState {
|
||||
// switch currentState {
|
||||
// case is State.Loading, is State.Fail, is State.Idle:
|
||||
// let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: false)
|
||||
// snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main)
|
||||
// case is State.Fail:
|
||||
// break
|
||||
// case is State.NoMore:
|
||||
// if snapshot.itemIdentifiers.isEmpty {
|
||||
// let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: true)
|
||||
// snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main)
|
||||
// }
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// diffableDataSource.defaultRowAnimation = .fade
|
||||
// diffableDataSource.apply(snapshot, animatingDifferences: true) { [weak self] in
|
||||
// guard let self = self else { return }
|
||||
// self.didDataSourceUpdate.send()
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchResultViewModel {
|
||||
func persistSearchHistory(for item: SearchResultItem) {
|
||||
fatalError()
|
||||
// guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
// let property = SearchHistory.Property(domain: box.domain, userID: box.userID)
|
||||
// let domain = box.domain
|
||||
//
|
||||
// switch item {
|
||||
// case .account(let entity):
|
||||
// let managedObjectContext = context.backgroundManagedObjectContext
|
||||
// managedObjectContext.performChanges {
|
||||
// let (user, _) = APIService.CoreData.createOrMergeMastodonUser(
|
||||
// into: managedObjectContext,
|
||||
// for: nil,
|
||||
// in: domain,
|
||||
// entity: entity,
|
||||
// userCache: nil,
|
||||
// networkDate: Date(),
|
||||
// log: OSLog.api
|
||||
// )
|
||||
// if let searchHistory = user.findSearchHistory(domain: box.domain, userID: box.userID) {
|
||||
// searchHistory.update(updatedAt: Date())
|
||||
// } else {
|
||||
// SearchHistory.insert(into: managedObjectContext, property: property, account: user)
|
||||
// }
|
||||
// }
|
||||
// .sink { result in
|
||||
// switch result {
|
||||
// case .failure(let error):
|
||||
// assertionFailure(error.localizedDescription)
|
||||
// case .success:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// .store(in: &context.disposeBag)
|
||||
//
|
||||
// case .hashtag(let entity):
|
||||
// let managedObjectContext = context.backgroundManagedObjectContext
|
||||
// var tag: Tag?
|
||||
// managedObjectContext.performChanges {
|
||||
// let (hashtag, _) = APIService.CoreData.createOrMergeTag(
|
||||
// into: managedObjectContext,
|
||||
// entity: entity
|
||||
// )
|
||||
// tag = hashtag
|
||||
// if let searchHistory = hashtag.findSearchHistory(domain: box.domain, userID: box.userID) {
|
||||
// searchHistory.update(updatedAt: Date())
|
||||
// } else {
|
||||
// _ = SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
|
||||
// }
|
||||
// }
|
||||
// .sink { result in
|
||||
// switch result {
|
||||
// case .failure(let error):
|
||||
// assertionFailure(error.localizedDescription)
|
||||
// case .success:
|
||||
// print(tag?.searchHistories)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// .store(in: &context.disposeBag)
|
||||
//
|
||||
// case .status:
|
||||
// // FIXME:
|
||||
// break
|
||||
// case .bottomLoader:
|
||||
// break
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import MastodonLocalization
|
|||
import MastodonUI
|
||||
|
||||
final class StatusTableViewCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "StatusTableViewCell"
|
||||
|
||||
static let marginForRegularHorizontalSizeClass: CGFloat = 64
|
||||
|
||||
|
|
|
@ -12,72 +12,65 @@ import Combine
|
|||
|
||||
extension UserTableViewCell {
|
||||
final class ViewModel {
|
||||
let value: Value
|
||||
let user: MastodonUser
|
||||
|
||||
let followedUsers: AnyPublisher<[String], Never>
|
||||
let blockedUsers: AnyPublisher<[String], Never>
|
||||
let followRequestedUsers: AnyPublisher<[String], Never>
|
||||
|
||||
init(value: Value, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
|
||||
self.value = value
|
||||
init(user: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
|
||||
self.user = user
|
||||
self.followedUsers = followedUsers
|
||||
self.followRequestedUsers = followRequestedUsers
|
||||
self.blockedUsers = blockedUsers
|
||||
}
|
||||
|
||||
enum Value {
|
||||
case user(MastodonUser)
|
||||
// case status(Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UserTableViewCell {
|
||||
|
||||
func configure(
|
||||
me: MastodonUser?,
|
||||
me: MastodonUser? = nil,
|
||||
tableView: UITableView,
|
||||
viewModel: ViewModel,
|
||||
delegate: UserTableViewCellDelegate?
|
||||
) {
|
||||
switch viewModel.value {
|
||||
case .user(let user):
|
||||
userView.configure(user: user, delegate: delegate)
|
||||
|
||||
guard let me = me else {
|
||||
return userView.setButtonState(.none)
|
||||
}
|
||||
|
||||
if user == me {
|
||||
userView.setButtonState(.none)
|
||||
} else {
|
||||
userView.setButtonState(.loading)
|
||||
}
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
viewModel.followedUsers,
|
||||
viewModel.followRequestedUsers,
|
||||
viewModel.blockedUsers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] followed, requested, blocked in
|
||||
if blocked.contains(user.id) {
|
||||
self?.userView.setButtonState(.blocked)
|
||||
} else if followed.contains(user.id) {
|
||||
self?.userView.setButtonState(.unfollow)
|
||||
} else if requested.contains(user.id) {
|
||||
self?.userView.setButtonState(.pending)
|
||||
} else if user.locked {
|
||||
self?.userView.setButtonState(.request)
|
||||
} else if user != me {
|
||||
self?.userView.setButtonState(.follow)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
userView.configure(user: viewModel.user, delegate: delegate)
|
||||
|
||||
guard let me = me else {
|
||||
return userView.setButtonState(.none)
|
||||
}
|
||||
|
||||
self.delegate = delegate
|
||||
|
||||
if viewModel.user == me {
|
||||
userView.setButtonState(.none)
|
||||
} else {
|
||||
userView.setButtonState(.loading)
|
||||
}
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
viewModel.followedUsers,
|
||||
viewModel.followRequestedUsers,
|
||||
viewModel.blockedUsers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] followed, requested, blocked in
|
||||
if viewModel.user == me {
|
||||
self?.userView.setButtonState(.none)
|
||||
} else if blocked.contains(viewModel.user.id) {
|
||||
self?.userView.setButtonState(.blocked)
|
||||
} else if followed.contains(viewModel.user.id) {
|
||||
self?.userView.setButtonState(.unfollow)
|
||||
} else if requested.contains(viewModel.user.id) {
|
||||
self?.userView.setButtonState(.pending)
|
||||
} else if viewModel.user.locked {
|
||||
self?.userView.setButtonState(.request)
|
||||
} else if viewModel.user != me {
|
||||
self?.userView.setButtonState(.follow)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ import MastodonSDK
|
|||
protocol UserTableViewCellDelegate: UserViewDelegate, AnyObject { }
|
||||
|
||||
final class UserTableViewCell: UITableViewCell {
|
||||
|
||||
|
||||
static let reuseIdentifier = "UserTableViewCell"
|
||||
weak var delegate: UserTableViewCellDelegate?
|
||||
|
||||
let userView = UserView()
|
||||
|
|
|
@ -548,3 +548,10 @@ extension MastodonUser: AutoUpdatableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
public var verifiedLink: MastodonField? {
|
||||
let firstVerified = fields.first(where: { $0.verifiedAt != nil })
|
||||
return firstVerified
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,8 +90,6 @@ extension StatusFetchedResultsController {
|
|||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
let indexes = statusIDs
|
||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||
|
||||
|
|
|
@ -97,7 +97,6 @@ extension UserFetchedResultsController {
|
|||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
let indexes = userIDs
|
||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||
|
|
|
@ -230,12 +230,12 @@ extension APIService {
|
|||
var result: MastodonUser?
|
||||
try await managedObjectContext.perform {
|
||||
result = Persistence.MastodonUser.fetch(in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: domain,
|
||||
entity: response.value,
|
||||
cache: nil,
|
||||
networkDate: response.networkDate
|
||||
))
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: domain,
|
||||
entity: response.value,
|
||||
cache: nil,
|
||||
networkDate: response.networkDate
|
||||
))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1277,21 +1277,39 @@ public enum L10n {
|
|||
public enum Searching {
|
||||
/// Clear
|
||||
public static let clear = L10n.tr("Localizable", "Scene.Search.Searching.Clear", fallback: "Clear")
|
||||
/// Clear all
|
||||
public static let clearAll = L10n.tr("Localizable", "Scene.Search.Searching.ClearAll", fallback: "Clear all")
|
||||
/// Go to #%@
|
||||
public static func hashtag(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Search.Searching.Hashtag", String(describing: p1), fallback: "Go to #%@")
|
||||
}
|
||||
/// People matching "%@"
|
||||
public static func people(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Search.Searching.People", String(describing: p1), fallback: "People matching \"%@\"")
|
||||
}
|
||||
/// Posts matching "%@"
|
||||
public static func posts(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Search.Searching.Posts", String(describing: p1), fallback: "Posts matching \"%@\"")
|
||||
}
|
||||
/// Go to @%@@%@
|
||||
public static func profile(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Search.Searching.Profile", String(describing: p1), String(describing: p2), fallback: "Go to @%@@%@")
|
||||
}
|
||||
/// Recent searches
|
||||
public static let recentSearch = L10n.tr("Localizable", "Scene.Search.Searching.RecentSearch", fallback: "Recent searches")
|
||||
/// Open URL in Mastodon
|
||||
public static let url = L10n.tr("Localizable", "Scene.Search.Searching.Url", fallback: "Open URL in Mastodon")
|
||||
public enum EmptyState {
|
||||
/// No results
|
||||
public static let noResults = L10n.tr("Localizable", "Scene.Search.Searching.EmptyState.NoResults", fallback: "No results")
|
||||
}
|
||||
public enum Segment {
|
||||
/// All
|
||||
public static let all = L10n.tr("Localizable", "Scene.Search.Searching.Segment.All", fallback: "All")
|
||||
/// Hashtags
|
||||
public static let hashtags = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Hashtags", fallback: "Hashtags")
|
||||
/// People
|
||||
public static let people = L10n.tr("Localizable", "Scene.Search.Searching.Segment.People", fallback: "People")
|
||||
/// Posts
|
||||
public static let posts = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Posts", fallback: "Posts")
|
||||
public enum NoUser {
|
||||
/// There's no Useraccount "%@" on %@
|
||||
public static func message(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Search.Searching.NoUser.Message", String(describing: p1), String(describing: p2), fallback: "There's no Useraccount \"%@\" on %@")
|
||||
}
|
||||
/// No User Account Found
|
||||
public static let title = L10n.tr("Localizable", "Scene.Search.Searching.NoUser.Title", fallback: "No User Account Found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -440,12 +440,18 @@ uploaded to Mastodon.";
|
|||
"Scene.Search.SearchBar.Cancel" = "Cancel";
|
||||
"Scene.Search.SearchBar.Placeholder" = "Search hashtags and users";
|
||||
"Scene.Search.Searching.Clear" = "Clear";
|
||||
"Scene.Search.Searching.ClearAll" = "Clear all";
|
||||
"Scene.Search.Searching.EmptyState.NoResults" = "No results";
|
||||
"Scene.Search.Searching.RecentSearch" = "Recent searches";
|
||||
"Scene.Search.Searching.Segment.All" = "All";
|
||||
"Scene.Search.Searching.Segment.Hashtags" = "Hashtags";
|
||||
"Scene.Search.Searching.Segment.People" = "People";
|
||||
"Scene.Search.Searching.Segment.Posts" = "Posts";
|
||||
"Scene.Search.Searching.Posts" = "Posts matching \"%@\"";
|
||||
"Scene.Search.Searching.People" = "People matching \"%@\"";
|
||||
"Scene.Search.Searching.Profile" = "Go to @%@@%@";
|
||||
"Scene.Search.Searching.Hashtag" = "Go to #%@";
|
||||
"Scene.Search.Searching.Url" = "Open URL in Mastodon";
|
||||
|
||||
"Scene.Search.Searching.NoUser.Title" = "No User Account Found";
|
||||
"Scene.Search.Searching.NoUser.Message" = "There's no Useraccount \"%@\" on %@";
|
||||
|
||||
"Scene.Search.Title" = "Search";
|
||||
"Scene.ServerPicker.Button.Category.Academia" = "academia";
|
||||
"Scene.ServerPicker.Button.Category.Activism" = "activism";
|
||||
|
@ -554,4 +560,4 @@ uploaded to Mastodon.";
|
|||
"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
||||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
|
|
|
@ -208,7 +208,6 @@ extension Mastodon.API {
|
|||
return try Mastodon.API.decoder.decode(type, from: data)
|
||||
} catch let decodeError {
|
||||
#if DEBUG
|
||||
os_log(.info, "%{public}s[%{public}ld], %{public}s: decode fail. content %s", ((#file as NSString).lastPathComponent), #line, #function, String(data: data, encoding: .utf8) ?? "<nil>")
|
||||
debugPrint(decodeError)
|
||||
#endif
|
||||
|
||||
|
|
|
@ -92,3 +92,10 @@ extension Mastodon.Entity.Account {
|
|||
return acct
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Account {
|
||||
public var verifiedLink: Mastodon.Entity.Field? {
|
||||
let firstVerified = fields?.first(where: { $0.verifiedAt != nil })
|
||||
return firstVerified
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/history/)
|
||||
public struct History: Codable, Sendable {
|
||||
public struct History: Hashable, Codable, Sendable {
|
||||
/// UNIX timestamp on midnight of the given day
|
||||
public let day: Date
|
||||
public let uses: String
|
||||
|
|
|
@ -25,6 +25,13 @@ extension Mastodon.Entity {
|
|||
public let history: [History]?
|
||||
public let following: Bool?
|
||||
|
||||
public init(name: String, url: String, history: [History]? = nil, following: Bool? = nil) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.history = history
|
||||
self.following = following
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case url
|
||||
|
|
|
@ -11,4 +11,15 @@ extension URL {
|
|||
public static func httpScheme(domain: String) -> String {
|
||||
return domain.hasSuffix(".onion") ? "http" : "https"
|
||||
}
|
||||
|
||||
// inspired by https://stackoverflow.com/a/49072718
|
||||
public func isValidURL() -> Bool {
|
||||
if let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue),
|
||||
let match = detector.firstMatch(in: absoluteString, options: [], range: NSRange(location: 0, length: absoluteString.utf16.count)) {
|
||||
// it is a link, if the match covers the whole string
|
||||
return match.range.length == absoluteString.utf16.count
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,8 +39,8 @@ extension FLAnimatedImageView {
|
|||
|
||||
public func setImage(
|
||||
url: URL?,
|
||||
placeholder: UIImage?,
|
||||
scaleToSize: CGSize?
|
||||
placeholder: UIImage? = nil,
|
||||
scaleToSize: CGSize? = nil
|
||||
) {
|
||||
// cancel task
|
||||
cancelTask()
|
||||
|
|
|
@ -22,4 +22,18 @@ public enum MastodonRegex {
|
|||
/// #…
|
||||
/// :…
|
||||
public static let autoCompletePattern = "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))|(^\\B:|\\s:)([a-zA-Z0-9_]+)"
|
||||
|
||||
public enum Search {
|
||||
public static let username = "^@?[a-z0-9_-]+(@[\\S]+)?$"
|
||||
|
||||
/// See: https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/utils/hashtags.ts
|
||||
public static var hashtag: String {
|
||||
let word = "\\p{L}\\p{M}\\p{N}\\p{Pc}"
|
||||
let alpha = "\\p{L}\\p{M}"
|
||||
let hashtag_separators = "_\\u00b7\\u200c"
|
||||
|
||||
return "^(([\(word)_][\(word)\(hashtag_separators)]*[\(alpha)\(hashtag_separators)][\(word)\(hashtag_separators)]*[\(word)_])|([\(word)_]*[\(alpha)][\(word)_]*))$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// Created by MainasuK Cirno on 2021-7-21.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonLocalization
|
||||
|
||||
|
@ -117,26 +116,3 @@ extension AvatarButton {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct AvatarButton_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
UIViewPreview(width: 42) {
|
||||
let avatarButton = AvatarButton()
|
||||
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
avatarButton.widthAnchor.constraint(equalToConstant: 42),
|
||||
avatarButton.heightAnchor.constraint(equalToConstant: 42),
|
||||
])
|
||||
return avatarButton
|
||||
}
|
||||
.previewLayout(.fixed(width: 42, height: 42))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonMeta
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
public class CondensedUserView: UIView {
|
||||
private static var metricFormatter = MastodonMetricFormatter()
|
||||
|
||||
private let avatarImageWrapperView: UIView
|
||||
let avatarImageView: AvatarImageView
|
||||
|
||||
private let metaInformationStackView: UIStackView
|
||||
|
||||
private let upperLineStackView: UIStackView
|
||||
let displayNameLabel: MetaLabel
|
||||
let acctLabel: UILabel
|
||||
|
||||
private let lowerLineStackView: UIStackView
|
||||
let followersLabel: UILabel
|
||||
let verifiedLinkImageView: UIImageView
|
||||
let verifiedLinkLabel: MetaLabel
|
||||
|
||||
private let contentStackView: UIStackView
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
avatarImageView = AvatarImageView()
|
||||
avatarImageView.cornerConfiguration = AvatarImageView.CornerConfiguration(corner: .fixed(radius: 8))
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
avatarImageWrapperView = UIView()
|
||||
avatarImageWrapperView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarImageWrapperView.addSubview(avatarImageView)
|
||||
|
||||
displayNameLabel = MetaLabel(style: .statusName)
|
||||
displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
displayNameLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
acctLabel = UILabel()
|
||||
acctLabel.textColor = .secondaryLabel
|
||||
acctLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
acctLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
|
||||
upperLineStackView = UIStackView(arrangedSubviews: [displayNameLabel, acctLabel])
|
||||
upperLineStackView.distribution = .fill
|
||||
upperLineStackView.alignment = .center
|
||||
|
||||
followersLabel = UILabel()
|
||||
followersLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
followersLabel.textColor = .secondaryLabel
|
||||
followersLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
verifiedLinkImageView = UIImageView()
|
||||
verifiedLinkImageView.setContentCompressionResistancePriority(.defaultHigh - 1, for: .vertical)
|
||||
verifiedLinkImageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||
verifiedLinkImageView.contentMode = .scaleAspectFit
|
||||
|
||||
verifiedLinkLabel = MetaLabel(style: .profileFieldValue)
|
||||
verifiedLinkLabel.setContentCompressionResistancePriority(.defaultHigh - 2, for: .horizontal)
|
||||
verifiedLinkLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
verifiedLinkLabel.textAttributes = [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)),
|
||||
.foregroundColor: UIColor.secondaryLabel
|
||||
]
|
||||
verifiedLinkLabel.linkAttributes = [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)),
|
||||
.foregroundColor: Asset.Colors.Brand.blurple.color
|
||||
]
|
||||
verifiedLinkLabel.isUserInteractionEnabled = false
|
||||
|
||||
lowerLineStackView = UIStackView(arrangedSubviews: [followersLabel, verifiedLinkImageView, verifiedLinkLabel])
|
||||
lowerLineStackView.distribution = .fill
|
||||
lowerLineStackView.alignment = .center
|
||||
lowerLineStackView.spacing = 4
|
||||
lowerLineStackView.setCustomSpacing(2, after: verifiedLinkImageView)
|
||||
|
||||
metaInformationStackView = UIStackView(arrangedSubviews: [upperLineStackView, lowerLineStackView])
|
||||
metaInformationStackView.axis = .vertical
|
||||
metaInformationStackView.alignment = .leading
|
||||
|
||||
contentStackView = UIStackView(arrangedSubviews: [avatarImageWrapperView, metaInformationStackView])
|
||||
contentStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentStackView.axis = .horizontal
|
||||
contentStackView.alignment = .center
|
||||
contentStackView.spacing = 16
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(contentStackView)
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor, constant: 16),
|
||||
bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8),
|
||||
|
||||
upperLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor),
|
||||
lowerLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor),
|
||||
metaInformationStackView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor),
|
||||
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: 30),
|
||||
avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor),
|
||||
avatarImageView.topAnchor.constraint(greaterThanOrEqualTo: avatarImageWrapperView.topAnchor),
|
||||
avatarImageView.leadingAnchor.constraint(equalTo: avatarImageWrapperView.leadingAnchor),
|
||||
avatarImageWrapperView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
||||
avatarImageWrapperView.bottomAnchor.constraint(greaterThanOrEqualTo: avatarImageView.bottomAnchor),
|
||||
avatarImageView.centerYAnchor.constraint(equalTo: avatarImageWrapperView.centerYAnchor),
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
public func prepareForReuse() {
|
||||
avatarImageView.prepareForReuse()
|
||||
}
|
||||
|
||||
public func configure(with user: MastodonUser) {
|
||||
let displayNameMetaContent: MetaContent
|
||||
do {
|
||||
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
|
||||
displayNameMetaContent = try MastodonMetaContent.convert(document: content)
|
||||
} catch {
|
||||
displayNameMetaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||
}
|
||||
|
||||
displayNameLabel.configure(content: displayNameMetaContent)
|
||||
acctLabel.text = user.acct
|
||||
followersLabel.attributedText = NSAttributedString(
|
||||
format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]),
|
||||
args: NSAttributedString(string: Self.metricFormatter.string(from: Int(user.followersCount)) ?? user.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))])
|
||||
)
|
||||
|
||||
avatarImageView.setImage(url: user.avatarImageURL())
|
||||
|
||||
if let verifiedLink = user.verifiedLink?.value {
|
||||
verifiedLinkImageView.image = UIImage(systemName: "checkmark")
|
||||
verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
let verifiedLinkMetaContent: MetaContent
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:])
|
||||
verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
} catch {
|
||||
verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink)
|
||||
}
|
||||
|
||||
verifiedLinkLabel.configure(content: verifiedLinkMetaContent)
|
||||
} else {
|
||||
verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle")
|
||||
verifiedLinkImageView.tintColor = .secondaryLabel
|
||||
|
||||
verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink))
|
||||
}
|
||||
}
|
||||
|
||||
public func configure(with account: Mastodon.Entity.Account) {
|
||||
let displayNameMetaContent: MetaContent
|
||||
do {
|
||||
let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis?.asDictionary ?? [:])
|
||||
displayNameMetaContent = try MastodonMetaContent.convert(document: content)
|
||||
} catch {
|
||||
displayNameMetaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
|
||||
}
|
||||
|
||||
displayNameLabel.configure(content: displayNameMetaContent)
|
||||
acctLabel.text = account.acct
|
||||
followersLabel.attributedText = NSAttributedString(
|
||||
format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]),
|
||||
args: NSAttributedString(string: Self.metricFormatter.string(from: account.followersCount) ?? account.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))])
|
||||
)
|
||||
|
||||
avatarImageView.setImage(url: account.avatarImageURL())
|
||||
|
||||
if let verifiedLink = account.verifiedLink?.value {
|
||||
verifiedLinkImageView.image = UIImage(systemName: "checkmark")
|
||||
verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
let verifiedLinkMetaContent: MetaContent
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:])
|
||||
verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
} catch {
|
||||
verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink)
|
||||
}
|
||||
|
||||
verifiedLinkLabel.configure(content: verifiedLinkMetaContent)
|
||||
} else {
|
||||
verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle")
|
||||
verifiedLinkImageView.tintColor = .secondaryLabel
|
||||
|
||||
verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -361,8 +361,6 @@ extension StatusView.ViewModel {
|
|||
statusView.statusCardControl.alpha = isContentReveal ? 1 : 0
|
||||
|
||||
statusView.setSpoilerOverlayViewHidden(isHidden: isContentReveal)
|
||||
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): isContentReveal: \(isContentReveal)")
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -400,7 +398,6 @@ extension StatusView.ViewModel {
|
|||
$mediaViewConfigurations
|
||||
.sink { [weak self] configurations in
|
||||
guard let self = self else { return }
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): configure media")
|
||||
|
||||
statusView.mediaGridContainerView.prepareForReuse()
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import CoreDataStack
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MetaTextKit
|
||||
|
@ -19,9 +18,7 @@ extension UserView {
|
|||
public final class ViewModel: ObservableObject {
|
||||
public var disposeBag = Set<AnyCancellable>()
|
||||
public var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
let logger = Logger(subsystem: "StatusView", category: "ViewModel")
|
||||
|
||||
|
||||
@Published public var authorAvatarImage: UIImage?
|
||||
@Published public var authorAvatarImageURL: URL?
|
||||
@Published public var authorName: MetaContent?
|
||||
|
|
|
@ -261,41 +261,48 @@ public extension UserView {
|
|||
switch state {
|
||||
|
||||
case .loading:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(nil, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.disabled.color, for: .normal)
|
||||
|
||||
case .follow:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
|
||||
followButton.setTitleColor(.white, for: .normal)
|
||||
|
||||
case .request:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.request, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
|
||||
followButton.setTitleColor(.white, for: .normal)
|
||||
|
||||
case .pending:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.pending, for: .normal)
|
||||
followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal)
|
||||
|
||||
case .unfollow:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal)
|
||||
followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal)
|
||||
|
||||
case .blocked:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.blocked, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userBlocked.color, for: .normal)
|
||||
followButton.setTitleColor(.systemRed, for: .normal)
|
||||
|
||||
case .none:
|
||||
followButtonWrapper.isHidden = true
|
||||
followButton.isHidden = true
|
||||
followButton.setTitle(nil, for: .normal)
|
||||
followButton.setBackgroundColor(.clear, for: .normal)
|
||||
|
|
Loading…
Reference in New Issue