diff --git a/CoreDataStack/Entity/Notification.swift b/CoreDataStack/Entity/Notification.swift index 31c361aa4..04f8e9fdf 100644 --- a/CoreDataStack/Entity/Notification.swift +++ b/CoreDataStack/Entity/Notification.swift @@ -101,6 +101,10 @@ extension MastodonNotification { ]) } } + + public static func predicate(validTypesRaws types: [String]) -> NSPredicate { + return NSPredicate(format: "%K IN %@", #keyPath(MastodonNotification.typeRaw), types) + } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 90d2a1358..fa84dcee3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -169,7 +169,7 @@ 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; }; - 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; }; + 5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */; }; 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; }; 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; 5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */; }; @@ -732,7 +732,7 @@ 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = ""; }; 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = ""; }; 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; - 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = ""; }; + 5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlaybackService.swift; sourceTree = ""; }; 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = ""; }; @@ -1343,7 +1343,7 @@ DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */, 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */, 2DA6054625F716A2006356F9 /* PlaybackState.swift */, - 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */, + 5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */, DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */, DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */, 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */, @@ -2947,7 +2947,7 @@ DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */, DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */, 2D7631B325C159F700929FB9 /* Item.swift in Sources */, - 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */, + 5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */, DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, @@ -3614,7 +3614,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3622,7 +3622,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3641,7 +3641,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3649,7 +3649,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3904,7 +3904,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3912,7 +3912,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3927,7 +3927,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3935,7 +3935,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.1; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 155ec3fc7..47e3b5bbf 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -30,14 +30,16 @@ extension NotificationSection { guard let dependency = dependency else { return nil } switch notificationItem { case .notification(let objectID, let attribute): - let notification = managedObjectContext.object(with: objectID) as! MastodonNotification guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else { + // filter out invalid type using predicate assertionFailure() - return nil + return UITableViewCell() } - let timeText = notification.createAt.slowedTimeAgoSinceNow - + + let createAt = notification.createAt + let timeText = createAt.slowedTimeAgoSinceNow + let actionText = type.actionText let actionImageName = type.actionImageName let color = type.color @@ -57,23 +59,24 @@ extension NotificationSection { requestUserID: requestUserID, statusItemAttribute: attribute ) + cell.actionImageBackground.backgroundColor = color + cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName + cell.actionLabel.text = actionText + " · " + timeText timestampUpdatePublisher - .sink { _ in - let timeText = notification.createAt.slowedTimeAgoSinceNow + .sink { [weak cell] _ in + guard let cell = cell else { return } + let timeText = createAt.slowedTimeAgoSinceNow cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) - cell.actionImageBackground.backgroundColor = color - cell.actionLabel.text = actionText + " · " + timeText - cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName if let url = notification.account.avatarImageURL() { - cell.avatatImageView.af.setImage( + cell.avatarImageView.af.setImage( withURL: url, placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2) ) } - cell.avatatImageView.gesture().sink { [weak cell] _ in + cell.avatarImageView.gesture().sink { [weak cell] _ in cell?.delegate?.userAvatarDidPressed(notification: notification) } .store(in: &cell.disposeBag) @@ -86,8 +89,9 @@ extension NotificationSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell cell.delegate = delegate timestampUpdatePublisher - .sink { _ in - let timeText = notification.createAt.slowedTimeAgoSinceNow + .sink { [weak cell] _ in + guard let cell = cell else { return } + let timeText = createAt.slowedTimeAgoSinceNow cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) diff --git a/Mastodon/Extension/CoreDataStack/Emojis.swift b/Mastodon/Extension/CoreDataStack/Emojis.swift index 87ae50171..a35e2630e 100644 --- a/Mastodon/Extension/CoreDataStack/Emojis.swift +++ b/Mastodon/Extension/CoreDataStack/Emojis.swift @@ -8,11 +8,11 @@ import Foundation import MastodonSDK -protocol EmojiContinaer { +protocol EmojiContainer { var emojisData: Data? { get } } -extension EmojiContinaer { +extension EmojiContainer { static func encode(emojis: [Mastodon.Entity.Emoji]) -> Data? { return try? JSONEncoder().encode(emojis) diff --git a/Mastodon/Extension/CoreDataStack/Fields.swift b/Mastodon/Extension/CoreDataStack/Fields.swift index 795863f88..5674c08b2 100644 --- a/Mastodon/Extension/CoreDataStack/Fields.swift +++ b/Mastodon/Extension/CoreDataStack/Fields.swift @@ -8,11 +8,11 @@ import Foundation import MastodonSDK -protocol FieldContinaer { +protocol FieldContainer { var fieldsData: Data? { get } } -extension FieldContinaer { +extension FieldContainer { static func encode(fields: [Mastodon.Entity.Field]) -> Data? { return try? JSONEncoder().encode(fields) diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift index f5cdc1af0..1e0fe7dad 100644 --- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift +++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift @@ -101,5 +101,5 @@ extension MastodonUser { } } -extension MastodonUser: EmojiContinaer { } -extension MastodonUser: FieldContinaer { } +extension MastodonUser: EmojiContainer { } +extension MastodonUser: FieldContainer { } diff --git a/Mastodon/Extension/CoreDataStack/Status.swift b/Mastodon/Extension/CoreDataStack/Status.swift index 0432c441b..d8e1b9307 100644 --- a/Mastodon/Extension/CoreDataStack/Status.swift +++ b/Mastodon/Extension/CoreDataStack/Status.swift @@ -88,4 +88,4 @@ extension Status { } } -extension Status: EmojiContinaer { } +extension Status: EmojiContainer { } diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift index 391519e84..d338615df 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -17,7 +17,6 @@ enum MastodonStatusContent { static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult { let document: String = { var content = content - content = content.replacingOccurrences(of: "
", with: "\n") for (shortcode, url) in emojiDict { let emojiNode = "\(shortcode)" let pattern = ":\(shortcode):" @@ -189,6 +188,14 @@ extension MastodonStatusContent { static func parse(document: String) throws -> MastodonStatusContent.Node { let html = try HTML(html: document, encoding: .utf8) + + // add `\r\n` explicit due to Kanna text missing it after convert to text + // ref: https://github.com/tid-kijyun/Kanna/issues/150 + let brNodes = html.css("br").makeIterator() + while let brNode = brNodes.next() { + brNode.addNextSibling(try! HTML(html: "\r\n", encoding: .utf8).body!) + } + let body = html.body ?? nil let text = body?.text ?? "" let level = 0 diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 90f3e4487..6dfb77d3c 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -368,6 +368,7 @@ extension ComposeViewController { guard let self = self else { return } let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle) self.composeToolbarView.visibilityButton.setImage(image, for: .normal) + self.composeToolbarView.activeVisibilityType.value = type } .store(in: &disposeBag) @@ -676,7 +677,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate { updateAttributedString attributedString: NSAttributedString, completion: @escaping (NSAttributedString?) -> Void ) { - // FIXME: needs O(1) update completion to fix profermance issue + // FIXME: needs O(1) update completion to fix performance issue DispatchQueue.global().async { let string = attributedString.string os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string) @@ -1291,7 +1292,8 @@ extension ComposeViewController { case togglePoll case toggleContentWarning case selectVisibilityPublic - case selectVisibilityUnlisted + // TODO: remove selectVisibilityUnlisted from codebase + // case selectVisibilityUnlisted case selectVisibilityPrivate case selectVisibilityDirect @@ -1305,7 +1307,7 @@ extension ComposeViewController { case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public) - case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted) + // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted) case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private) case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct) } @@ -1322,9 +1324,9 @@ extension ComposeViewController { case .togglePoll: return "p" // + shift + command case .toggleContentWarning: return "c" // + shift + command case .selectVisibilityPublic: return "1" // + command - case .selectVisibilityUnlisted: return "2" // + command - case .selectVisibilityPrivate: return "3" // + command - case .selectVisibilityDirect: return "4" // + command + // case .selectVisibilityUnlisted: return "2" // + command + case .selectVisibilityPrivate: return "2" // + command + case .selectVisibilityDirect: return "3" // + command } } @@ -1338,7 +1340,7 @@ extension ComposeViewController { case .togglePoll: return [.shift, .command] case .toggleContentWarning: return [.shift, .command] case .selectVisibilityPublic: return [.command] - case .selectVisibilityUnlisted: return [.command] + // case .selectVisibilityUnlisted: return [.command] case .selectVisibilityPrivate: return [.command] case .selectVisibilityDirect: return [.command] } @@ -1390,8 +1392,8 @@ extension ComposeViewController { composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside) case .selectVisibilityPublic: viewModel.selectedStatusVisibility.value = .public - case .selectVisibilityUnlisted: - viewModel.selectedStatusVisibility.value = .unlisted + // case .selectVisibilityUnlisted: + // viewModel.selectedStatusVisibility.value = .unlisted case .selectVisibilityPrivate: viewModel.selectedStatusVisibility.value = .private case .selectVisibilityDirect: diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 0ac124586..a368bfbb9 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -92,7 +92,8 @@ final class ComposeViewModel { self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value) self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) // end init - if case let .reply(repliedToStatusObjectID) = composeKind { + switch composeKind { + case .reply(let repliedToStatusObjectID): context.managedObjectContext.performAndWait { guard let status = context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return } let composeAuthor: MastodonUser? = { @@ -124,14 +125,13 @@ final class ComposeViewModel { self.preInsertedContent = preInsertedContent self.composeStatusAttribute.composeContent.value = preInsertedContent } - - } else if case let .hashtag(text) = composeKind { - let initialComposeContent = "#" + text + case .hashtag(let hashtag): + let initialComposeContent = "#" + hashtag UITextChecker.learnWord(initialComposeContent) let preInsertedContent = initialComposeContent + " " self.preInsertedContent = preInsertedContent self.composeStatusAttribute.composeContent.value = preInsertedContent - } else if case let .mention(mastodonUserObjectID) = composeKind { + case .mention(let mastodonUserObjectID): context.managedObjectContext.performAndWait { let mastodonUser = context.managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser let initialComposeContent = "@" + mastodonUser.acct @@ -140,7 +140,7 @@ final class ComposeViewModel { self.preInsertedContent = preInsertedContent self.composeStatusAttribute.composeContent.value = preInsertedContent } - } else { + case .post: self.preInsertedContent = nil } @@ -263,7 +263,7 @@ final class ComposeViewModel { } // if preInsertedContent plus a space is equal to the content, simply dismiss the modal if let preInsertedContent = self?.preInsertedContent { - return content == (preInsertedContent + " ") + return content == preInsertedContent } return false } @@ -374,11 +374,6 @@ final class ComposeViewModel { self.isPollToolbarButtonEnabled.value = !shouldPollDisable }) .store(in: &disposeBag) - - if let preInsertedContent = preInsertedContent { - // add a space after the injected text - composeStatusAttribute.composeContent.send(preInsertedContent + " ") - } } deinit { diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 9f2b5e74f..6acbf80ef 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -7,6 +7,7 @@ import os.log import UIKit +import Combine import MastodonSDK protocol ComposeToolbarViewDelegate: AnyObject { @@ -19,6 +20,8 @@ protocol ComposeToolbarViewDelegate: AnyObject { final class ComposeToolbarView: UIView { + var disposeBag = Set() + static let toolbarButtonSize: CGSize = CGSize(width: 44, height: 44) static let toolbarHeight: CGFloat = 44 @@ -76,6 +79,8 @@ final class ComposeToolbarView: UIView { return label }() + let activeVisibilityType = CurrentValueSubject(.public) + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -142,6 +147,15 @@ extension ComposeToolbarView { visibilityButton.showsMenuAsPrimaryAction = true updateToolbarButtonUserInterfaceStyle() + + // update menu when selected visibility type changed + activeVisibilityType + .receive(on: RunLoop.main) + .sink { [weak self] type in + guard let self = self else { return } + self.visibilityButton.menu = self.createVisibilityContextMenu(interfaceStyle: self.traitCollection.userInterfaceStyle) + } + .store(in: &disposeBag) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -161,14 +175,15 @@ extension ComposeToolbarView { enum VisibilitySelectionType: String, CaseIterable { case `public` - case unlisted + // TODO: remove unlisted option from codebase + // case unlisted case `private` case direct var title: String { switch self { case .public: return L10n.Scene.Compose.Visibility.public - case .unlisted: return L10n.Scene.Compose.Visibility.unlisted + // case .unlisted: return L10n.Scene.Compose.Visibility.unlisted case .private: return L10n.Scene.Compose.Visibility.private case .direct: return L10n.Scene.Compose.Visibility.direct } @@ -181,7 +196,7 @@ extension ComposeToolbarView { case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! } - case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! + // case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))! } @@ -190,7 +205,7 @@ extension ComposeToolbarView { func imageNameForTimeline() -> String { switch self { case .public: return "person.3" - case .unlisted: return "eye.slash" + // case .unlisted: return "eye.slash" case .private: return "person.crop.circle.badge.plus" case .direct: return "at" } @@ -199,7 +214,7 @@ extension ComposeToolbarView { var visibility: Mastodon.Entity.Status.Visibility { switch self { case .public: return .public - case .unlisted: return .unlisted + // case .unlisted: return .unlisted case .private: return .private case .direct: return .direct } @@ -268,7 +283,8 @@ extension ComposeToolbarView { private func createVisibilityContextMenu(interfaceStyle: UIUserInterfaceStyle) -> UIMenu { let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in - UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in + let state: UIMenuElement.State = activeVisibilityType.value == type ? .on : .off + return UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [weak self] action in guard let self = self else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue) self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type) diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index cd28c5f5a..4e0d9b6d9 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -9,6 +9,7 @@ import CoreData import CoreDataStack import os.log import UIKit +import MastodonSDK extension NotificationViewModel { func setupDiffableDataSource( @@ -16,7 +17,7 @@ extension NotificationViewModel { delegate: NotificationTableViewCellDelegate, dependency: NeedsDependency ) { - let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common) + let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .share() .eraseToAnyPublisher() @@ -44,7 +45,14 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { guard let diffableDataSource = self.diffableDataSource else { return } - let predicate = fetchedResultsController.fetchRequest.predicate + let predicate: NSPredicate = { + let notificationTypePredicate = MastodonNotification.predicate( + validTypesRaws: Mastodon.Entity.Notification.NotificationType.knownCases.map { $0.rawValue } + ) + return fetchedResultsController.fetchRequest.predicate.flatMap { + NSCompoundPredicate(andPredicateWithSubpredicates: [$0, notificationTypePredicate]) + } ?? notificationTypePredicate + }() let parentManagedObjectContext = fetchedResultsController.managedObjectContext let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) managedObjectContext.parent = parentManagedObjectContext @@ -73,19 +81,6 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { newSnapshot.appendSections([.main]) let items: [NotificationItem] = notifications.map { notification in let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute() - -// let attribute: Item.StatusAttribute = { -// if let attribute = oldSnapshotAttributeDict[notification.objectID] { -// return attribute -// } else if let status = notification.status { -// let attribute = Item.StatusAttribute() -// let isSensitive = status.sensitive || !(status.spoilerText ?? "").isEmpty -// attribute.isRevealing.value = !isSensitive -// return attribute -// } else { -// return Item.StatusAttribute() -// } -// }() return NotificationItem.notification(objectID: notification.objectID, attribute: attribute) } newSnapshot.appendItems(items, toSection: .main) diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index a950ede46..d4bd7b8f8 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -17,7 +17,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { var pollCountdownSubscription: AnyCancellable? var delegate: NotificationTableViewCellDelegate? - let avatatImageView: UIImageView = { + let avatarImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 4 imageView.layer.cornerCurve = .continuous @@ -86,7 +86,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { override func prepareForReuse() { super.prepareForReuse() - avatatImageView.af.cancelImageRequest() + avatarImageView.af.cancelImageRequest() statusView.updateContentWarningDisplay(isHidden: true, animated: false) statusView.pollTableView.dataSource = nil statusView.playerContainerView.reset() @@ -142,13 +142,13 @@ extension NotificationStatusTableViewCell { avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1) ]) - avatarContainer.addSubview(avatatImageView) - avatatImageView.translatesAutoresizingMaskIntoConstraints = false + avatarContainer.addSubview(avatarImageView) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1), - avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1), - avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor), - avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor) + avatarImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1), + avatarImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1), + avatarImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor), + avatarImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor) ]) avatarContainer.addSubview(actionImageBackground) diff --git a/Mastodon/Service/APIService/APIService+Media.swift b/Mastodon/Service/APIService/APIService+Media.swift index 03e333424..1fffb5a6c 100644 --- a/Mastodon/Service/APIService/APIService+Media.swift +++ b/Mastodon/Service/APIService/APIService+Media.swift @@ -13,7 +13,7 @@ extension APIService { func uploadMedia( domain: String, - query: Mastodon.API.Media.UploadMeidaQuery, + query: Mastodon.API.Media.UploadMediaQuery, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+UserTimeline.swift b/Mastodon/Service/APIService/APIService+UserTimeline.swift index cb20c85ef..0e1e223b1 100644 --- a/Mastodon/Service/APIService/APIService+UserTimeline.swift +++ b/Mastodon/Service/APIService/APIService+UserTimeline.swift @@ -27,7 +27,7 @@ extension APIService { ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization let requestMastodonUserID = authorizationBox.userID - let query = Mastodon.API.Account.AccountStatuseseQuery( + let query = Mastodon.API.Account.AccountStatusesQuery( maxID: maxID, sinceID: sinceID, excludeReplies: excludeReplies, diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index 23233ec3f..b154d17c2 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -56,7 +56,7 @@ extension MastodonAttachmentService.UploadState { guard let file = service.file.value else { return } let description = service.description.value - let query = Mastodon.API.Media.UploadMeidaQuery( + let query = Mastodon.API.Media.UploadMediaQuery( file: file, thumbnail: nil, description: description, diff --git a/Mastodon/Service/ViedeoPlaybackService.swift b/Mastodon/Service/VideoPlaybackService.swift similarity index 100% rename from Mastodon/Service/ViedeoPlaybackService.swift rename to Mastodon/Service/VideoPlaybackService.swift diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index d1c5458c4..0ca985b05 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -71,14 +71,14 @@ extension Mastodon.API.Account { /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" - /// - query: `AccountStatuseseQuery` with query parameters + /// - query: `AccountStatusesQuery` with query parameters /// - authorization: User token /// - Returns: `AnyPublisher` contains `Token` nested in the response public static func statuses( session: URLSession, domain: String, accountID: Mastodon.Entity.Account.ID, - query: AccountStatuseseQuery, + query: AccountStatusesQuery, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { let request = Mastodon.API.get( @@ -94,7 +94,7 @@ extension Mastodon.API.Account { .eraseToAnyPublisher() } - public struct AccountStatuseseQuery: GetQuery { + public struct AccountStatusesQuery: GetQuery { public let maxID: Mastodon.Entity.Status.ID? public let sinceID: Mastodon.Entity.Status.ID? public let excludeReplies: Bool? // undocumented diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift index 0918cbd07..a292df6ae 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift @@ -33,7 +33,7 @@ extension Mastodon.API.Media { public static func uploadMedia( session: URLSession, domain: String, - query: UploadMeidaQuery, + query: UploadMediaQuery, authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { var request = Mastodon.API.post( @@ -56,7 +56,7 @@ extension Mastodon.API.Media { .eraseToAnyPublisher() } - public struct UploadMeidaQuery: PostQuery, PutQuery { + public struct UploadMediaQuery: PostQuery, PutQuery { public let file: Mastodon.Query.MediaAttachment? public let thumbnail: Mastodon.Query.MediaAttachment? public let description: String? diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift index 0cdcc2e7c..f7b3147bf 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift @@ -49,6 +49,18 @@ extension Mastodon.Entity.Notification { case _other(String) + public static var knownCases: [NotificationType] { + return [ + .follow, + .followRequest, + .mention, + .reblog, + .favourite, + .poll, + .status + ] + } + public init?(rawValue: String) { switch rawValue { case "follow": self = .follow