// // HomeTimelineViewController+DebugAction.swift // Mastodon // // Created by MainasuK Cirno on 2021-2-5. // #if DEBUG || SNAPSHOT import os.log import UIKit import CoreData import CoreDataStack import FLEX import SwiftUI import MastodonUI import MastodonSDK import StoreKit extension HomeTimelineViewController { var debugMenu: UIMenu { let menu = UIMenu( title: "Debug Tools", image: nil, identifier: nil, options: .displayInline, children: [ showMenu, moveMenu, dropMenu, miscMenu, notificationMenu, UIAction(title: "Settings", image: UIImage(systemName: "gear"), attributes: []) { [weak self] action in guard let self = self else { return } self.showSettings(action) }, UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in guard let self = self else { return } self.signOutAction(action) } ] ) return menu } var showMenu: UIMenu { return UIMenu( title: "Show…", image: UIImage(systemName: "plus.rectangle.on.rectangle"), identifier: nil, options: [], children: [ UIAction(title: "FLEX", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } self.showFLEXAction(action) }), UIAction(title: "Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in guard let self = self else { return } self.showWelcomeAction(action) }, UIAction(title: "Confirm Email", image: UIImage(systemName: "envelope"), attributes: []) { [weak self] action in guard let self = self else { return } self.showConfirmEmail(action) }, UIAction(title: "Account List", image: UIImage(systemName: "person"), attributes: []) { [weak self] action in guard let self = self else { return } self.showAccountList(action) }, UIAction(title: "Profile", image: UIImage(systemName: "person.crop.circle"), attributes: []) { [weak self] action in guard let self = self else { return } self.showProfileAction(action) }, UIAction(title: "Thread", image: UIImage(systemName: "bubble.left.and.bubble.right"), attributes: []) { [weak self] action in guard let self = self else { return } self.showThreadAction(action) }, UIAction(title: "Account Recommend", image: UIImage(systemName: "human"), attributes: []) { [weak self] action in guard let self = self else { return } let suggestionAccountViewModel = SuggestionAccountViewModel(context: self.context) self.coordinator.present( scene: .suggestionAccount(viewModel: suggestionAccountViewModel), from: self, transition: .modal(animated: true, completion: nil) ) }, UIAction(title: "Store Rating", image: UIImage(systemName: "star.fill"), attributes: []) { [weak self] action in guard let self = self else { return } guard let windowScene = self.view.window?.windowScene else { return } SKStoreReviewController.requestReview(in: windowScene) }, ] ) } var dropMenu: UIMenu { return UIMenu( title: "Drop…", image: UIImage(systemName: "minus.circle"), identifier: nil, options: [], children: [50, 100, 150, 200, 250, 300].map { count in UIAction(title: "Drop Recent \(count) Statuses", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } self.dropRecentStatusAction(action, count: count) }) } ) } var miscMenu: UIMenu { return UIMenu( title: "Debug…", image: UIImage(systemName: "switch.2"), identifier: nil, options: [], children: [ UIAction(title: "Toggle EmptyView", image: UIImage(systemName: "clear"), attributes: []) { [weak self] action in guard let self = self else { return } if self.emptyView.superview != nil { self.emptyView.removeFromSuperview() } else { self.showEmptyView() } }, UIAction( title: "Enable account switcher wizard", image: UIImage(systemName: "square.stack.3d.down.forward.fill"), identifier: nil, attributes: [], state: .off, handler: { _ in UserDefaults.shared.didShowMultipleAccountSwitchWizard = false } ), ] ) } var notificationMenu: UIMenu { return UIMenu( title: "Notification…", image: UIImage(systemName: "bell.badge"), identifier: nil, options: [], children: [ UIAction(title: "Badge +1", image: UIImage(systemName: "app.badge.fill"), attributes: []) { [weak self] action in guard let self = self else { return } guard let accessToken = self.context.authenticationService.activeMastodonAuthentication.value?.userAccessToken else { return } UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) self.context.notificationService.applicationIconBadgeNeedsUpdate.send() }, UIAction(title: "Profile", image: UIImage(systemName: "person.badge.plus"), attributes: []) { [weak self] action in guard let self = self else { return } self.showNotification(action, notificationType: .follow) }, UIAction(title: "Status", image: UIImage(systemName: "list.bullet.rectangle"), attributes: []) { [weak self] action in guard let self = self else { return } self.showNotification(action, notificationType: .mention) }, ] ) } } extension HomeTimelineViewController { enum MoveAction: String, CaseIterable { case gap case reply case mention case poll // case quote // case gif // case video // case location // case followsYouAuthor // case blockingAuthor var title: String { return rawValue.capitalized } func match(item: StatusItem) -> Bool { let authenticationBox = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value switch item { case .feed(let record): guard let feed = record.object(in: AppContext.shared.managedObjectContext) else { return false } if let status = feed.status { switch self { case .gap: return false case .reply: return status.inReplyToID != nil case .mention: return !(status.reblog ?? status).mentions.isEmpty case .poll: return (status.reblog ?? status).poll != nil // case .quote: // return status.quote != nil // case .gif: // return status.attachments.contains(where: { attachment in attachment.kind == .animatedGIF }) // case .video: // return status.attachments.contains(where: { attachment in attachment.kind == .video }) // case .location: // return status.location != nil // case .followsYouAuthor: // guard case let .twitter(authenticationContext) = authenticationContext else { return false } // guard let me = authenticationContext.authenticationRecord.object(in: AppContext.shared.managedObjectContext)?.user else { return false } // return (status.repost ?? status).author.following.contains(me) // case .blockingAuthor: // guard case let .twitter(authenticationContext) = authenticationContext else { return false } // guard let me = authenticationContext.authenticationRecord.object(in: AppContext.shared.managedObjectContext)?.user else { return false } // return (status.repost ?? status).author.blockingBy.contains(me) // default: // return false } // end switch } else { return false } case .feedLoader where self == .gap: return true default: return false } } func firstMatch(in items: [StatusItem]) -> StatusItem? { return items.first { item in self.match(item: item) } } } var moveMenu: UIMenu { return UIMenu( title: "Move to…", image: UIImage(systemName: "arrow.forward.circle"), identifier: nil, options: [], children: MoveAction.allCases.map { moveAction in UIAction(title: "First \(moveAction.title)", image: nil, attributes: []) { [weak self] action in guard let self = self else { return } self.moveToFirst(action, moveAction: moveAction) } } ) } private func moveToFirst(_ sender: UIAction, moveAction: MoveAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshot = diffableDataSource.snapshot() let items = snapshot.itemIdentifiers guard let targetItem = moveAction.firstMatch(in: items), let index = snapshot.indexOfItem(targetItem) else { return } let indexPath = IndexPath(row: index, section: 0) tableView.scrollToRow(at: indexPath, at: .middle, animated: true) tableView.blinkRow(at: indexPath) } } extension HomeTimelineViewController { @objc private func showFLEXAction(_ sender: UIAction) { FLEXManager.shared.showExplorer() } @objc private func dropRecentStatusAction(_ sender: UIAction, count: Int) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshot = diffableDataSource.snapshot() let feedRecords = snapshot.itemIdentifiers.prefix(count).compactMap { item -> ManagedObjectRecord? in switch item { case .feed(let record): return record default: return nil } } let managedObjectContext = viewModel.context.backgroundManagedObjectContext Task { try await managedObjectContext.performChanges { for record in feedRecords { guard let feed = record.object(in: managedObjectContext) else { continue } let status = feed.status managedObjectContext.delete(feed) if let status = status { managedObjectContext.delete(status) } } // end for in } // end managedObjectContext.performChanges } // end Task } @objc private func showWelcomeAction(_ sender: UIAction) { coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) } @objc private func showConfirmEmail(_ sender: UIAction) { let mastodonConfirmEmailViewModel = MastodonConfirmEmailViewModel() coordinator.present(scene: .mastodonConfirmEmail(viewModel: mastodonConfirmEmailViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } @objc private func showAccountList(_ sender: UIAction) { coordinator.present(scene: .accountList, from: self, transition: .modal(animated: true, completion: nil)) } @objc private func showProfileAction(_ sender: UIAction) { let alertController = UIAlertController(title: "Enter User ID", message: nil, preferredStyle: .alert) alertController.addTextField() let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in guard let self = self else { return } guard let textField = alertController?.textFields?.first else { return } let profileViewModel = RemoteProfileViewModel(context: self.context, userID: textField.text ?? "") self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) } alertController.addAction(showAction) let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(cancelAction) coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } @objc private func showThreadAction(_ sender: UIAction) { let alertController = UIAlertController(title: "Enter Status ID", message: nil, preferredStyle: .alert) alertController.addTextField() let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in guard let self = self else { return } guard let textField = alertController?.textFields?.first else { return } let threadViewModel = RemoteThreadViewModel(context: self.context, statusID: textField.text ?? "") self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show) } alertController.addAction(showAction) let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(cancelAction) coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } private func showNotification(_ sender: UIAction, notificationType: Mastodon.Entity.Notification.NotificationType) { guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } let alertController = UIAlertController(title: "Enter notification ID", message: nil, preferredStyle: .alert) alertController.addTextField() let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in guard let self = self else { return } guard let textField = alertController?.textFields?.first, let text = textField.text, let notificationID = Int(text) else { return } let pushNotification = MastodonPushNotification( accessToken: authenticationBox.userAuthorization.accessToken, notificationID: notificationID, notificationType: notificationType.rawValue, preferredLocale: nil, icon: nil, title: "", body: "" ) self.context.notificationService.requestRevealNotificationPublisher.send(pushNotification) } alertController.addAction(showAction) // for multiple accounts debug let boxes = self.context.authenticationService.mastodonAuthenticationBoxes.value // already sorted if boxes.count >= 2 { let accessToken = boxes[1].userAuthorization.accessToken let showForSecondaryAction = UIAlertAction(title: "Show for Secondary", style: .default) { [weak self, weak alertController] _ in guard let self = self else { return } guard let textField = alertController?.textFields?.first, let text = textField.text, let notificationID = Int(text) else { return } let pushNotification = MastodonPushNotification( accessToken: accessToken, notificationID: notificationID, notificationType: notificationType.rawValue, preferredLocale: nil, icon: nil, title: "", body: "" ) self.context.notificationService.requestRevealNotificationPublisher.send(pushNotification) } alertController.addAction(showForSecondaryAction) } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(cancelAction) self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } @objc private func showSettings(_ sender: UIAction) { guard let currentSetting = context.settingService.currentSetting.value else { return } let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting) coordinator.present( scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil) ) } } #endif