feature: suggestion account scene
This commit is contained in:
parent
e7cd130bf1
commit
c8474c6a7f
|
@ -51,7 +51,9 @@
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share_user": "Share %s",
|
"share_user": "Share %s",
|
||||||
"open_in_safari": "Open in Safari"
|
"open_in_safari": "Open in Safari",
|
||||||
|
"find_people": "Find people to follow",
|
||||||
|
"manually_search": "Manually search instead"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"user_reblogged": "%s reblogged",
|
"user_reblogged": "%s reblogged",
|
||||||
|
@ -230,6 +232,10 @@
|
||||||
"Publishing": "Publishing post..."
|
"Publishing": "Publishing post..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"suggestion_account": {
|
||||||
|
"title": "Find People to Follow",
|
||||||
|
"follow_explain": "When you follow someone, you’ll see their posts in your home feed."
|
||||||
|
},
|
||||||
"public_timeline": {
|
"public_timeline": {
|
||||||
"title": "Public"
|
"title": "Public"
|
||||||
},
|
},
|
||||||
|
|
|
@ -120,6 +120,9 @@
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; };
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; };
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
||||||
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
||||||
|
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; };
|
||||||
|
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */; };
|
||||||
|
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */; };
|
||||||
2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */; };
|
2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */; };
|
||||||
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; };
|
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; };
|
||||||
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */; };
|
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */; };
|
||||||
|
@ -530,6 +533,9 @@
|
||||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||||
|
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewController.swift; sourceTree = "<group>"; };
|
||||||
|
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Notification+Type.swift"; sourceTree = "<group>"; };
|
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Notification+Type.swift"; sourceTree = "<group>"; };
|
||||||
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
||||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = "<group>"; };
|
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1182,6 +1188,24 @@
|
||||||
path = Decoration;
|
path = Decoration;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */,
|
||||||
|
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */,
|
||||||
|
2DAC9E43262FC9DE0062E1A6 /* TableViewCell */,
|
||||||
|
);
|
||||||
|
path = SuggestionAccount;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
2DAC9E43262FC9DE0062E1A6 /* TableViewCell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = TableViewCell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2DE0FAC62615F5D200CDF649 /* View */ = {
|
2DE0FAC62615F5D200CDF649 /* View */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1669,6 +1693,7 @@
|
||||||
2D38F1D325CD463600561493 /* HomeTimeline */,
|
2D38F1D325CD463600561493 /* HomeTimeline */,
|
||||||
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
||||||
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
||||||
|
2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */,
|
||||||
DB9D6BEE25E4F5370051B173 /* Search */,
|
DB9D6BEE25E4F5370051B173 /* Search */,
|
||||||
5B90C455262599800002E742 /* Settings */,
|
5B90C455262599800002E742 /* Settings */,
|
||||||
DB9D6BFD25E4F57B0051B173 /* Notification */,
|
DB9D6BFD25E4F57B0051B173 /* Notification */,
|
||||||
|
@ -2385,6 +2410,7 @@
|
||||||
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
|
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
|
||||||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
||||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
||||||
|
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
|
||||||
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
||||||
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
||||||
|
@ -2425,6 +2451,7 @@
|
||||||
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
||||||
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */,
|
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */,
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
||||||
|
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
|
||||||
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
|
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||||
|
@ -2503,6 +2530,7 @@
|
||||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
||||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||||
|
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||||
|
|
|
@ -13,6 +13,7 @@ final public class SceneCoordinator {
|
||||||
private weak var scene: UIScene!
|
private weak var scene: UIScene!
|
||||||
private weak var sceneDelegate: SceneDelegate!
|
private weak var sceneDelegate: SceneDelegate!
|
||||||
private weak var appContext: AppContext!
|
private weak var appContext: AppContext!
|
||||||
|
private weak var tabBarController: MainTabBarController!
|
||||||
|
|
||||||
let id = UUID().uuidString
|
let id = UUID().uuidString
|
||||||
|
|
||||||
|
@ -61,6 +62,8 @@ extension SceneCoordinator {
|
||||||
case profile(viewModel: ProfileViewModel)
|
case profile(viewModel: ProfileViewModel)
|
||||||
case favorite(viewModel: FavoriteViewModel)
|
case favorite(viewModel: FavoriteViewModel)
|
||||||
|
|
||||||
|
// suggestion account
|
||||||
|
case suggestionAccount(viewModel: SuggestionAccountViewModel)
|
||||||
// misc
|
// misc
|
||||||
case safari(url: URL)
|
case safari(url: URL)
|
||||||
case alertController(alertController: UIAlertController)
|
case alertController(alertController: UIAlertController)
|
||||||
|
@ -93,6 +96,7 @@ extension SceneCoordinator {
|
||||||
func setup() {
|
func setup() {
|
||||||
let viewController = MainTabBarController(context: appContext, coordinator: self)
|
let viewController = MainTabBarController(context: appContext, coordinator: self)
|
||||||
sceneDelegate.window?.rootViewController = viewController
|
sceneDelegate.window?.rootViewController = viewController
|
||||||
|
tabBarController = viewController
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupOnboardingIfNeeds(animated: Bool) {
|
func setupOnboardingIfNeeds(animated: Bool) {
|
||||||
|
@ -187,6 +191,9 @@ extension SceneCoordinator {
|
||||||
return viewController
|
return viewController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func switchToTabBar(tab: MainTabBarController.Tab) {
|
||||||
|
tabBarController.selectedIndex = tab.rawValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SceneCoordinator {
|
private extension SceneCoordinator {
|
||||||
|
@ -246,6 +253,10 @@ private extension SceneCoordinator {
|
||||||
let _viewController = FavoriteViewController()
|
let _viewController = FavoriteViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .suggestionAccount(let viewModel):
|
||||||
|
let _viewController = SuggestionAccountViewController()
|
||||||
|
_viewController.viewModel = viewModel
|
||||||
|
viewController = _viewController
|
||||||
case .safari(let url):
|
case .safari(let url):
|
||||||
guard let scheme = url.scheme?.lowercased(),
|
guard let scheme = url.scheme?.lowercased(),
|
||||||
scheme == "http" || scheme == "https" else {
|
scheme == "http" || scheme == "https" else {
|
||||||
|
|
|
@ -29,4 +29,21 @@ extension RecommendAccountSection {
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func tableViewDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
managedObjectContext: NSManagedObjectContext,
|
||||||
|
viewModel: SuggestionAccountViewModel,
|
||||||
|
delegate: SuggestionAccountTableViewCellDelegate
|
||||||
|
) -> UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
|
||||||
|
UITableViewDiffableDataSource(tableView: tableView) { [weak viewModel,weak delegate] (tableView, indexPath, objectID) -> UITableViewCell? in
|
||||||
|
guard let viewModel = viewModel else { return nil }
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
|
||||||
|
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||||
|
let isSelected = viewModel.selectedAccounts.contains(objectID)
|
||||||
|
cell.delegate = delegate
|
||||||
|
cell.config(with: user, isSelected: isSelected)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,10 @@ internal enum L10n {
|
||||||
internal static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done")
|
internal static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done")
|
||||||
/// Edit
|
/// Edit
|
||||||
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
|
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
|
||||||
|
/// Find people to follow
|
||||||
|
internal static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople")
|
||||||
|
/// Manually search instead
|
||||||
|
internal static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch")
|
||||||
/// OK
|
/// OK
|
||||||
internal static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok")
|
internal static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok")
|
||||||
/// Open in Safari
|
/// Open in Safari
|
||||||
|
@ -675,6 +679,12 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal enum SuggestionAccount {
|
||||||
|
/// When you follow someone, you’ll see their posts in your home feed.
|
||||||
|
internal static let followExplain = L10n.tr("Localizable", "Scene.SuggestionAccount.FollowExplain")
|
||||||
|
/// Find People to Follow
|
||||||
|
internal static let title = L10n.tr("Localizable", "Scene.SuggestionAccount.Title")
|
||||||
|
}
|
||||||
internal enum Thread {
|
internal enum Thread {
|
||||||
/// Post
|
/// Post
|
||||||
internal static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle")
|
internal static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle")
|
||||||
|
|
|
@ -44,7 +44,8 @@ extension UserProviderFacade {
|
||||||
|
|
||||||
return context.apiService.toggleFollow(
|
return context.apiService.toggleFollow(
|
||||||
for: mastodonUser,
|
for: mastodonUser,
|
||||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
|
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||||
|
needFeedback: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.switchToLatest()
|
.switchToLatest()
|
||||||
|
|
|
@ -20,6 +20,8 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.Discard" = "Discard";
|
"Common.Controls.Actions.Discard" = "Discard";
|
||||||
"Common.Controls.Actions.Done" = "Done";
|
"Common.Controls.Actions.Done" = "Done";
|
||||||
"Common.Controls.Actions.Edit" = "Edit";
|
"Common.Controls.Actions.Edit" = "Edit";
|
||||||
|
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
||||||
|
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||||
"Common.Controls.Actions.Ok" = "OK";
|
"Common.Controls.Actions.Ok" = "OK";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
"Common.Controls.Actions.Preview" = "Preview";
|
"Common.Controls.Actions.Preview" = "Preview";
|
||||||
|
@ -220,6 +222,8 @@ any server.";
|
||||||
"Scene.Settings.Section.Spicyzone.Signout" = "Sign Out";
|
"Scene.Settings.Section.Spicyzone.Signout" = "Sign Out";
|
||||||
"Scene.Settings.Section.Spicyzone.Title" = "The spicy zone";
|
"Scene.Settings.Section.Spicyzone.Title" = "The spicy zone";
|
||||||
"Scene.Settings.Title" = "Settings";
|
"Scene.Settings.Title" = "Settings";
|
||||||
|
"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed.";
|
||||||
|
"Scene.SuggestionAccount.Title" = "Find People to Follow";
|
||||||
"Scene.Thread.BackTitle" = "Post";
|
"Scene.Thread.BackTitle" = "Post";
|
||||||
"Scene.Thread.Favorite.Multiple" = "%@ favorites";
|
"Scene.Thread.Favorite.Multiple" = "%@ favorites";
|
||||||
"Scene.Thread.Favorite.Single" = "%@ favorite";
|
"Scene.Thread.Favorite.Single" = "%@ favorite";
|
||||||
|
|
|
@ -23,6 +23,15 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
private(set) lazy var viewModel = HomeTimelineViewModel(context: context)
|
private(set) lazy var viewModel = HomeTimelineViewModel(context: context)
|
||||||
|
|
||||||
|
lazy var emptyView: UIStackView = {
|
||||||
|
let emptyView = UIStackView()
|
||||||
|
emptyView.axis = .vertical
|
||||||
|
emptyView.distribution = .fill
|
||||||
|
emptyView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 54, right: 20)
|
||||||
|
emptyView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
return emptyView
|
||||||
|
}()
|
||||||
|
|
||||||
let titleView = HomeTimelineNavigationBarTitleView()
|
let titleView = HomeTimelineNavigationBarTitleView()
|
||||||
|
|
||||||
let settingBarButtonItem: UIBarButtonItem = {
|
let settingBarButtonItem: UIBarButtonItem = {
|
||||||
|
@ -142,6 +151,13 @@ extension HomeTimelineViewController {
|
||||||
UIView.animate(withDuration: 0.5) { [weak self] in
|
UIView.animate(withDuration: 0.5) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.refreshControl.endRefreshing()
|
self.refreshControl.endRefreshing()
|
||||||
|
} completion: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty {
|
||||||
|
self.showEmptyView()
|
||||||
|
} else {
|
||||||
|
self.emptyView.removeFromSuperview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,6 +233,54 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HomeTimelineViewController {
|
extension HomeTimelineViewController {
|
||||||
|
func showEmptyView() {
|
||||||
|
if emptyView.superview != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
view.addSubview(emptyView)
|
||||||
|
emptyView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
emptyView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||||
|
emptyView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||||
|
emptyView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
let findPeopleButton: PrimaryActionButton = {
|
||||||
|
let button = PrimaryActionButton()
|
||||||
|
button.setTitle(L10n.Common.Controls.Actions.findPeople, for: .normal)
|
||||||
|
button.addTarget(self, action: #selector(HomeTimelineViewController.findPeopleButtonPressed(_:)), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
findPeopleButton.heightAnchor.constraint(equalToConstant: 46)
|
||||||
|
])
|
||||||
|
|
||||||
|
let manuallySearchButton: HighlightDimmableButton = {
|
||||||
|
let button = HighlightDimmableButton()
|
||||||
|
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
||||||
|
button.setTitle(L10n.Common.Controls.Actions.manuallySearch, for: .normal)
|
||||||
|
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
|
||||||
|
button.addTarget(self, action: #selector(HomeTimelineViewController.manuallySearchButtonPressed(_:)), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
emptyView.addArrangedSubview(findPeopleButton)
|
||||||
|
emptyView.setCustomSpacing(17, after: findPeopleButton)
|
||||||
|
emptyView.addArrangedSubview(manuallySearchButton)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineViewController {
|
||||||
|
|
||||||
|
@objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) {
|
||||||
|
let viewModel = SuggestionAccountViewModel(context: context)
|
||||||
|
coordinator.present(scene: .suggestionAccount(viewModel: viewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func manuallySearchButtonPressed(_ sender: UIButton) {
|
||||||
|
coordinator.switchToTabBar(tab: .search)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
|
@ -233,7 +233,7 @@ final class SearchViewModel: NSObject {
|
||||||
promise(.failure(APIService.APIError.implicit(APIService.APIError.ErrorReason.authenticationMissing)))
|
promise(.failure(APIService.APIError.implicit(APIService.APIError.ErrorReason.authenticationMissing)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.context.apiService.recommendAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
self.context.apiService.suggestionAccountV2(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
//
|
||||||
|
// SuggestionAccountViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SuggestionAccountViewController: UIViewController, NeedsDependency {
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
var viewModel: SuggestionAccountViewModel!
|
||||||
|
|
||||||
|
let tableView: UITableView = {
|
||||||
|
let tableView = ControlContainableTableView()
|
||||||
|
tableView.register(SuggestionAccountTableViewCell.self, forCellReuseIdentifier: String(describing: SuggestionAccountTableViewCell.self))
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.tableFooterView = UIView()
|
||||||
|
tableView.separatorStyle = .singleLine
|
||||||
|
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var tableHeader: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
view.frame = CGRect(origin: .zero, size: CGSize(width: tableView.frame.width, height: 156))
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let followExplainLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = L10n.Scene.SuggestionAccount.followExplain
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||||
|
label.numberOfLines = 0
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let avatarStackView: UIStackView = {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.distribution = .equalSpacing
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.spacing = 15
|
||||||
|
return stackView
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SuggestionAccountViewController {
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
title = L10n.Scene.SuggestionAccount.title
|
||||||
|
navigationItem.rightBarButtonItem
|
||||||
|
= UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done,
|
||||||
|
target: self,
|
||||||
|
action: #selector(SuggestionAccountViewController.doneButtonDidClick(_:)))
|
||||||
|
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
viewModel.diffableDataSource = RecommendAccountSection.tableViewDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
viewModel: viewModel,
|
||||||
|
delegate: self
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.accounts
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] accounts in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.setupHeader(accounts: accounts)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupHeader(accounts: [NSManagedObjectID]) {
|
||||||
|
if accounts.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
followExplainLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tableHeader.addSubview(followExplainLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
followExplainLabel.topAnchor.constraint(equalTo: tableHeader.topAnchor, constant: 20),
|
||||||
|
followExplainLabel.leadingAnchor.constraint(equalTo: tableHeader.leadingAnchor, constant: 20),
|
||||||
|
tableHeader.trailingAnchor.constraint(equalTo: followExplainLabel.trailingAnchor, constant: 20),
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tableHeader.addSubview(avatarStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarStackView.topAnchor.constraint(equalTo: followExplainLabel.topAnchor, constant: 20),
|
||||||
|
avatarStackView.leadingAnchor.constraint(equalTo: tableHeader.leadingAnchor, constant: 20),
|
||||||
|
avatarStackView.trailingAnchor.constraint(equalTo: tableHeader.trailingAnchor),
|
||||||
|
avatarStackView.bottomAnchor.constraint(equalTo: tableHeader.bottomAnchor),
|
||||||
|
])
|
||||||
|
let avatarImageViewHeight: Double = 56
|
||||||
|
let avatarImageViewCount = Int(floor((Double(tableView.frame.width) - 20) / (avatarImageViewHeight + 15)))
|
||||||
|
let count = min(avatarImageViewCount, accounts.count)
|
||||||
|
for i in 0 ..< count {
|
||||||
|
let account = context.managedObjectContext.object(with: accounts[i]) as! MastodonUser
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.layer.cornerRadius = 6
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.widthAnchor.constraint(equalToConstant: CGFloat(avatarImageViewHeight)),
|
||||||
|
imageView.heightAnchor.constraint(equalToConstant: CGFloat(avatarImageViewHeight)),
|
||||||
|
])
|
||||||
|
if let url = account.avatarImageURL() {
|
||||||
|
imageView.af.setImage(
|
||||||
|
withURL: url,
|
||||||
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
imageTransition: .crossDissolve(0.2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
avatarStackView.addArrangedSubview(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
tableView.tableHeaderView = tableHeader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate {
|
||||||
|
func accountButtonPressed(objectID: NSManagedObjectID, sender: UIButton) {
|
||||||
|
let selected = !sender.isSelected
|
||||||
|
sender.isSelected = !sender.isSelected
|
||||||
|
if selected {
|
||||||
|
viewModel.selectedAccounts.append(objectID)
|
||||||
|
} else {
|
||||||
|
viewModel.selectedAccounts.removeAll { $0 == objectID }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SuggestionAccountViewController {
|
||||||
|
@objc func doneButtonDidClick(_ sender: UIButton) {
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
viewModel.followAction()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
//
|
||||||
|
// SuggestionAccountViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class SuggestionAccountViewModel: NSObject {
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||||
|
var selectedAccounts = [NSManagedObjectID]()
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
|
||||||
|
|
||||||
|
init(context: AppContext, accounts: [NSManagedObjectID]? = nil) {
|
||||||
|
self.context = context
|
||||||
|
if let accounts = accounts {
|
||||||
|
self.accounts.value = accounts
|
||||||
|
}
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.accounts
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] accounts in
|
||||||
|
guard let dataSource = self?.diffableDataSource else { return }
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, NSManagedObjectID>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
snapshot.appendItems(accounts, toSection: .main)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
if accounts == nil || (accounts ?? []).isEmpty {
|
||||||
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
|
|
||||||
|
context.apiService.suggestionAccountV2(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: fetch recommendAccountV2 failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let ids = response.value.map(\.account.id)
|
||||||
|
let users: [MastodonUser]? = {
|
||||||
|
let request = MastodonUser.sortedFetchRequest
|
||||||
|
request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: ids)
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try context.managedObjectContext.fetch(request)
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if let accounts = users?.map(\.objectID) {
|
||||||
|
self.accounts.value = accounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func followAction() {
|
||||||
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
|
for objectID in selectedAccounts {
|
||||||
|
let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser
|
||||||
|
context.apiService.toggleFollow(
|
||||||
|
for: mastodonUser,
|
||||||
|
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||||
|
needFeedback: false
|
||||||
|
)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: follow failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { _ in
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
//
|
||||||
|
// SuggestionAccountTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol SuggestionAccountTableViewCellDelegate: AnyObject {
|
||||||
|
func accountButtonPressed(objectID: NSManagedObjectID, sender: UIButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SuggestionAccountTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
weak var delegate: SuggestionAccountTableViewCellDelegate?
|
||||||
|
|
||||||
|
let _imageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||||
|
imageView.layer.cornerRadius = 4
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.brandBlue.color
|
||||||
|
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let subTitleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.font = .preferredFont(forTextStyle: .body)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var button: HighlightDimmableButton = {
|
||||||
|
let button = HighlightDimmableButton(type: .custom)
|
||||||
|
if let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))?.withRenderingMode(.alwaysTemplate) {
|
||||||
|
button.setImage(plusImage, for: .normal)
|
||||||
|
}
|
||||||
|
if let minusImage = UIImage(systemName: "minus.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))?.withRenderingMode(.alwaysTemplate) {
|
||||||
|
button.setImage(minusImage, for: .selected)
|
||||||
|
}
|
||||||
|
button.publisher(for: \.isSelected)
|
||||||
|
.sink { isSelected in
|
||||||
|
if isSelected {
|
||||||
|
button.tintColor = Asset.Colors.danger.color
|
||||||
|
} else {
|
||||||
|
button.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &self.disposeBag)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
_imageView.af.cancelImageRequest()
|
||||||
|
_imageView.image = nil
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SuggestionAccountTableViewCell {
|
||||||
|
private func configure() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
containerStackView.axis = .horizontal
|
||||||
|
containerStackView.distribution = .fill
|
||||||
|
containerStackView.spacing = 12
|
||||||
|
containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 21, bottom: 12, right: 12)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
_imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(_imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
_imageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1),
|
||||||
|
_imageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
let textStackView = UIStackView()
|
||||||
|
textStackView.axis = .vertical
|
||||||
|
textStackView.distribution = .fill
|
||||||
|
textStackView.alignment = .leading
|
||||||
|
textStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textStackView.addArrangedSubview(titleLabel)
|
||||||
|
subTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textStackView.addArrangedSubview(subTitleLabel)
|
||||||
|
subTitleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical)
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(textStackView)
|
||||||
|
textStackView.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
|
||||||
|
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
func config(with account: MastodonUser, isSelected: Bool) {
|
||||||
|
if let url = account.avatarImageURL() {
|
||||||
|
_imageView.af.setImage(
|
||||||
|
withURL: url,
|
||||||
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
imageTransition: .crossDissolve(0.2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
||||||
|
subTitleLabel.text = account.acct
|
||||||
|
button.isSelected = isSelected
|
||||||
|
button.publisher(for: .touchUpInside)
|
||||||
|
.sink { [weak self] sender in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.accountButtonPressed(objectID: account.objectID, sender: self.button)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,10 +24,15 @@ extension APIService {
|
||||||
/// - Returns: publisher for `Relationship`
|
/// - Returns: publisher for `Relationship`
|
||||||
func toggleFollow(
|
func toggleFollow(
|
||||||
for mastodonUser: MastodonUser,
|
for mastodonUser: MastodonUser,
|
||||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||||
|
needFeedback: Bool
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||||
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
var impactFeedbackGenerator: UIImpactFeedbackGenerator?
|
||||||
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
var notificationFeedbackGenerator: UINotificationFeedbackGenerator?
|
||||||
|
if needFeedback {
|
||||||
|
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||||
|
}
|
||||||
|
|
||||||
return followUpdateLocal(
|
return followUpdateLocal(
|
||||||
mastodonUserObjectID: mastodonUser.objectID,
|
mastodonUserObjectID: mastodonUser.objectID,
|
||||||
|
@ -35,9 +40,9 @@ extension APIService {
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.handleEvents { _ in
|
.handleEvents { _ in
|
||||||
impactFeedbackGenerator.prepare()
|
impactFeedbackGenerator?.prepare()
|
||||||
} receiveOutput: { _ in
|
} receiveOutput: { _ in
|
||||||
impactFeedbackGenerator.impactOccurred()
|
impactFeedbackGenerator?.impactOccurred()
|
||||||
} receiveCompletion: { completion in
|
} receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -74,13 +79,13 @@ extension APIService {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Friendship] rollback finish", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Friendship] rollback finish", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
} receiveValue: { _ in
|
} receiveValue: { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
notificationFeedbackGenerator.prepare()
|
notificationFeedbackGenerator?.prepare()
|
||||||
notificationFeedbackGenerator.notificationOccurred(.error)
|
notificationFeedbackGenerator?.notificationOccurred(.error)
|
||||||
}
|
}
|
||||||
.store(in: &self.disposeBag)
|
.store(in: &self.disposeBag)
|
||||||
|
|
||||||
case .finished:
|
case .finished:
|
||||||
notificationFeedbackGenerator.notificationOccurred(.success)
|
notificationFeedbackGenerator?.notificationOccurred(.success)
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: [Friendship] remote friendship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s: [Friendship] remote friendship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,7 +13,38 @@ import CoreDataStack
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
extension APIService {
|
extension APIService {
|
||||||
func recommendAccount(
|
func suggestionAccount(
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Suggestions.Query?,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
|
||||||
|
return Mastodon.API.Suggestions.get(session: session, domain: domain, query: query, authorization: authorization)
|
||||||
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> in
|
||||||
|
let log = OSLog.api
|
||||||
|
return self.backgroundManagedObjectContext.performChanges {
|
||||||
|
response.value.forEach { user in
|
||||||
|
let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: user, userCache: nil, networkDate: Date(), log: log)
|
||||||
|
let flag = isCreated ? "+" : "-"
|
||||||
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Account]> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func suggestionAccountV2(
|
||||||
domain: String,
|
domain: String,
|
||||||
query: Mastodon.API.Suggestions.Query?,
|
query: Mastodon.API.Suggestions.Query?,
|
||||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
@ -44,7 +75,7 @@ extension APIService {
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func recommendTrends(
|
func recommendTrends(
|
||||||
domain: String,
|
domain: String,
|
||||||
query: Mastodon.API.Trends.Query?
|
query: Mastodon.API.Trends.Query?
|
||||||
|
|
Loading…
Reference in New Issue