diff --git a/.gitignore b/.gitignore index 13d57e65..a766fc62 100644 --- a/.gitignore +++ b/.gitignore @@ -118,4 +118,6 @@ xcuserdata **/xcshareddata/WorkspaceSettings.xcsettings # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,cocoapods -n \ No newline at end of file + +Localization/StringsConvertor/input +Localization/StringsConvertor/output \ No newline at end of file diff --git a/Localization/StringsConvertor/input/en_US/app.json b/Localization/StringsConvertor/input/en_US/app.json deleted file mode 100644 index 8fa05456..00000000 --- a/Localization/StringsConvertor/input/en_US/app.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "common": { - "alerts": {}, - "controls": { - "actions": { - "add": "Add", - "remove": "Remove", - "edit": "Edit", - "save": "Save", - "ok": "OK", - "confirm": "Confirm", - "continue": "Continue", - "cancel": "Cancel", - "take_photo": "Take photo", - "save_photo": "Save photo", - "sign_in": "Sign in", - "sign_up": "Sign up", - "see_more": "See More", - "preview": "Preview", - "open_in_safari": "Open in Safari" - }, - "status": { - "user_boosted": "%s boosted", - "content_warning": "content warning", - "show_post": "Show Post" - }, - "timeline": { - "load_more": "Load More" - } - }, - "countable": { - "photo": { - "single": "photo", - "multiple": "photos" - } - } - }, - "scene": { - "welcome": { - "slogan": "Social networking\nback in your hands." - }, - "server_picker": { - "title": "Pick a Server,\nany server.", - "input": { - "placeholder": "Find a server or join your own..." - } - }, - "register": { - "title": "Tell us about you.", - "input": { - "username": { - "placeholder": "username", - "duplicate_prompt": "This username is taken." - }, - "display_name": { - "placeholder": "display name" - }, - "email": { - "placeholder": "email" - }, - "password": { - "placeholder": "password", - "prompt": "Your password needs at least:", - "prompt_eight_characters": "Eight characters" - } - } - }, - "server_rules": { - "title": "Some ground rules.", - "subtitle": "These rules are set by the admins of %s.", - "prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.", - "button": { - "confirm": "I Agree" - } - }, - "home_timeline": { - "title": "Home" - }, - "public_timeline": { - "title": "Public" - } - } -} diff --git a/Localization/StringsConvertor/input/en_US/ios-infoPlist.json b/Localization/StringsConvertor/input/en_US/ios-infoPlist.json deleted file mode 100644 index 0a260c27..00000000 --- a/Localization/StringsConvertor/input/en_US/ios-infoPlist.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "NSCameraUsageDescription": "Used to take photo for toot", - "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library" -} diff --git a/Localization/StringsConvertor/output/en.lproj/Localizable.strings b/Localization/StringsConvertor/output/en.lproj/Localizable.strings deleted file mode 100644 index 75dc3999..00000000 --- a/Localization/StringsConvertor/output/en.lproj/Localizable.strings +++ /dev/null @@ -1,40 +0,0 @@ -"Common.Controls.Actions.Add" = "Add"; -"Common.Controls.Actions.Cancel" = "Cancel"; -"Common.Controls.Actions.Confirm" = "Confirm"; -"Common.Controls.Actions.Continue" = "Continue"; -"Common.Controls.Actions.Edit" = "Edit"; -"Common.Controls.Actions.Ok" = "OK"; -"Common.Controls.Actions.OpenInSafari" = "Open in Safari"; -"Common.Controls.Actions.Preview" = "Preview"; -"Common.Controls.Actions.Remove" = "Remove"; -"Common.Controls.Actions.Save" = "Save"; -"Common.Controls.Actions.SavePhoto" = "Save photo"; -"Common.Controls.Actions.SeeMore" = "See More"; -"Common.Controls.Actions.SignIn" = "Sign in"; -"Common.Controls.Actions.SignUp" = "Sign up"; -"Common.Controls.Actions.TakePhoto" = "Take photo"; -"Common.Controls.Status.ContentWarning" = "content warning"; -"Common.Controls.Status.ShowPost" = "Show Post"; -"Common.Controls.Status.UserBoosted" = "%@ boosted"; -"Common.Controls.Timeline.LoadMore" = "Load More"; -"Common.Countable.Photo.Multiple" = "photos"; -"Common.Countable.Photo.Single" = "photo"; -"Scene.HomeTimeline.Title" = "Home"; -"Scene.PublicTimeline.Title" = "Public"; -"Scene.Register.Input.DisplayName.Placeholder" = "display name"; -"Scene.Register.Input.Email.Placeholder" = "email"; -"Scene.Register.Input.Password.Placeholder" = "password"; -"Scene.Register.Input.Password.Prompt" = "Your password needs at least:"; -"Scene.Register.Input.Password.PromptEightCharacters" = "Eight characters"; -"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; -"Scene.Register.Input.Username.Placeholder" = "username"; -"Scene.Register.Title" = "Tell us about you."; -"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own..."; -"Scene.ServerPicker.Title" = "Pick a Server, -any server."; -"Scene.ServerRules.Button.Confirm" = "I Agree"; -"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; -"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; -"Scene.ServerRules.Title" = "Some ground rules."; -"Scene.Welcome.Slogan" = "Social networking -back in your hands."; \ No newline at end of file diff --git a/Localization/StringsConvertor/output/en.lproj/infoPlist.strings b/Localization/StringsConvertor/output/en.lproj/infoPlist.strings deleted file mode 100644 index 972e1a7a..00000000 --- a/Localization/StringsConvertor/output/en.lproj/infoPlist.strings +++ /dev/null @@ -1,2 +0,0 @@ -"NSCameraUsageDescription" = "Used to take photo for toot"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; \ No newline at end of file diff --git a/Localization/app.json b/Localization/app.json index cb74deb8..f79d9945 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -1,6 +1,14 @@ { "common": { - "alerts": {}, + "alerts": { + "sign_up_failure": { + "title": "Sign Up Failure" + }, + "server_error": { + "title": "Server Error" + } + + }, "controls": { "actions": { "add": "Add", @@ -21,8 +29,9 @@ }, "status": { "user_boosted": "%s boosted", - "content_warning": "content warning", - "show_post": "Show Post" + "show_post": "Show Post", + "status_content_warning": "content warning", + "media_content_warning": "Tap to reveal that may be sensitive" }, "timeline": { "load_more": "Load More" diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 27e648a5..51d06791 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -145,7 +145,7 @@ DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; }; DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; }; DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; }; - DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */; }; + DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */; }; DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; }; DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; }; DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; }; @@ -353,7 +353,7 @@ DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; - DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageView.swift; sourceTree = ""; }; + DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewContainer.swift; sourceTree = ""; }; DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = ""; }; DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; @@ -989,7 +989,7 @@ DB9D6C1325E4F97A0051B173 /* Container */ = { isa = PBXGroup; children = ( - DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */, + DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, ); path = Container; sourceTree = ""; @@ -1432,7 +1432,7 @@ DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, - DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */, + DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 30b68ce5..4ae96f9e 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -47,6 +47,10 @@ extension SceneCoordinator { case mastodonResendEmail(viewModel: MastodonResendEmailViewModel) case alertController(alertController: UIAlertController) + + #if DEBUG + case publicTimeline + #endif } } @@ -171,6 +175,12 @@ private extension SceneCoordinator { ) } viewController = alertController + #if DEBUG + case .publicTimeline: + let _viewController = PublicTimelineViewController() + _viewController.viewModel = PublicTimelineViewModel(context: appContext) + viewController = _viewController + #endif } setupDependency(for: viewController as? NeedsDependency) diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 2b34753b..c6a182b4 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -26,36 +26,32 @@ enum Item { protocol StatusContentWarningAttribute { var isStatusTextSensitive: Bool { get set } + var isStatusSensitive: Bool { get set } } extension Item { class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute { - var separatorLineStyle: SeparatorLineStyle = .indent - var isStatusTextSensitive: Bool = false + var isStatusTextSensitive: Bool + var isStatusSensitive: Bool public init( - separatorLineStyle: Item.StatusTimelineAttribute.SeparatorLineStyle = .indent, - isStatusTextSensitive: Bool + isStatusTextSensitive: Bool, + isStatusSensitive: Bool ) { - self.separatorLineStyle = separatorLineStyle self.isStatusTextSensitive = isStatusTextSensitive + self.isStatusSensitive = isStatusSensitive } static func == (lhs: Item.StatusTimelineAttribute, rhs: Item.StatusTimelineAttribute) -> Bool { - return lhs.separatorLineStyle == rhs.separatorLineStyle && - lhs.isStatusTextSensitive == rhs.isStatusTextSensitive + return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive && + lhs.isStatusSensitive == rhs.isStatusSensitive } func hash(into hasher: inout Hasher) { - hasher.combine(separatorLineStyle) hasher.combine(isStatusTextSensitive) + hasher.combine(isStatusSensitive) } - enum SeparatorLineStyle { - case indent // alignment to name label - case expand // alignment to table view two edges - case normal // alignment to readable guideline - } } } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 4cc19bdc..4fac88b4 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -94,13 +94,18 @@ extension StatusSection { // set text cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content) - // set content warning - let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? (toot.reblog ?? toot).sensitive + // set status text content warning + let spoilerText = (toot.reblog ?? toot).spoilerText ?? "" + let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? !spoilerText.isEmpty + cell.statusView.isStatusTextSensitive = isStatusTextSensitive cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive) - cell.statusView.contentWarningTitle.text = (toot.reblog ?? toot).spoilerText.flatMap { spoilerText in - guard !spoilerText.isEmpty else { return nil } - return L10n.Common.Controls.Status.contentWarning + ": \(spoilerText)" - } ?? L10n.Common.Controls.Status.contentWarning + cell.statusView.contentWarningTitle.text = { + if spoilerText.isEmpty { + return L10n.Common.Controls.Status.statusContentWarning + } else { + return L10n.Common.Controls.Status.statusContentWarning + ": \(spoilerText)" + } + }() // prepare media attachments let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } @@ -127,14 +132,14 @@ extension StatusSection { }() if mosiacImageViewModel.metas.count == 1 { let meta = mosiacImageViewModel.metas[0] - let imageView = cell.statusView.mosaicImageView.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) + let imageView = cell.statusView.statusMosaicImageView.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) imageView.af.setImage( withURL: meta.url, placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2) ) } else { - let imageViews = cell.statusView.mosaicImageView.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) + let imageViews = cell.statusView.statusMosaicImageView.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) for (i, imageView) in imageViews.enumerated() { let meta = mosiacImageViewModel.metas[i] imageView.af.setImage( @@ -144,7 +149,10 @@ extension StatusSection { ) } } - cell.statusView.mosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty + cell.statusView.statusMosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty + let isStatusSensitive = statusContentWarningAttribute?.isStatusSensitive ?? (toot.reblog ?? toot).sensitive + cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil + cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 // toolbar let replyCountTitle: String = { diff --git a/Mastodon/Extension/UIAlertController.swift b/Mastodon/Extension/UIAlertController.swift index 7abe20cb..83c0ff55 100644 --- a/Mastodon/Extension/UIAlertController.swift +++ b/Mastodon/Extension/UIAlertController.swift @@ -9,26 +9,34 @@ import UIKit // https://nshipster.com/swift-foundation-error-protocols/ extension UIAlertController { convenience init( - _ error: Error, + for error: Error, + title: String?, preferredStyle: UIAlertController.Style ) { - let title: String + let _title: String let message: String? if let error = error as? LocalizedError { - title = error.errorDescription ?? "Unknown Error" - message = [ + var messages: [String?] = [] + if let title = title { + _title = title + messages.append(error.errorDescription) + } else { + _title = error.errorDescription ?? "Error" + } + messages.append(contentsOf: [ error.failureReason, error.recoverySuggestion - ] - .compactMap { $0 } - .joined(separator: " ") + ]) + message = messages + .compactMap { $0 } + .joined(separator: " ") } else { - title = "Internal Error" + _title = "Internal Error" message = error.localizedDescription } self.init( - title: title, + title: _title, message: message, preferredStyle: preferredStyle ) diff --git a/Mastodon/Extension/UIView.swift b/Mastodon/Extension/UIView.swift index 7e1ba379..d9e3af5b 100644 --- a/Mastodon/Extension/UIView.swift +++ b/Mastodon/Extension/UIView.swift @@ -20,10 +20,6 @@ extension UIView { return 1.0 / view.traitCollection.displayScale } - static var floatyButtonBottomMargin: CGFloat { - return 16 - } - } // MARK: - Convinience view appearance modification method diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index bfb8db80..fee851c2 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -12,6 +12,16 @@ import Foundation internal enum L10n { internal enum Common { + internal enum Alerts { + internal enum ServerError { + /// Server Error + internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title") + } + internal enum SignUpFailure { + /// Sign Up Failure + internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title") + } + } internal enum Controls { internal enum Actions { /// Add @@ -46,10 +56,12 @@ internal enum L10n { internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") } internal enum Status { - /// content warning - internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning") + /// Tap to reveal that may be sensitive + internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning") /// Show Post internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost") + /// content warning + internal static let statusContentWarning = L10n.tr("Localizable", "Common.Controls.Status.StatusContentWarning") /// %@ boosted internal static func userBoosted(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1)) diff --git a/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift b/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift index bc90ab1e..4f32be54 100644 --- a/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift +++ b/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift @@ -23,6 +23,13 @@ extension LoadMoreConfigurableTableViewContainer { func handleScrollViewDidScroll(_ scrollView: UIScrollView) { guard scrollView === loadMoreConfigurableTableView else { return } + // check if current scroll position is the bottom of table + let contentOffsetY = loadMoreConfigurableTableView.contentOffset.y + let bottomVisiblePageContentOffsetY = loadMoreConfigurableTableView.contentSize.height - (1.5 * loadMoreConfigurableTableView.visibleSize.height) + guard contentOffsetY > bottomVisiblePageContentOffsetY else { + return + } + let cells = loadMoreConfigurableTableView.visibleCells.compactMap { $0 as? BottomLoaderTableViewCell } guard let loaderTableViewCell = cells.first else { return } diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift index 9c6127b0..336434ff 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift @@ -43,3 +43,39 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { } } + +extension StatusTableViewCellDelegate where Self: StatusProvider { + + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { + + } + + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { + guard let diffableDataSource = self.tableViewDiffableDataSource else { return } + item(for: cell, indexPath: nil) + .receive(on: DispatchQueue.main) + .sink { [weak self] item in + guard let _ = self else { return } + guard let item = item else { return } + switch item { + case .homeTimelineIndex(_, let attribute): + attribute.isStatusSensitive = false + case .toot(_, let attribute): + attribute.isStatusSensitive = false + default: + return + } + + var snapshot = diffableDataSource.snapshot() + snapshot.reloadItems([item]) + UIView.animate(withDuration: 0.33) { + cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = nil + cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = 0.0 + } completion: { _ in + diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + } + .store(in: &cell.disposeBag) + } + +} diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index eaf202c0..89446156 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -85,7 +85,7 @@ extension StatusProviderFacade { os_log("%{public}s[%{public}ld], %{public}s: [Like] update local toot like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike") } receiveCompletion: { completion in switch completion { - case .failure(let error): + case .failure: // TODO: handle error break case .finished: diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 1311dcc6..08f872c8 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -1,3 +1,5 @@ +"Common.Alerts.ServerError.Title" = "Server Error"; +"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure"; "Common.Controls.Actions.Add" = "Add"; "Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Confirm" = "Confirm"; @@ -13,8 +15,9 @@ "Common.Controls.Actions.SignIn" = "Sign in"; "Common.Controls.Actions.SignUp" = "Sign up"; "Common.Controls.Actions.TakePhoto" = "Take photo"; -"Common.Controls.Status.ContentWarning" = "content warning"; +"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive"; "Common.Controls.Status.ShowPost" = "Show Post"; +"Common.Controls.Status.StatusContentWarning" = "content warning"; "Common.Controls.Status.UserBoosted" = "%@ boosted"; "Common.Controls.Timeline.LoadMore" = "Load More"; "Common.Countable.Photo.Multiple" = "photos"; diff --git a/Mastodon/Scene/Authentication/AuthenticationViewController.swift b/Mastodon/Scene/Authentication/AuthenticationViewController.swift index 5236bb92..090d8423 100644 --- a/Mastodon/Scene/Authentication/AuthenticationViewController.swift +++ b/Mastodon/Scene/Authentication/AuthenticationViewController.swift @@ -193,7 +193,7 @@ extension AuthenticationViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } - let alertController = UIAlertController(error, preferredStyle: .alert) + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) alertController.addAction(okAction) self.coordinator.present( diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index aaded394..ad2c1976 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -398,7 +398,7 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } - let alertController = UIAlertController(error, preferredStyle: .alert) + let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) self.coordinator.present( diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 84c0885d..db6ddfa6 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -17,6 +17,10 @@ extension HomeTimelineViewController { identifier: nil, options: .displayInline, children: [ + UIAction(title: "Show Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showPublicTimelineAction(action) + }, UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in guard let self = self else { return } self.signOutAction(action) @@ -29,6 +33,10 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { + @objc private func showPublicTimelineAction(_ sender: UIAction) { + coordinator.present(scene: .publicTimeline, from: self, transition: .show) + } + @objc private func signOutAction(_ sender: UIAction) { guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 153f4613..125ce854 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -175,23 +175,17 @@ extension HomeTimelineViewController { // MARK: - UIScrollViewDelegate extension HomeTimelineViewController { func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard scrollView === tableView else { return } - let cells = tableView.visibleCells.compactMap { $0 as? TimelineBottomLoaderTableViewCell } - guard let loaderTableViewCell = cells.first else { return } - - if let tabBar = tabBarController?.tabBar, let window = view.window { - let loaderTableViewCellFrameInWindow = tableView.convert(loaderTableViewCell.frame, to: nil) - let windowHeight = window.frame.height - let loaderAppear = (loaderTableViewCellFrameInWindow.origin.y + 0.8 * loaderTableViewCell.frame.height) < (windowHeight - tabBar.frame.height) - if loaderAppear { - viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) - } - } else { - viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) - } + handleScrollViewDidScroll(scrollView) } } +extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer { + typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell + typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading + var loadMoreConfigurableTableView: UITableView { return tableView } + var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } +} + // MARK: - UITableViewDelegate extension HomeTimelineViewController: UITableViewDelegate { @@ -206,14 +200,7 @@ extension HomeTimelineViewController: UITableViewDelegate { return ceil(frame.height) } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if let cell = cell as? StatusTableViewCell { - DispatchQueue.main.async { - cell.statusView.drawContentWarningImageView() - } - } - } + } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate @@ -233,7 +220,7 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate viewModel.loadMiddleSateMachineList .receive(on: DispatchQueue.main) .sink { [weak self] ids in - guard let self = self else { return } + guard let _ = self else { return } if let stateMachine = ids[upperTimelineIndexObjectID] { guard let state = stateMachine.currentState else { assertionFailure() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 0091f06b..d5345de4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -83,23 +83,24 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { var newTimelineItems: [Item] = [] for (i, timelineIndex) in timelineIndexes.enumerated() { - let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: timelineIndex.toot.sensitive) + let toot = timelineIndex.toot.reblog ?? timelineIndex.toot + let isStatusTextSensitive: Bool = { + guard let spoilerText = toot.spoilerText, !spoilerText.isEmpty else { return false } + return true + }() + let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: toot.sensitive) // append new item into snapshot newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute)) let isLast = i == timelineIndexes.count - 1 switch (isLast, timelineIndex.hasMore) { - case (true, false): - attribute.separatorLineStyle = .normal case (false, true): - attribute.separatorLineStyle = .expand newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID)) case (true, true): - attribute.separatorLineStyle = .normal shouldAddBottomLoader = true - case (false, false): - attribute.separatorLineStyle = .indent + default: + break } } // end for diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index c946ffef..a556854e 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -112,7 +112,7 @@ extension MainTabBarController { case .implicit: break case .explicit: - let alertController = UIAlertController(error, preferredStyle: .alert) + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) alertController.addAction(okAction) coordinator.present( diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index 62993bfb..dd5ffc84 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -13,7 +13,7 @@ import GameplayKit import os.log import UIKit -final class PublicTimelineViewController: UIViewController, NeedsDependency, StatusTableViewCellDelegate { +final class PublicTimelineViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -42,6 +42,7 @@ extension PublicTimelineViewController { override func viewDidLoad() { super.viewDidLoad() + title = "Public" view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color tableView.refreshControl = refreshControl @@ -202,3 +203,6 @@ extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegat } } } + +// MARK: - StatusTableViewCellDelegate +extension PublicTimelineViewController: StatusTableViewCellDelegate { } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift index 26638578..f9c92fa0 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift @@ -58,7 +58,12 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate { var items = [Item]() for (_, toot) in indexTootTuples { - let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: toot.sensitive) + let targetToot = toot.reblog ?? toot + let isStatusTextSensitive: Bool = { + guard let spoilerText = targetToot.spoilerText, !spoilerText.isEmpty else { return false } + return true + }() + let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetToot.sensitive) items.append(Item.toot(objectID: toot.objectID, attribute: attribute)) if tootIDsWhichHasGap.contains(toot.id) { items.append(Item.publicMiddleLoader(tootID: toot.id)) diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageView.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift similarity index 64% rename from Mastodon/Scene/Share/View/Container/MosaicImageView.swift rename to Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 5f8c877d..5240d4e2 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageView.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -1,5 +1,5 @@ // -// MosaicImageView.swift +// MosaicImageViewContainer.swift // Mastodon // // Created by Cirno MainasuK on 2021-2-23. @@ -9,32 +9,44 @@ import os.log import func AVFoundation.AVMakeRect import UIKit -protocol MosaicImageViewPresentable: class { - var mosaicImageView: MosaicImageView { get } +protocol MosaicImageViewContainerPresentable: class { + var mosaicImageViewContainer: MosaicImageViewContainer { get } } -protocol MosaicImageViewDelegate: class { - func mosaicImageView(_ mosaicImageView: MosaicImageView, didTapImageView imageView: UIImageView, atIndex index: Int) +protocol MosaicImageViewContainerDelegate: class { + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + } -final class MosaicImageView: UIView { +final class MosaicImageViewContainer: UIView { static let cornerRadius: CGFloat = 4 + static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) - weak var delegate: MosaicImageViewDelegate? + weak var delegate: MosaicImageViewContainerDelegate? let container = UIStackView() - var imageViews = [UIImageView]() { + var imageViews: [UIImageView] = [] { didSet { imageViews.forEach { imageView in imageView.isUserInteractionEnabled = true let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer - tapGesture.addTarget(self, action: #selector(MosaicImageView.photoTapGestureRecognizerHandler(_:))) + tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:))) imageView.addGestureRecognizer(tapGesture) } } } - + let blurVisualEffectView = UIVisualEffectView(effect: MosaicImageViewContainer.blurVisualEffect) + let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicImageViewContainer.blurVisualEffect)) + let contentWarningLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15)) + label.text = L10n.Common.Controls.Status.mediaContentWarning + label.textAlignment = .center + return label + }() + private var containerHeightLayoutConstraint: NSLayoutConstraint! override init(frame: CGRect) { @@ -49,10 +61,12 @@ final class MosaicImageView: UIView { } -extension MosaicImageView { +extension MosaicImageViewContainer { private func _init() { container.translatesAutoresizingMaskIntoConstraints = false + container.axis = .horizontal + container.distribution = .fillEqually addSubview(container) containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) NSLayoutConstraint.activate([ @@ -63,13 +77,37 @@ extension MosaicImageView { containerHeightLayoutConstraint ]) - container.axis = .horizontal - container.distribution = .fillEqually + // add blur visual effect view in the setup method + blurVisualEffectView.layer.masksToBounds = true + blurVisualEffectView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius + blurVisualEffectView.layer.cornerCurve = .continuous + + vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView) + NSLayoutConstraint.activate([ + vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.topAnchor), + vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.leadingAnchor), + vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.trailingAnchor), + vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.bottomAnchor), + ]) + + contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false + vibrancyVisualEffectView.contentView.addSubview(contentWarningLabel) + NSLayoutConstraint.activate([ + contentWarningLabel.leadingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.leadingAnchor), + contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor), + contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor), + ]) + + blurVisualEffectView.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer + tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.visualEffectViewTapGestureRecognizerHandler(_:))) + blurVisualEffectView.addGestureRecognizer(tapGesture) } } -extension MosaicImageView { +extension MosaicImageViewContainer { func reset() { container.arrangedSubviews.forEach { subview in @@ -79,6 +117,9 @@ extension MosaicImageView { container.subviews.forEach { subview in subview.removeFromSuperview() } + blurVisualEffectView.removeFromSuperview() + blurVisualEffectView.effect = MosaicImageViewContainer.blurVisualEffect + vibrancyVisualEffectView.alpha = 1.0 imageViews = [] container.spacing = 1 @@ -99,7 +140,8 @@ extension MosaicImageView { let imageView = UIImageView() imageViews.append(imageView) imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = MosaicImageView.cornerRadius + imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius + imageView.layer.cornerCurve = .continuous imageView.contentMode = .scaleAspectFill imageView.translatesAutoresizingMaskIntoConstraints = false @@ -112,6 +154,15 @@ extension MosaicImageView { ]) containerHeightLayoutConstraint.constant = floor(rect.height) containerHeightLayoutConstraint.isActive = true + + blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + addSubview(blurVisualEffectView) + NSLayoutConstraint.activate([ + blurVisualEffectView.topAnchor.constraint(equalTo: imageView.topAnchor), + blurVisualEffectView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), + blurVisualEffectView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), + blurVisualEffectView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), + ]) return imageView } @@ -142,7 +193,7 @@ extension MosaicImageView { self.imageViews.append(contentsOf: imageViews) imageViews.forEach { imageView in imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = MosaicImageView.cornerRadius + imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius imageView.layer.cornerCurve = .continuous imageView.contentMode = .scaleAspectFill } @@ -191,18 +242,34 @@ extension MosaicImageView { } } + blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + addSubview(blurVisualEffectView) + NSLayoutConstraint.activate([ + blurVisualEffectView.topAnchor.constraint(equalTo: container.topAnchor), + blurVisualEffectView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + blurVisualEffectView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + blurVisualEffectView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + return imageViews } + } -extension MosaicImageView { - +extension MosaicImageViewContainer { + + @objc private func visualEffectViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.mosaicImageViewContainer(self, didTapContentWarningVisualEffectView: blurVisualEffectView) + } + @objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { guard let imageView = sender.view as? UIImageView else { return } guard let index = imageViews.firstIndex(of: imageView) else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: tap photo at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index) - delegate?.mosaicImageView(self, didTapImageView: imageView, atIndex: index) + delegate?.mosaicImageViewContainer(self, didTapImageView: imageView, atIndex: index) } + } #if DEBUG && canImport(SwiftUI) @@ -218,7 +285,7 @@ struct MosaicImageView_Previews: PreviewProvider { static var previews: some View { Group { UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let image = images[3] let imageView = view.setupImageView( aspectRatio: image.size, @@ -230,7 +297,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 400)) .previewDisplayName("Portrait - one image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let image = images[1] let imageView = view.setupImageView( aspectRatio: image.size, @@ -245,7 +312,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 400)) .previewDisplayName("Landscape - one image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let images = self.images.prefix(2) let imageViews = view.setupImageViews(count: images.count, maxHeight: 162) for (i, imageView) in imageViews.enumerated() { @@ -256,7 +323,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 200)) .previewDisplayName("two image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let images = self.images.prefix(3) let imageViews = view.setupImageViews(count: images.count, maxHeight: 162) for (i, imageView) in imageViews.enumerated() { @@ -267,7 +334,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 200)) .previewDisplayName("three image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let images = self.images.prefix(4) let imageViews = view.setupImageViews(count: images.count, maxHeight: 162) for (i, imageView) in imageViews.enumerated() { diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index fc502597..be754ed8 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -22,6 +22,7 @@ final class StatusView: UIView { static let contentWarningBlurRadius: CGFloat = 12 weak var delegate: StatusViewDelegate? + var isStatusTextSensitive = false let headerContainerStackView = UIStackView() @@ -88,7 +89,7 @@ final class StatusView: UIView { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) label.textColor = Asset.Colors.Label.primary.color - label.text = L10n.Common.Controls.Status.contentWarning + label.text = L10n.Common.Controls.Status.statusContentWarning return label }() let contentWarningActionButton: UIButton = { @@ -98,12 +99,12 @@ final class StatusView: UIView { button.setTitle(L10n.Common.Controls.Status.showPost, for: .normal) return button }() - let mosaicImageView = MosaicImageView() + let statusMosaicImageView = MosaicImageViewContainer() // do not use visual effect view due to we blur text only without background let contentWarningBlurContentImageView: UIImageView = { let imageView = UIImageView() - imageView.backgroundColor = .secondarySystemGroupedBackground + imageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color imageView.layer.masksToBounds = false return imageView }() @@ -126,6 +127,15 @@ final class StatusView: UIView { super.init(coder: coder) _init() } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + // update blur image when interface style changed + if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle { + drawContentWarningImageView() + } + } } @@ -247,7 +257,7 @@ extension StatusView { ]) statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle) statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton) - statusContainerStackView.addArrangedSubview(mosaicImageView) + statusContainerStackView.addArrangedSubview(statusMosaicImageView) // action toolbar container @@ -255,7 +265,7 @@ extension StatusView { actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical) headerContainerStackView.isHidden = true - mosaicImageView.isHidden = true + statusMosaicImageView.isHidden = true contentWarningBlurContentImageView.isHidden = true statusContentWarningContainerStackView.isHidden = true statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false @@ -272,7 +282,9 @@ extension StatusView { } func drawContentWarningImageView() { - guard activeTextLabel.frame != .zero, let text = activeTextLabel.text, !text.isEmpty else { + guard activeTextLabel.frame != .zero, + isStatusTextSensitive, + let text = activeTextLabel.text, !text.isEmpty else { cleanUpContentWarning() return } @@ -331,7 +343,7 @@ struct StatusView_Previews: PreviewProvider { } .previewLayout(.fixed(width: 375, height: 200)) UIViewPreview(width: 375) { - let statusView = StatusView() + let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500)) statusView.configure( with: AvatarConfigurableViewConfiguration( avatarImageURL: nil, @@ -339,9 +351,20 @@ struct StatusView_Previews: PreviewProvider { ) ) statusView.headerContainerStackView.isHidden = false + statusView.isStatusTextSensitive = true + statusView.setNeedsLayout() + statusView.layoutIfNeeded() + statusView.drawContentWarningImageView() + statusView.updateContentWarningDisplay(isHidden: false) + let images = MosaicImageView_Previews.images + let imageViews = statusView.statusMosaicImageView.setupImageViews(count: 4, maxHeight: 162) + for (i, imageView) in imageViews.enumerated() { + imageView.image = images[i] + } + statusView.statusMosaicImageView.isHidden = false return statusView } - .previewLayout(.fixed(width: 375, height: 200)) + .previewLayout(.fixed(width: 375, height: 380)) } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 3c968f79..572f23e0 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -14,10 +14,14 @@ import Combine protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + } final class StatusTableViewCell: UITableViewCell { + static let bottomPaddingHeight: CGFloat = 10 weak var delegate: StatusTableViewCellDelegate? @@ -28,6 +32,7 @@ final class StatusTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + statusView.isStatusTextSensitive = false statusView.cleanUpContentWarning() disposeBag.removeAll() observations.removeAll() @@ -43,6 +48,13 @@ final class StatusTableViewCell: UITableViewCell { _init() } + override func layoutSubviews() { + super.layoutSubviews() + DispatchQueue.main.async { + self.statusView.drawContentWarningImageView() + } + } + } extension StatusTableViewCell { @@ -50,6 +62,7 @@ extension StatusTableViewCell { private func _init() { selectionStyle = .none backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color statusView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(statusView) @@ -67,12 +80,13 @@ extension StatusTableViewCell { bottomPaddingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), bottomPaddingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - bottomPaddingView.heightAnchor.constraint(equalToConstant: 10).priority(.defaultHigh), + bottomPaddingView.heightAnchor.constraint(equalToConstant: StatusTableViewCell.bottomPaddingHeight).priority(.defaultHigh), ]) + bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color statusView.delegate = self + statusView.statusMosaicImageView.delegate = self statusView.actionToolbarContainer.delegate = self - bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color } } @@ -84,6 +98,19 @@ extension StatusTableViewCell: StatusViewDelegate { } } +// MARK: - MosaicImageViewDelegate +extension StatusTableViewCell: MosaicImageViewContainerDelegate { + + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { + delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index) + } + + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { + delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapContentWarningVisualEffectView: visualEffectView) + } + +} + // MARK: - ActionToolbarContainerDelegate extension StatusTableViewCell: ActionToolbarContainerDelegate { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) { diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift index 2dfbd625..7fe4c0a7 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift @@ -17,3 +17,20 @@ final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { activityIndicatorView.startAnimating() } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct TimelineBottomLoaderTableViewCell_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + TimelineBottomLoaderTableViewCell() + } + .previewLayout(.fixed(width: 375, height: 100)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift index 676f44ff..6aa19524 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -10,7 +10,9 @@ import Combine class TimelineLoaderTableViewCell: UITableViewCell { - static let cellHeight: CGFloat = 48 + static let cellHeight: CGFloat = 44 + TimelineLoaderTableViewCell.extraTopPadding + TimelineLoaderTableViewCell.bottomPadding + static let extraTopPadding: CGFloat = 0 // the status cell already has 10pt bottom padding + static let bottomPadding: CGFloat = StatusTableViewCell.bottomPaddingHeight + TimelineLoaderTableViewCell.extraTopPadding // make balance var disposeBag = Set() @@ -50,18 +52,18 @@ class TimelineLoaderTableViewCell: UITableViewCell { loadMoreButton.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(loadMoreButton) NSLayoutConstraint.activate([ - loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TimelineLoaderTableViewCell.extraTopPadding), loadMoreButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), contentView.readableContentGuide.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: 8), - loadMoreButton.heightAnchor.constraint(equalToConstant: TimelineLoaderTableViewCell.cellHeight - 2 * 8).priority(.defaultHigh), + contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: TimelineLoaderTableViewCell.bottomPadding), + loadMoreButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false addSubview(activityIndicatorView) NSLayoutConstraint.activate([ - activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor), - activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor), + activityIndicatorView.centerXAnchor.constraint(equalTo: loadMoreButton.centerXAnchor), + activityIndicatorView.centerYAnchor.constraint(equalTo: loadMoreButton.centerYAnchor), ]) loadMoreButton.isHidden = true diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift index d768865d..16ab241f 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift @@ -23,16 +23,6 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { backgroundColor = .clear - let separatorLine = UIView.separatorLine - separatorLine.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separatorLine) - NSLayoutConstraint.activate([ - separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: separatorLine.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: separatorLine.bottomAnchor), - separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: separatorLine)) - ]) - loadMoreButton.isHidden = false loadMoreButton.setImage(Asset.Arrows.arrowTriangle2Circlepath.image.withRenderingMode(.alwaysTemplate), for: .normal) loadMoreButton.setInsets(forContentPadding: .zero, imageTitlePadding: 4) @@ -46,3 +36,20 @@ extension TimelineMiddleLoaderTableViewCell { delegate?.timelineMiddleLoaderTableViewCell(self, loadMoreButtonDidPressed: sender) } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct TimelineMiddleLoaderTableViewCell_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + TimelineMiddleLoaderTableViewCell() + } + .previewLayout(.fixed(width: 375, height: 100)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Welcome/WelcomeViewController.swift index 99aa89f9..3c95fa4f 100644 --- a/Mastodon/Scene/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Welcome/WelcomeViewController.swift @@ -14,7 +14,12 @@ final class WelcomeViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } #if DEBUG - let authenticationViewController = AuthenticationViewController() + lazy var authenticationViewController: AuthenticationViewController = { + let authenticationViewController = AuthenticationViewController() + authenticationViewController.context = context + authenticationViewController.coordinator = coordinator + return authenticationViewController + }() #endif let logoImageView: UIImageView = { @@ -105,8 +110,6 @@ extension WelcomeViewController { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) #if DEBUG - authenticationViewController.context = context - authenticationViewController.coordinator = coordinator authenticationViewController.viewModel = AuthenticationViewModel(context: context, coordinator: coordinator, isAuthenticationExist: true) authenticationViewController.viewModel.domain.value = "pawoo.net" let _ = authenticationViewController.view // trigger view load diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index 8dd4839a..34bd3f0e 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -139,7 +139,7 @@ extension APIService { return APIService.Persist.persistTimeline( managedObjectContext: self.backgroundManagedObjectContext, domain: mastodonAuthenticationBox.domain, - query: query as! TimelineQueryType, + query: query, response: response, persistType: .likeList, requestMastodonUserID: requestMastodonUserID, diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift index 5a672e01..70b86e61 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift @@ -13,14 +13,14 @@ extension Mastodon.Entity { /// - Since: 2.8.0 /// - Version: 3.3.0 /// # Last Update - /// 2021/2/4 + /// 2021/2/24 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/poll/) public struct Poll: Codable { public typealias ID = String public let id: ID - public let expiresAt: Date + public let expiresAt: Date? // if nil the poll does not end public let expired: Bool public let multiple: Bool public let votesCount: Int