forked from zelo72/mastodon-ios
Merge branch 'feat/serverPicker' of https://github.com/tootsuite/mastodon-ios into feat/serverPicker
This commit is contained in:
commit
ec96690ac9
|
@ -118,4 +118,6 @@ xcuserdata
|
||||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,cocoapods
|
# End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,cocoapods
|
||||||
n
|
|
||||||
|
Localization/StringsConvertor/input
|
||||||
|
Localization/StringsConvertor/output
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"NSCameraUsageDescription": "Used to take photo for toot",
|
|
||||||
"NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
"NSCameraUsageDescription" = "Used to take photo for toot";
|
|
||||||
"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library";
|
|
|
@ -1,6 +1,14 @@
|
||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"alerts": {},
|
"alerts": {
|
||||||
|
"sign_up_failure": {
|
||||||
|
"title": "Sign Up Failure"
|
||||||
|
},
|
||||||
|
"server_error": {
|
||||||
|
"title": "Server Error"
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
|
@ -21,8 +29,9 @@
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"user_boosted": "%s boosted",
|
"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": {
|
"timeline": {
|
||||||
"load_more": "Load More"
|
"load_more": "Load More"
|
||||||
|
|
|
@ -145,7 +145,7 @@
|
||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
||||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.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 */; };
|
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; };
|
||||||
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
|
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
|
||||||
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
|
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
|
||||||
|
@ -354,7 +354,7 @@
|
||||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageView.swift; sourceTree = "<group>"; };
|
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewContainer.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = "<group>"; };
|
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
|
@ -998,7 +998,7 @@
|
||||||
DB9D6C1325E4F97A0051B173 /* Container */ = {
|
DB9D6C1325E4F97A0051B173 /* Container */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */,
|
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
|
||||||
);
|
);
|
||||||
path = Container;
|
path = Container;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1438,7 +1438,7 @@
|
||||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||||
DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */,
|
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */,
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||||
|
|
|
@ -46,6 +46,10 @@ extension SceneCoordinator {
|
||||||
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
|
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
|
||||||
|
|
||||||
case alertController(alertController: UIAlertController)
|
case alertController(alertController: UIAlertController)
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
case publicTimeline
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,6 +170,12 @@ private extension SceneCoordinator {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewController = alertController
|
viewController = alertController
|
||||||
|
#if DEBUG
|
||||||
|
case .publicTimeline:
|
||||||
|
let _viewController = PublicTimelineViewController()
|
||||||
|
_viewController.viewModel = PublicTimelineViewModel(context: appContext)
|
||||||
|
viewController = _viewController
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependency(for: viewController as? NeedsDependency)
|
setupDependency(for: viewController as? NeedsDependency)
|
||||||
|
|
|
@ -26,36 +26,32 @@ enum Item {
|
||||||
|
|
||||||
protocol StatusContentWarningAttribute {
|
protocol StatusContentWarningAttribute {
|
||||||
var isStatusTextSensitive: Bool { get set }
|
var isStatusTextSensitive: Bool { get set }
|
||||||
|
var isStatusSensitive: Bool { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Item {
|
extension Item {
|
||||||
class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute {
|
class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute {
|
||||||
var separatorLineStyle: SeparatorLineStyle = .indent
|
var isStatusTextSensitive: Bool
|
||||||
var isStatusTextSensitive: Bool = false
|
var isStatusSensitive: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
separatorLineStyle: Item.StatusTimelineAttribute.SeparatorLineStyle = .indent,
|
isStatusTextSensitive: Bool,
|
||||||
isStatusTextSensitive: Bool
|
isStatusSensitive: Bool
|
||||||
) {
|
) {
|
||||||
self.separatorLineStyle = separatorLineStyle
|
|
||||||
self.isStatusTextSensitive = isStatusTextSensitive
|
self.isStatusTextSensitive = isStatusTextSensitive
|
||||||
|
self.isStatusSensitive = isStatusSensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: Item.StatusTimelineAttribute, rhs: Item.StatusTimelineAttribute) -> Bool {
|
static func == (lhs: Item.StatusTimelineAttribute, rhs: Item.StatusTimelineAttribute) -> Bool {
|
||||||
return lhs.separatorLineStyle == rhs.separatorLineStyle &&
|
return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive &&
|
||||||
lhs.isStatusTextSensitive == rhs.isStatusTextSensitive
|
lhs.isStatusSensitive == rhs.isStatusSensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(separatorLineStyle)
|
|
||||||
hasher.combine(isStatusTextSensitive)
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,13 +94,18 @@ extension StatusSection {
|
||||||
// set text
|
// set text
|
||||||
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
||||||
|
|
||||||
// set content warning
|
// set status text content warning
|
||||||
let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? (toot.reblog ?? toot).sensitive
|
let spoilerText = (toot.reblog ?? toot).spoilerText ?? ""
|
||||||
|
let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? !spoilerText.isEmpty
|
||||||
|
cell.statusView.isStatusTextSensitive = isStatusTextSensitive
|
||||||
cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive)
|
cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive)
|
||||||
cell.statusView.contentWarningTitle.text = (toot.reblog ?? toot).spoilerText.flatMap { spoilerText in
|
cell.statusView.contentWarningTitle.text = {
|
||||||
guard !spoilerText.isEmpty else { return nil }
|
if spoilerText.isEmpty {
|
||||||
return L10n.Common.Controls.Status.contentWarning + ": \(spoilerText)"
|
return L10n.Common.Controls.Status.statusContentWarning
|
||||||
} ?? L10n.Common.Controls.Status.contentWarning
|
} else {
|
||||||
|
return L10n.Common.Controls.Status.statusContentWarning + ": \(spoilerText)"
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// prepare media attachments
|
// prepare media attachments
|
||||||
let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
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 {
|
if mosiacImageViewModel.metas.count == 1 {
|
||||||
let meta = mosiacImageViewModel.metas[0]
|
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(
|
imageView.af.setImage(
|
||||||
withURL: meta.url,
|
withURL: meta.url,
|
||||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
imageTransition: .crossDissolve(0.2)
|
imageTransition: .crossDissolve(0.2)
|
||||||
)
|
)
|
||||||
} else {
|
} 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() {
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
let meta = mosiacImageViewModel.metas[i]
|
let meta = mosiacImageViewModel.metas[i]
|
||||||
imageView.af.setImage(
|
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
|
// toolbar
|
||||||
let replyCountTitle: String = {
|
let replyCountTitle: String = {
|
||||||
|
|
|
@ -9,26 +9,34 @@ import UIKit
|
||||||
// https://nshipster.com/swift-foundation-error-protocols/
|
// https://nshipster.com/swift-foundation-error-protocols/
|
||||||
extension UIAlertController {
|
extension UIAlertController {
|
||||||
convenience init(
|
convenience init(
|
||||||
_ error: Error,
|
for error: Error,
|
||||||
|
title: String?,
|
||||||
preferredStyle: UIAlertController.Style
|
preferredStyle: UIAlertController.Style
|
||||||
) {
|
) {
|
||||||
let title: String
|
let _title: String
|
||||||
let message: String?
|
let message: String?
|
||||||
if let error = error as? LocalizedError {
|
if let error = error as? LocalizedError {
|
||||||
title = error.errorDescription ?? "Unknown Error"
|
var messages: [String?] = []
|
||||||
message = [
|
if let title = title {
|
||||||
|
_title = title
|
||||||
|
messages.append(error.errorDescription)
|
||||||
|
} else {
|
||||||
|
_title = error.errorDescription ?? "Error"
|
||||||
|
}
|
||||||
|
messages.append(contentsOf: [
|
||||||
error.failureReason,
|
error.failureReason,
|
||||||
error.recoverySuggestion
|
error.recoverySuggestion
|
||||||
]
|
])
|
||||||
.compactMap { $0 }
|
message = messages
|
||||||
.joined(separator: " ")
|
.compactMap { $0 }
|
||||||
|
.joined(separator: " ")
|
||||||
} else {
|
} else {
|
||||||
title = "Internal Error"
|
_title = "Internal Error"
|
||||||
message = error.localizedDescription
|
message = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
self.init(
|
self.init(
|
||||||
title: title,
|
title: _title,
|
||||||
message: message,
|
message: message,
|
||||||
preferredStyle: preferredStyle
|
preferredStyle: preferredStyle
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,10 +20,6 @@ extension UIView {
|
||||||
return 1.0 / view.traitCollection.displayScale
|
return 1.0 / view.traitCollection.displayScale
|
||||||
}
|
}
|
||||||
|
|
||||||
static var floatyButtonBottomMargin: CGFloat {
|
|
||||||
return 16
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Convinience view appearance modification method
|
// MARK: - Convinience view appearance modification method
|
||||||
|
|
|
@ -12,6 +12,16 @@ import Foundation
|
||||||
internal enum L10n {
|
internal enum L10n {
|
||||||
|
|
||||||
internal enum Common {
|
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 Controls {
|
||||||
internal enum Actions {
|
internal enum Actions {
|
||||||
/// Add
|
/// Add
|
||||||
|
@ -46,10 +56,12 @@ internal enum L10n {
|
||||||
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
|
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
|
||||||
}
|
}
|
||||||
internal enum Status {
|
internal enum Status {
|
||||||
/// content warning
|
/// Tap to reveal that may be sensitive
|
||||||
internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning")
|
internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning")
|
||||||
/// Show Post
|
/// Show Post
|
||||||
internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost")
|
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
|
/// %@ boosted
|
||||||
internal static func userBoosted(_ p1: Any) -> String {
|
internal static func userBoosted(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1))
|
return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1))
|
||||||
|
|
|
@ -23,6 +23,13 @@ extension LoadMoreConfigurableTableViewContainer {
|
||||||
func handleScrollViewDidScroll(_ scrollView: UIScrollView) {
|
func handleScrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
guard scrollView === loadMoreConfigurableTableView else { return }
|
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 }
|
let cells = loadMoreConfigurableTableView.visibleCells.compactMap { $0 as? BottomLoaderTableViewCell }
|
||||||
guard let loaderTableViewCell = cells.first else { return }
|
guard let loaderTableViewCell = cells.first else { return }
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
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
|
} receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure:
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
break
|
break
|
||||||
case .finished:
|
case .finished:
|
||||||
|
|
|
@ -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.Add" = "Add";
|
||||||
"Common.Controls.Actions.Cancel" = "Cancel";
|
"Common.Controls.Actions.Cancel" = "Cancel";
|
||||||
"Common.Controls.Actions.Confirm" = "Confirm";
|
"Common.Controls.Actions.Confirm" = "Confirm";
|
||||||
|
@ -13,8 +15,9 @@
|
||||||
"Common.Controls.Actions.SignIn" = "Sign in";
|
"Common.Controls.Actions.SignIn" = "Sign in";
|
||||||
"Common.Controls.Actions.SignUp" = "Sign up";
|
"Common.Controls.Actions.SignUp" = "Sign up";
|
||||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
"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.ShowPost" = "Show Post";
|
||||||
|
"Common.Controls.Status.StatusContentWarning" = "content warning";
|
||||||
"Common.Controls.Status.UserBoosted" = "%@ boosted";
|
"Common.Controls.Status.UserBoosted" = "%@ boosted";
|
||||||
"Common.Controls.Timeline.LoadMore" = "Load More";
|
"Common.Controls.Timeline.LoadMore" = "Load More";
|
||||||
"Common.Countable.Photo.Multiple" = "photos";
|
"Common.Countable.Photo.Multiple" = "photos";
|
||||||
|
|
|
@ -195,7 +195,7 @@ extension AuthenticationViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] error in
|
.sink { [weak self] error in
|
||||||
guard let self = self else { return }
|
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)
|
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||||
alertController.addAction(okAction)
|
alertController.addAction(okAction)
|
||||||
self.coordinator.present(
|
self.coordinator.present(
|
||||||
|
|
|
@ -398,7 +398,7 @@ extension MastodonRegisterViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] error in
|
.sink { [weak self] error in
|
||||||
guard let self = self else { return }
|
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)
|
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||||
alertController.addAction(okAction)
|
alertController.addAction(okAction)
|
||||||
self.coordinator.present(
|
self.coordinator.present(
|
||||||
|
|
|
@ -17,6 +17,10 @@ extension HomeTimelineViewController {
|
||||||
identifier: nil,
|
identifier: nil,
|
||||||
options: .displayInline,
|
options: .displayInline,
|
||||||
children: [
|
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
|
UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.signOutAction(action)
|
self.signOutAction(action)
|
||||||
|
@ -29,6 +33,10 @@ extension HomeTimelineViewController {
|
||||||
|
|
||||||
extension HomeTimelineViewController {
|
extension HomeTimelineViewController {
|
||||||
|
|
||||||
|
@objc private func showPublicTimelineAction(_ sender: UIAction) {
|
||||||
|
coordinator.present(scene: .publicTimeline, from: self, transition: .show)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func signOutAction(_ sender: UIAction) {
|
@objc private func signOutAction(_ sender: UIAction) {
|
||||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -175,23 +175,17 @@ extension HomeTimelineViewController {
|
||||||
// MARK: - UIScrollViewDelegate
|
// MARK: - UIScrollViewDelegate
|
||||||
extension HomeTimelineViewController {
|
extension HomeTimelineViewController {
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
guard scrollView === tableView else { return }
|
handleScrollViewDidScroll(scrollView)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||||
|
typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading
|
||||||
|
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||||
|
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
extension HomeTimelineViewController: UITableViewDelegate {
|
extension HomeTimelineViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
@ -207,13 +201,6 @@ extension HomeTimelineViewController: UITableViewDelegate {
|
||||||
return ceil(frame.height)
|
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
|
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||||
|
@ -233,7 +220,7 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate
|
||||||
viewModel.loadMiddleSateMachineList
|
viewModel.loadMiddleSateMachineList
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] ids in
|
.sink { [weak self] ids in
|
||||||
guard let self = self else { return }
|
guard let _ = self else { return }
|
||||||
if let stateMachine = ids[upperTimelineIndexObjectID] {
|
if let stateMachine = ids[upperTimelineIndexObjectID] {
|
||||||
guard let state = stateMachine.currentState else {
|
guard let state = stateMachine.currentState else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
|
|
@ -83,23 +83,24 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
var newTimelineItems: [Item] = []
|
var newTimelineItems: [Item] = []
|
||||||
|
|
||||||
for (i, timelineIndex) in timelineIndexes.enumerated() {
|
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
|
// append new item into snapshot
|
||||||
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
||||||
|
|
||||||
let isLast = i == timelineIndexes.count - 1
|
let isLast = i == timelineIndexes.count - 1
|
||||||
switch (isLast, timelineIndex.hasMore) {
|
switch (isLast, timelineIndex.hasMore) {
|
||||||
case (true, false):
|
|
||||||
attribute.separatorLineStyle = .normal
|
|
||||||
case (false, true):
|
case (false, true):
|
||||||
attribute.separatorLineStyle = .expand
|
|
||||||
newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID))
|
newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID))
|
||||||
case (true, true):
|
case (true, true):
|
||||||
attribute.separatorLineStyle = .normal
|
|
||||||
shouldAddBottomLoader = true
|
shouldAddBottomLoader = true
|
||||||
case (false, false):
|
default:
|
||||||
attribute.separatorLineStyle = .indent
|
break
|
||||||
}
|
}
|
||||||
} // end for
|
} // end for
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ extension MainTabBarController {
|
||||||
case .implicit:
|
case .implicit:
|
||||||
break
|
break
|
||||||
case .explicit:
|
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)
|
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||||
alertController.addAction(okAction)
|
alertController.addAction(okAction)
|
||||||
coordinator.present(
|
coordinator.present(
|
||||||
|
|
|
@ -13,7 +13,7 @@ import GameplayKit
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class PublicTimelineViewController: UIViewController, NeedsDependency, StatusTableViewCellDelegate {
|
final class PublicTimelineViewController: UIViewController, NeedsDependency {
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ extension PublicTimelineViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
title = "Public"
|
||||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
tableView.refreshControl = refreshControl
|
tableView.refreshControl = refreshControl
|
||||||
|
@ -202,3 +203,6 @@ extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - StatusTableViewCellDelegate
|
||||||
|
extension PublicTimelineViewController: StatusTableViewCellDelegate { }
|
||||||
|
|
|
@ -58,7 +58,12 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
|
|
||||||
var items = [Item]()
|
var items = [Item]()
|
||||||
for (_, toot) in indexTootTuples {
|
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))
|
items.append(Item.toot(objectID: toot.objectID, attribute: attribute))
|
||||||
if tootIDsWhichHasGap.contains(toot.id) {
|
if tootIDsWhichHasGap.contains(toot.id) {
|
||||||
items.append(Item.publicMiddleLoader(tootID: toot.id))
|
items.append(Item.publicMiddleLoader(tootID: toot.id))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// MosaicImageView.swift
|
// MosaicImageViewContainer.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by Cirno MainasuK on 2021-2-23.
|
// Created by Cirno MainasuK on 2021-2-23.
|
||||||
|
@ -9,31 +9,43 @@ import os.log
|
||||||
import func AVFoundation.AVMakeRect
|
import func AVFoundation.AVMakeRect
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol MosaicImageViewPresentable: class {
|
protocol MosaicImageViewContainerPresentable: class {
|
||||||
var mosaicImageView: MosaicImageView { get }
|
var mosaicImageViewContainer: MosaicImageViewContainer { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol MosaicImageViewDelegate: class {
|
protocol MosaicImageViewContainerDelegate: class {
|
||||||
func mosaicImageView(_ mosaicImageView: MosaicImageView, didTapImageView imageView: UIImageView, atIndex index: Int)
|
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 cornerRadius: CGFloat = 4
|
||||||
|
static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||||
|
|
||||||
weak var delegate: MosaicImageViewDelegate?
|
weak var delegate: MosaicImageViewContainerDelegate?
|
||||||
|
|
||||||
let container = UIStackView()
|
let container = UIStackView()
|
||||||
var imageViews = [UIImageView]() {
|
var imageViews: [UIImageView] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
imageViews.forEach { imageView in
|
imageViews.forEach { imageView in
|
||||||
imageView.isUserInteractionEnabled = true
|
imageView.isUserInteractionEnabled = true
|
||||||
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
tapGesture.addTarget(self, action: #selector(MosaicImageView.photoTapGestureRecognizerHandler(_:)))
|
tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:)))
|
||||||
imageView.addGestureRecognizer(tapGesture)
|
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!
|
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
@ -49,10 +61,12 @@ final class MosaicImageView: UIView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MosaicImageView {
|
extension MosaicImageViewContainer {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
container.translatesAutoresizingMaskIntoConstraints = false
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.axis = .horizontal
|
||||||
|
container.distribution = .fillEqually
|
||||||
addSubview(container)
|
addSubview(container)
|
||||||
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1)
|
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -63,13 +77,37 @@ extension MosaicImageView {
|
||||||
containerHeightLayoutConstraint
|
containerHeightLayoutConstraint
|
||||||
])
|
])
|
||||||
|
|
||||||
container.axis = .horizontal
|
// add blur visual effect view in the setup method
|
||||||
container.distribution = .fillEqually
|
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() {
|
func reset() {
|
||||||
container.arrangedSubviews.forEach { subview in
|
container.arrangedSubviews.forEach { subview in
|
||||||
|
@ -79,6 +117,9 @@ extension MosaicImageView {
|
||||||
container.subviews.forEach { subview in
|
container.subviews.forEach { subview in
|
||||||
subview.removeFromSuperview()
|
subview.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
blurVisualEffectView.removeFromSuperview()
|
||||||
|
blurVisualEffectView.effect = MosaicImageViewContainer.blurVisualEffect
|
||||||
|
vibrancyVisualEffectView.alpha = 1.0
|
||||||
imageViews = []
|
imageViews = []
|
||||||
|
|
||||||
container.spacing = 1
|
container.spacing = 1
|
||||||
|
@ -99,7 +140,8 @@ extension MosaicImageView {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
imageViews.append(imageView)
|
imageViews.append(imageView)
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = MosaicImageView.cornerRadius
|
imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
|
||||||
|
imageView.layer.cornerCurve = .continuous
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -113,6 +155,15 @@ extension MosaicImageView {
|
||||||
containerHeightLayoutConstraint.constant = floor(rect.height)
|
containerHeightLayoutConstraint.constant = floor(rect.height)
|
||||||
containerHeightLayoutConstraint.isActive = true
|
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
|
return imageView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +193,7 @@ extension MosaicImageView {
|
||||||
self.imageViews.append(contentsOf: imageViews)
|
self.imageViews.append(contentsOf: imageViews)
|
||||||
imageViews.forEach { imageView in
|
imageViews.forEach { imageView in
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = MosaicImageView.cornerRadius
|
imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
|
||||||
imageView.layer.cornerCurve = .continuous
|
imageView.layer.cornerCurve = .continuous
|
||||||
imageView.contentMode = .scaleAspectFill
|
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
|
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) {
|
@objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
guard let imageView = sender.view as? UIImageView else { return }
|
guard let imageView = sender.view as? UIImageView else { return }
|
||||||
guard let index = imageViews.firstIndex(of: imageView) 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)
|
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)
|
#if DEBUG && canImport(SwiftUI)
|
||||||
|
@ -218,7 +285,7 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageView()
|
let view = MosaicImageViewContainer()
|
||||||
let image = images[3]
|
let image = images[3]
|
||||||
let imageView = view.setupImageView(
|
let imageView = view.setupImageView(
|
||||||
aspectRatio: image.size,
|
aspectRatio: image.size,
|
||||||
|
@ -230,7 +297,7 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
.previewDisplayName("Portrait - one image")
|
.previewDisplayName("Portrait - one image")
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageView()
|
let view = MosaicImageViewContainer()
|
||||||
let image = images[1]
|
let image = images[1]
|
||||||
let imageView = view.setupImageView(
|
let imageView = view.setupImageView(
|
||||||
aspectRatio: image.size,
|
aspectRatio: image.size,
|
||||||
|
@ -245,7 +312,7 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
.previewDisplayName("Landscape - one image")
|
.previewDisplayName("Landscape - one image")
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageView()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(2)
|
let images = self.images.prefix(2)
|
||||||
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
for (i, imageView) in imageViews.enumerated() {
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
@ -256,7 +323,7 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
.previewDisplayName("two image")
|
.previewDisplayName("two image")
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageView()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(3)
|
let images = self.images.prefix(3)
|
||||||
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
for (i, imageView) in imageViews.enumerated() {
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
@ -267,7 +334,7 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
.previewDisplayName("three image")
|
.previewDisplayName("three image")
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageView()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(4)
|
let images = self.images.prefix(4)
|
||||||
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
for (i, imageView) in imageViews.enumerated() {
|
for (i, imageView) in imageViews.enumerated() {
|
|
@ -22,6 +22,7 @@ final class StatusView: UIView {
|
||||||
static let contentWarningBlurRadius: CGFloat = 12
|
static let contentWarningBlurRadius: CGFloat = 12
|
||||||
|
|
||||||
weak var delegate: StatusViewDelegate?
|
weak var delegate: StatusViewDelegate?
|
||||||
|
var isStatusTextSensitive = false
|
||||||
|
|
||||||
let headerContainerStackView = UIStackView()
|
let headerContainerStackView = UIStackView()
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ final class StatusView: UIView {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
label.text = L10n.Common.Controls.Status.contentWarning
|
label.text = L10n.Common.Controls.Status.statusContentWarning
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
let contentWarningActionButton: UIButton = {
|
let contentWarningActionButton: UIButton = {
|
||||||
|
@ -98,12 +99,12 @@ final class StatusView: UIView {
|
||||||
button.setTitle(L10n.Common.Controls.Status.showPost, for: .normal)
|
button.setTitle(L10n.Common.Controls.Status.showPost, for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
let mosaicImageView = MosaicImageView()
|
let statusMosaicImageView = MosaicImageViewContainer()
|
||||||
|
|
||||||
// do not use visual effect view due to we blur text only without background
|
// do not use visual effect view due to we blur text only without background
|
||||||
let contentWarningBlurContentImageView: UIImageView = {
|
let contentWarningBlurContentImageView: UIImageView = {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
imageView.backgroundColor = .secondarySystemGroupedBackground
|
imageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||||
imageView.layer.masksToBounds = false
|
imageView.layer.masksToBounds = false
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
|
@ -127,6 +128,15 @@ final class StatusView: UIView {
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
// update blur image when interface style changed
|
||||||
|
if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle {
|
||||||
|
drawContentWarningImageView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusView {
|
extension StatusView {
|
||||||
|
@ -247,7 +257,7 @@ extension StatusView {
|
||||||
])
|
])
|
||||||
statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle)
|
statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle)
|
||||||
statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton)
|
statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton)
|
||||||
statusContainerStackView.addArrangedSubview(mosaicImageView)
|
statusContainerStackView.addArrangedSubview(statusMosaicImageView)
|
||||||
|
|
||||||
|
|
||||||
// action toolbar container
|
// action toolbar container
|
||||||
|
@ -255,7 +265,7 @@ extension StatusView {
|
||||||
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||||
|
|
||||||
headerContainerStackView.isHidden = true
|
headerContainerStackView.isHidden = true
|
||||||
mosaicImageView.isHidden = true
|
statusMosaicImageView.isHidden = true
|
||||||
contentWarningBlurContentImageView.isHidden = true
|
contentWarningBlurContentImageView.isHidden = true
|
||||||
statusContentWarningContainerStackView.isHidden = true
|
statusContentWarningContainerStackView.isHidden = true
|
||||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
||||||
|
@ -272,7 +282,9 @@ extension StatusView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawContentWarningImageView() {
|
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()
|
cleanUpContentWarning()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -331,7 +343,7 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let statusView = StatusView()
|
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
||||||
statusView.configure(
|
statusView.configure(
|
||||||
with: AvatarConfigurableViewConfiguration(
|
with: AvatarConfigurableViewConfiguration(
|
||||||
avatarImageURL: nil,
|
avatarImageURL: nil,
|
||||||
|
@ -339,9 +351,20 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
statusView.headerContainerStackView.isHidden = false
|
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
|
return statusView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
.previewLayout(.fixed(width: 375, height: 380))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,14 @@ import Combine
|
||||||
protocol StatusTableViewCellDelegate: class {
|
protocol StatusTableViewCellDelegate: class {
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: 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 {
|
final class StatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
static let bottomPaddingHeight: CGFloat = 10
|
||||||
|
|
||||||
weak var delegate: StatusTableViewCellDelegate?
|
weak var delegate: StatusTableViewCellDelegate?
|
||||||
|
|
||||||
|
@ -28,6 +32,7 @@ final class StatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
statusView.isStatusTextSensitive = false
|
||||||
statusView.cleanUpContentWarning()
|
statusView.cleanUpContentWarning()
|
||||||
disposeBag.removeAll()
|
disposeBag.removeAll()
|
||||||
observations.removeAll()
|
observations.removeAll()
|
||||||
|
@ -43,6 +48,13 @@ final class StatusTableViewCell: UITableViewCell {
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.statusView.drawContentWarningImageView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewCell {
|
extension StatusTableViewCell {
|
||||||
|
@ -50,6 +62,7 @@ extension StatusTableViewCell {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||||
|
statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||||
|
|
||||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(statusView)
|
contentView.addSubview(statusView)
|
||||||
|
@ -67,12 +80,13 @@ extension StatusTableViewCell {
|
||||||
bottomPaddingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
bottomPaddingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
bottomPaddingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
bottomPaddingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
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.delegate = self
|
||||||
|
statusView.statusMosaicImageView.delegate = self
|
||||||
statusView.actionToolbarContainer.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
|
// MARK: - ActionToolbarContainerDelegate
|
||||||
extension StatusTableViewCell: ActionToolbarContainerDelegate {
|
extension StatusTableViewCell: ActionToolbarContainerDelegate {
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
|
||||||
|
|
|
@ -17,3 +17,20 @@ final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell {
|
||||||
activityIndicatorView.startAnimating()
|
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
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@ import Combine
|
||||||
|
|
||||||
class TimelineLoaderTableViewCell: UITableViewCell {
|
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<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -50,18 +52,18 @@ class TimelineLoaderTableViewCell: UITableViewCell {
|
||||||
loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
|
loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(loadMoreButton)
|
contentView.addSubview(loadMoreButton)
|
||||||
NSLayoutConstraint.activate([
|
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),
|
loadMoreButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor),
|
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: 8),
|
contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: TimelineLoaderTableViewCell.bottomPadding),
|
||||||
loadMoreButton.heightAnchor.constraint(equalToConstant: TimelineLoaderTableViewCell.cellHeight - 2 * 8).priority(.defaultHigh),
|
loadMoreButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
|
|
||||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(activityIndicatorView)
|
addSubview(activityIndicatorView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
activityIndicatorView.centerXAnchor.constraint(equalTo: loadMoreButton.centerXAnchor),
|
||||||
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
activityIndicatorView.centerYAnchor.constraint(equalTo: loadMoreButton.centerYAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
loadMoreButton.isHidden = true
|
loadMoreButton.isHidden = true
|
||||||
|
|
|
@ -23,16 +23,6 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell {
|
||||||
|
|
||||||
backgroundColor = .clear
|
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.isHidden = false
|
||||||
loadMoreButton.setImage(Asset.Arrows.arrowTriangle2Circlepath.image.withRenderingMode(.alwaysTemplate), for: .normal)
|
loadMoreButton.setImage(Asset.Arrows.arrowTriangle2Circlepath.image.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||||
loadMoreButton.setInsets(forContentPadding: .zero, imageTitlePadding: 4)
|
loadMoreButton.setInsets(forContentPadding: .zero, imageTitlePadding: 4)
|
||||||
|
@ -46,3 +36,20 @@ extension TimelineMiddleLoaderTableViewCell {
|
||||||
delegate?.timelineMiddleLoaderTableViewCell(self, loadMoreButtonDidPressed: sender)
|
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
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,7 @@ extension APIService {
|
||||||
return APIService.Persist.persistTimeline(
|
return APIService.Persist.persistTimeline(
|
||||||
managedObjectContext: self.backgroundManagedObjectContext,
|
managedObjectContext: self.backgroundManagedObjectContext,
|
||||||
domain: mastodonAuthenticationBox.domain,
|
domain: mastodonAuthenticationBox.domain,
|
||||||
query: query as! TimelineQueryType,
|
query: query,
|
||||||
response: response,
|
response: response,
|
||||||
persistType: .likeList,
|
persistType: .likeList,
|
||||||
requestMastodonUserID: requestMastodonUserID,
|
requestMastodonUserID: requestMastodonUserID,
|
||||||
|
|
|
@ -13,14 +13,14 @@ extension Mastodon.Entity {
|
||||||
/// - Since: 2.8.0
|
/// - Since: 2.8.0
|
||||||
/// - Version: 3.3.0
|
/// - Version: 3.3.0
|
||||||
/// # Last Update
|
/// # Last Update
|
||||||
/// 2021/2/4
|
/// 2021/2/24
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/poll/)
|
/// [Document](https://docs.joinmastodon.org/entities/poll/)
|
||||||
public struct Poll: Codable {
|
public struct Poll: Codable {
|
||||||
public typealias ID = String
|
public typealias ID = String
|
||||||
|
|
||||||
public let id: ID
|
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 expired: Bool
|
||||||
public let multiple: Bool
|
public let multiple: Bool
|
||||||
public let votesCount: Int
|
public let votesCount: Int
|
||||||
|
|
Loading…
Reference in New Issue