New items indicator
This commit is contained in:
parent
9274cd2615
commit
beee1ff73b
|
@ -160,6 +160,7 @@
|
||||||
"main-navigation.conversations" = "Messages";
|
"main-navigation.conversations" = "Messages";
|
||||||
"metatext" = "Metatext";
|
"metatext" = "Metatext";
|
||||||
"notification.signed-in-as-%@" = "Logged in as %@";
|
"notification.signed-in-as-%@" = "Logged in as %@";
|
||||||
|
"notification.new-items" = "New notifications";
|
||||||
"notifications.all" = "All";
|
"notifications.all" = "All";
|
||||||
"notifications.mentions" = "Mentions";
|
"notifications.mentions" = "Mentions";
|
||||||
"ok" = "OK";
|
"ok" = "OK";
|
||||||
|
@ -274,6 +275,8 @@
|
||||||
"status.delete-and-redraft.confirm.post" = "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.";
|
"status.delete-and-redraft.confirm.post" = "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.";
|
||||||
"status.delete-and-redraft.confirm.toot" = "Are you sure you want to delete this toot and re-draft it? Favorites and boosts will be lost, and replies to the original toot will be orphaned.";
|
"status.delete-and-redraft.confirm.toot" = "Are you sure you want to delete this toot and re-draft it? Favorites and boosts will be lost, and replies to the original toot will be orphaned.";
|
||||||
"status.mute" = "Mute conversation";
|
"status.mute" = "Mute conversation";
|
||||||
|
"status.new-items.post" = "New posts";
|
||||||
|
"status.new-items.toot" = "New toots";
|
||||||
"status.pin" = "Pin on profile";
|
"status.pin" = "Pin on profile";
|
||||||
"status.pinned.post" = "Pinned post";
|
"status.pinned.post" = "Pinned post";
|
||||||
"status.pinned.toot" = "Pinned toot";
|
"status.pinned.toot" = "Pinned toot";
|
||||||
|
|
|
@ -160,6 +160,7 @@
|
||||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; };
|
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; };
|
||||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
||||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
||||||
|
D0CEC0E125E0BB9700FEF5A6 /* NewItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CEC0E025E0BB9700FEF5A6 /* NewItemsView.swift */; };
|
||||||
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */; };
|
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */; };
|
||||||
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4625BCD289003D5DF2 /* TagView.swift */; };
|
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4625BCD289003D5DF2 /* TagView.swift */; };
|
||||||
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */; };
|
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */; };
|
||||||
|
@ -376,6 +377,7 @@
|
||||||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = "<group>"; };
|
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = "<group>"; };
|
||||||
|
D0CEC0E025E0BB9700FEF5A6 /* NewItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewItemsView.swift; sourceTree = "<group>"; };
|
||||||
D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = "<group>"; };
|
D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0D2AC4625BCD289003D5DF2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
D0D2AC4625BCD289003D5DF2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||||
D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTableViewCell.swift; sourceTree = "<group>"; };
|
D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -463,6 +465,7 @@
|
||||||
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
|
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
|
||||||
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */,
|
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */,
|
||||||
D025B17D25C500BC001C69A8 /* CapsuleButton.swift */,
|
D025B17D25C500BC001C69A8 /* CapsuleButton.swift */,
|
||||||
|
D0477F4525C72E50005C5368 /* CapsuleLabel.swift */,
|
||||||
D0EA593F2522AC8700804347 /* CardView.swift */,
|
D0EA593F2522AC8700804347 /* CardView.swift */,
|
||||||
D021A66F25C3E1F9008A0C0D /* Collection View Cells */,
|
D021A66F25C3E1F9008A0C0D /* Collection View Cells */,
|
||||||
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
|
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
|
||||||
|
@ -475,11 +478,11 @@
|
||||||
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
||||||
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */,
|
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */,
|
||||||
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */,
|
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */,
|
||||||
D0477F4525C72E50005C5368 /* CapsuleLabel.swift */,
|
|
||||||
D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */,
|
D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */,
|
||||||
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
||||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||||
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
||||||
|
D0CEC0E025E0BB9700FEF5A6 /* NewItemsView.swift */,
|
||||||
D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */,
|
D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */,
|
||||||
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
||||||
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */,
|
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */,
|
||||||
|
@ -1138,6 +1141,7 @@
|
||||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
||||||
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
|
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
|
||||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
||||||
|
D0CEC0E125E0BB9700FEF5A6 /* NewItemsView.swift in Sources */,
|
||||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||||
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */,
|
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */,
|
||||||
D0477F4625C72E50005C5368 /* CapsuleLabel.swift in Sources */,
|
D0477F4625C72E50005C5368 /* CapsuleLabel.swift in Sources */,
|
||||||
|
|
|
@ -11,6 +11,7 @@ public protocol CollectionService {
|
||||||
var canRefresh: Bool { get }
|
var canRefresh: Bool { get }
|
||||||
var title: AnyPublisher<String, Never> { get }
|
var title: AnyPublisher<String, Never> { get }
|
||||||
var titleLocalizationComponents: AnyPublisher<[String], Never> { get }
|
var titleLocalizationComponents: AnyPublisher<[String], Never> { get }
|
||||||
|
var announcesNewItems: Bool { get }
|
||||||
var navigationService: NavigationService { get }
|
var navigationService: NavigationService { get }
|
||||||
var positionTimeline: Timeline? { get }
|
var positionTimeline: Timeline? { get }
|
||||||
func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error>
|
func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error>
|
||||||
|
@ -31,6 +32,8 @@ extension CollectionService {
|
||||||
|
|
||||||
public var titleLocalizationComponents: AnyPublisher<[String], Never> { Empty().eraseToAnyPublisher() }
|
public var titleLocalizationComponents: AnyPublisher<[String], Never> { Empty().eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
public var announcesNewItems: Bool { false }
|
||||||
|
|
||||||
public var positionTimeline: Timeline? { nil }
|
public var positionTimeline: Timeline? { nil }
|
||||||
|
|
||||||
public func requestMarkerLastReadId() -> AnyPublisher<CollectionItem.Id, Error> { Empty().eraseToAnyPublisher() }
|
public func requestMarkerLastReadId() -> AnyPublisher<CollectionItem.Id, Error> { Empty().eraseToAnyPublisher() }
|
||||||
|
|
|
@ -10,6 +10,7 @@ public struct NotificationsService {
|
||||||
public let sections: AnyPublisher<[CollectionSection], Error>
|
public let sections: AnyPublisher<[CollectionSection], Error>
|
||||||
public let nextPageMaxId: AnyPublisher<String, Never>
|
public let nextPageMaxId: AnyPublisher<String, Never>
|
||||||
public let navigationService: NavigationService
|
public let navigationService: NavigationService
|
||||||
|
public let announcesNewItems = true
|
||||||
|
|
||||||
private let excludeTypes: Set<MastodonNotification.NotificationType>
|
private let excludeTypes: Set<MastodonNotification.NotificationType>
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
|
|
@ -13,6 +13,7 @@ public struct TimelineService {
|
||||||
public let accountIdsForRelationships: AnyPublisher<Set<Account.Id>, Never>
|
public let accountIdsForRelationships: AnyPublisher<Set<Account.Id>, Never>
|
||||||
public let title: AnyPublisher<String, Never>
|
public let title: AnyPublisher<String, Never>
|
||||||
public let titleLocalizationComponents: AnyPublisher<[String], Never>
|
public let titleLocalizationComponents: AnyPublisher<[String], Never>
|
||||||
|
public let announcesNewItems = true
|
||||||
|
|
||||||
private let timeline: Timeline
|
private let timeline: Timeline
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
|
|
@ -16,11 +16,14 @@ class TableViewController: UITableViewController {
|
||||||
private let rootViewModel: RootViewModel?
|
private let rootViewModel: RootViewModel?
|
||||||
private let loadingTableFooterView = LoadingTableFooterView()
|
private let loadingTableFooterView = LoadingTableFooterView()
|
||||||
private let webfingerIndicatorView = WebfingerIndicatorView()
|
private let webfingerIndicatorView = WebfingerIndicatorView()
|
||||||
|
private let newItemsView = NewItemsView()
|
||||||
@Published private var loading = false
|
@Published private var loading = false
|
||||||
private var visibleLoadMoreViews = Set<LoadMoreView>()
|
private var visibleLoadMoreViews = Set<LoadMoreView>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
||||||
private var shouldKeepPlayingVideoAfterDismissal = false
|
private var shouldKeepPlayingVideoAfterDismissal = false
|
||||||
|
private var newItemsViewHiddenConstraint: NSLayoutConstraint?
|
||||||
|
private var newItemsViewVisibleConstraint: NSLayoutConstraint?
|
||||||
private let insetBottom: Bool
|
private let insetBottom: Bool
|
||||||
private weak var parentNavigationController: UINavigationController?
|
private weak var parentNavigationController: UINavigationController?
|
||||||
|
|
||||||
|
@ -67,11 +70,27 @@ class TableViewController: UITableViewController {
|
||||||
view.addSubview(webfingerIndicatorView)
|
view.addSubview(webfingerIndicatorView)
|
||||||
webfingerIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
webfingerIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
view.addSubview(newItemsView)
|
||||||
|
newItemsView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
newItemsView.alpha = 0
|
||||||
|
|
||||||
|
newItemsViewHiddenConstraint = newItemsView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
|
||||||
|
newItemsViewHiddenConstraint?.isActive = true
|
||||||
|
newItemsViewVisibleConstraint = newItemsView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
|
||||||
|
constant: .defaultSpacing)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
webfingerIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
|
webfingerIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
|
||||||
webfingerIndicatorView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
|
webfingerIndicatorView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||||
|
newItemsView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
newItemsView.button.addAction(UIAction { [weak self] _ in
|
||||||
|
self?.newItemsTapped()
|
||||||
|
self?.hideNewItemsView()
|
||||||
|
},
|
||||||
|
for: .touchUpInside)
|
||||||
|
|
||||||
setupViewModelBindings()
|
setupViewModelBindings()
|
||||||
|
|
||||||
viewModel.request(maxId: nil, minId: nil, search: nil)
|
viewModel.request(maxId: nil, minId: nil, search: nil)
|
||||||
|
@ -91,6 +110,10 @@ class TableViewController: UITableViewController {
|
||||||
for loadMoreView in visibleLoadMoreViews {
|
for loadMoreView in visibleLoadMoreViews {
|
||||||
loadMoreView.directionChanged(up: up)
|
loadMoreView.directionChanged(up: up)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if up, newItemsView.alpha > 0 {
|
||||||
|
hideNewItemsView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
@ -313,7 +336,9 @@ extension TableViewController: ZoomAnimatorDelegate {
|
||||||
|
|
||||||
extension TableViewController: ScrollableToTop {
|
extension TableViewController: ScrollableToTop {
|
||||||
func scrollToTop(animated: Bool) {
|
func scrollToTop(animated: Bool) {
|
||||||
tableView.scrollToTop(animated: animated)
|
guard !dataSource.snapshot().itemIdentifiers.isEmpty else { return }
|
||||||
|
|
||||||
|
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,6 +426,7 @@ private extension TableViewController {
|
||||||
let positionMaintenanceOffset: CGFloat
|
let positionMaintenanceOffset: CGFloat
|
||||||
let preUpdateContentOffsetY = tableView.contentOffset.y
|
let preUpdateContentOffsetY = tableView.contentOffset.y
|
||||||
var setPreviousOffset = false
|
var setPreviousOffset = false
|
||||||
|
let firstItemId = dataSource.snapshot().itemIdentifiers.first?.itemId
|
||||||
|
|
||||||
if let itemId = update.maintainScrollPositionItemId,
|
if let itemId = update.maintainScrollPositionItemId,
|
||||||
let indexPath = dataSource.indexPath(itemId: itemId) {
|
let indexPath = dataSource.indexPath(itemId: itemId) {
|
||||||
|
@ -430,6 +456,16 @@ private extension TableViewController {
|
||||||
|
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
||||||
self.tableView.contentOffset.y -= positionMaintenanceOffset
|
self.tableView.contentOffset.y -= positionMaintenanceOffset
|
||||||
|
|
||||||
|
if self.viewModel.announcesNewItems,
|
||||||
|
let firstItemId = firstItemId,
|
||||||
|
let newFirstItem = self.dataSource.snapshot().itemIdentifiers.first,
|
||||||
|
let newFirstItemId = newFirstItem.itemId,
|
||||||
|
newFirstItemId > firstItemId {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.announceNewItems(newestItem: newFirstItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if setPreviousOffset {
|
} else if setPreviousOffset {
|
||||||
self.tableView.contentOffset.y = preUpdateContentOffsetY
|
self.tableView.contentOffset.y = preUpdateContentOffsetY
|
||||||
}
|
}
|
||||||
|
@ -723,5 +759,49 @@ private extension TableViewController {
|
||||||
viewModel.request(maxId: nil, minId: nil, search: nil)
|
viewModel.request(maxId: nil, minId: nil, search: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newItemsTapped() {
|
||||||
|
scrollToTop(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func announceNewItems(newestItem: CollectionItem) {
|
||||||
|
switch newestItem {
|
||||||
|
case .status:
|
||||||
|
switch viewModel.identityContext.appPreferences.statusWord {
|
||||||
|
case .toot:
|
||||||
|
newItemsView.title = NSLocalizedString("status.new-items.toot", comment: "")
|
||||||
|
case .post:
|
||||||
|
newItemsView.title = NSLocalizedString("status.new-items.post", comment: "")
|
||||||
|
}
|
||||||
|
case .notification:
|
||||||
|
newItemsView.title = NSLocalizedString("notification.new-items", comment: "")
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newItemsView.layoutIfNeeded()
|
||||||
|
|
||||||
|
UIView.animate(withDuration: .zeroIfReduceMotion(.defaultAnimationDuration),
|
||||||
|
delay: 0,
|
||||||
|
usingSpringWithDamping: 0.5,
|
||||||
|
initialSpringVelocity: 5,
|
||||||
|
options: .curveEaseInOut) {
|
||||||
|
self.newItemsView.alpha = 1
|
||||||
|
self.newItemsViewHiddenConstraint?.isActive = false
|
||||||
|
self.newItemsViewVisibleConstraint?.isActive = true
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
} completion: { _ in
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideNewItemsView() {
|
||||||
|
UIView.animate(withDuration: .zeroIfReduceMotion(.defaultAnimationDuration)) {
|
||||||
|
self.newItemsView.alpha = 0
|
||||||
|
self.newItemsViewHiddenConstraint?.isActive = true
|
||||||
|
self.newItemsViewVisibleConstraint?.isActive = false
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// swiftlint:enable file_length
|
// swiftlint:enable file_length
|
||||||
|
|
|
@ -125,6 +125,8 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
|
|
||||||
public var canRefresh: Bool { collectionService.canRefresh }
|
public var canRefresh: Bool { collectionService.canRefresh }
|
||||||
|
|
||||||
|
public var announcesNewItems: Bool { collectionService.announcesNewItems }
|
||||||
|
|
||||||
public func request(maxId: String? = nil, minId: String? = nil, search: Search?) {
|
public func request(maxId: String? = nil, minId: String? = nil, search: Search?) {
|
||||||
collectionService.request(maxId: realMaxId(maxId: maxId), minId: minId, search: search)
|
collectionService.request(maxId: realMaxId(maxId: maxId), minId: minId, search: search)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -409,7 +411,7 @@ private extension CollectionItemsViewModel {
|
||||||
return configuration.isContextParent // Maintain scroll position of parent after initial load of context
|
return configuration.isContextParent // Maintain scroll position of parent after initial load of context
|
||||||
}) {
|
}) {
|
||||||
return contextParent.itemId
|
return contextParent.itemId
|
||||||
} else if collectionService is TimelineService {
|
} else if collectionService is TimelineService || collectionService is NotificationsService {
|
||||||
let difference = newItems.difference(from: items)
|
let difference = newItems.difference(from: items)
|
||||||
|
|
||||||
if let lastSelectedLoadMore = lastSelectedLoadMore {
|
if let lastSelectedLoadMore = lastSelectedLoadMore {
|
||||||
|
|
|
@ -15,6 +15,7 @@ public protocol CollectionViewModel {
|
||||||
var searchScopeChanges: AnyPublisher<SearchScope, Never> { get }
|
var searchScopeChanges: AnyPublisher<SearchScope, Never> { get }
|
||||||
var nextPageMaxId: String? { get }
|
var nextPageMaxId: String? { get }
|
||||||
var canRefresh: Bool { get }
|
var canRefresh: Bool { get }
|
||||||
|
var announcesNewItems: Bool { get }
|
||||||
func request(maxId: String?, minId: String?, search: Search?)
|
func request(maxId: String?, minId: String?, search: Search?)
|
||||||
func requestNextPage(fromIndexPath indexPath: IndexPath)
|
func requestNextPage(fromIndexPath indexPath: IndexPath)
|
||||||
func viewedAtTop(indexPath: IndexPath)
|
func viewedAtTop(indexPath: IndexPath)
|
||||||
|
|
|
@ -167,7 +167,9 @@ public extension NavigationViewModel {
|
||||||
collectionService: identityContext.service.notificationsService(excludeTypes: excludeTypes),
|
collectionService: identityContext.service.notificationsService(excludeTypes: excludeTypes),
|
||||||
identityContext: identityContext)
|
identityContext: identityContext)
|
||||||
|
|
||||||
viewModel.request(maxId: nil, minId: nil, search: nil)
|
if excludeTypes.isEmpty {
|
||||||
|
viewModel.request(maxId: nil, minId: nil, search: nil)
|
||||||
|
}
|
||||||
|
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,8 @@ extension ProfileViewModel: CollectionViewModel {
|
||||||
|
|
||||||
public var canRefresh: Bool { collectionViewModel.value.canRefresh }
|
public var canRefresh: Bool { collectionViewModel.value.canRefresh }
|
||||||
|
|
||||||
|
public var announcesNewItems: Bool { collectionViewModel.value.canRefresh }
|
||||||
|
|
||||||
public func request(maxId: String?, minId: String?, search: Search?) {
|
public func request(maxId: String?, minId: String?, search: Search?) {
|
||||||
if case .statuses = collection, maxId == nil {
|
if case .statuses = collection, maxId == nil {
|
||||||
profileService.fetchPinnedStatuses()
|
profileService.fetchPinnedStatuses()
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class NewItemsView: UIView {
|
||||||
|
let button = UIButton()
|
||||||
|
|
||||||
|
public var title: String? {
|
||||||
|
get { label.text }
|
||||||
|
set {
|
||||||
|
label.text = newValue
|
||||||
|
button.accessibilityLabel = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let label = UILabel()
|
||||||
|
private let blurView: UIVisualEffectView
|
||||||
|
private let vibrancyView: UIVisualEffectView
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
|
||||||
|
blurView = UIVisualEffectView(effect: blurEffect)
|
||||||
|
vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect, style: .label))
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
let cornerRadius = bounds.height / 2
|
||||||
|
|
||||||
|
layer.cornerRadius = cornerRadius
|
||||||
|
blurView.layer.cornerRadius = cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewItemsView {
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
|
func initialSetup() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
layer.shadowOffset = .zero
|
||||||
|
layer.shadowRadius = .defaultShadowRadius
|
||||||
|
layer.shadowOpacity = .defaultShadowOpacity
|
||||||
|
|
||||||
|
addSubview(blurView)
|
||||||
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
blurView.clipsToBounds = true
|
||||||
|
blurView.contentView.addSubview(vibrancyView)
|
||||||
|
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
vibrancyView.contentView.addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
|
let arrowImage = UIImage(systemName: "arrow.up",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(weight: .bold))
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(UIImageView(image: arrowImage))
|
||||||
|
stackView.addArrangedSubview(label)
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
|
||||||
|
addSubview(button)
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let touchStartAction = UIAction { [weak self] _ in self?.alpha = 0.75 }
|
||||||
|
|
||||||
|
button.addAction(touchStartAction, for: .touchDown)
|
||||||
|
button.addAction(touchStartAction, for: .touchDragEnter)
|
||||||
|
|
||||||
|
let touchEndAction = UIAction { [weak self] _ in self?.alpha = 1 }
|
||||||
|
|
||||||
|
button.addAction(touchEndAction, for: .touchDragExit)
|
||||||
|
button.addAction(touchEndAction, for: .touchUpInside)
|
||||||
|
button.addAction(touchEndAction, for: .touchUpOutside)
|
||||||
|
button.addAction(touchEndAction, for: .touchCancel)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
button.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
button.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
button.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
button.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
vibrancyView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||||
|
vibrancyView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||||
|
vibrancyView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||||
|
vibrancyView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: vibrancyView.contentView.leadingAnchor,
|
||||||
|
constant: .defaultSpacing),
|
||||||
|
stackView.topAnchor.constraint(equalTo: vibrancyView.contentView.topAnchor, constant: .defaultSpacing),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: vibrancyView.contentView.trailingAnchor,
|
||||||
|
constant: -.defaultSpacing * 2),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: vibrancyView.contentView.bottomAnchor,
|
||||||
|
constant: -.defaultSpacing)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue