forked from zelo72/mastodon-ios
feat: add notification timeline fetcher
This commit is contained in:
parent
59812807c6
commit
d3e8f85cb3
|
@ -279,9 +279,8 @@ extension DataSourceFacade {
|
|||
dependency: NeedsDependency,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async throws {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let _status = status.object(in: managedObjectContext) else { return }
|
||||
try await dependency.context.managedObjectContext.perform {
|
||||
guard let _status = status.object(in: dependency.context.managedObjectContext) else { return }
|
||||
let status = _status.reblog ?? _status
|
||||
|
||||
let isToggled = status.isContentSensitiveToggled || status.isMediaSensitiveToggled
|
||||
|
@ -295,9 +294,8 @@ extension DataSourceFacade {
|
|||
dependency: NeedsDependency,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async throws {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let _status = status.object(in: managedObjectContext) else { return }
|
||||
try await dependency.context.managedObjectContext.perform {
|
||||
guard let _status = status.object(in: dependency.context.managedObjectContext) else { return }
|
||||
let status = _status.reblog ?? _status
|
||||
|
||||
status.update(isMediaSensitiveToggled: !status.isMediaSensitiveToggled)
|
||||
|
|
|
@ -202,7 +202,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
return
|
||||
}
|
||||
guard case let .notification(notification) = item else {
|
||||
assertionFailure("only works for status data provider")
|
||||
assertionFailure("only works for notification item")
|
||||
return
|
||||
}
|
||||
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
|
||||
|
@ -222,6 +222,105 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell, notificationView: NotificationView,
|
||||
statusView: StatusView,
|
||||
spoilerBannerViewDidPressed bannerView: SpoilerBannerView
|
||||
) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .notification(notification) = item else {
|
||||
assertionFailure("only works for notification item")
|
||||
return
|
||||
}
|
||||
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
|
||||
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
|
||||
guard let status = notification.status else { return nil }
|
||||
return .init(objectID: status.objectID)
|
||||
}
|
||||
guard let status = _status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
try await DataSourceFacade.responseToToggleSensitiveAction(
|
||||
dependency: self,
|
||||
status: status
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
quoteStatusView: StatusView,
|
||||
spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView
|
||||
) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .notification(notification) = item else {
|
||||
assertionFailure("only works for notification item")
|
||||
return
|
||||
}
|
||||
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
|
||||
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
|
||||
guard let status = notification.status else { return nil }
|
||||
return .init(objectID: status.objectID)
|
||||
}
|
||||
guard let status = _status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
try await DataSourceFacade.responseToToggleSensitiveAction(
|
||||
dependency: self,
|
||||
status: status
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
quoteStatusView: StatusView,
|
||||
spoilerBannerViewDidPressed bannerView: SpoilerBannerView
|
||||
) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .notification(notification) = item else {
|
||||
assertionFailure("only works for notification item")
|
||||
return
|
||||
}
|
||||
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
|
||||
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
|
||||
guard let status = notification.status else { return nil }
|
||||
return .init(objectID: status.objectID)
|
||||
}
|
||||
guard let status = _status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
try await DataSourceFacade.responseToToggleSensitiveAction(
|
||||
dependency: self,
|
||||
status: status
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: a11y
|
||||
|
|
|
@ -424,29 +424,16 @@ extension HomeTimelineViewController {
|
|||
}
|
||||
|
||||
@objc func signOutAction(_ sender: UIAction) {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
context.authenticationService.signOutMastodonUser(
|
||||
domain: activeMastodonAuthenticationBox.domain,
|
||||
userID: activeMastodonAuthenticationBox.userID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isSignOut):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail")
|
||||
guard isSignOut else { return }
|
||||
Task { @MainActor in
|
||||
try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox)
|
||||
self.coordinator.setup()
|
||||
self.coordinator.setupOnboardingIfNeeds(animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
// MARK: - UIScrollViewDelegate
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
extension NotificationTableViewCell {
|
||||
|
@ -42,8 +43,26 @@ extension NotificationTableViewCell {
|
|||
case .feed(let feed):
|
||||
notificationView.configure(feed: feed)
|
||||
}
|
||||
//
|
||||
|
||||
self.delegate = delegate
|
||||
|
||||
Publishers.CombineLatest(
|
||||
notificationView.statusView.viewModel.$isContentReveal.removeDuplicates(),
|
||||
notificationView.quoteStatusView.viewModel.$isContentReveal.removeDuplicates()
|
||||
)
|
||||
.dropFirst()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak tableView, weak self] _, _ in
|
||||
guard let tableView = tableView else { return }
|
||||
guard let self = self else { return }
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): tableView updates")
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,9 +26,13 @@ protocol NotificationTableViewCellDelegate: AnyObject, AutoGenerateProtocolDeleg
|
|||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void)
|
||||
// sourcery:end
|
||||
}
|
||||
|
@ -49,6 +53,14 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie
|
|||
delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, spoilerOverlayViewDidPressed: overlayView)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, spoilerBannerViewDidPressed: bannerView)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, actionToolbarContainer: actionToolbarContainer, buttonDidPressed: button, action: action)
|
||||
}
|
||||
|
@ -61,6 +73,14 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie
|
|||
delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, metaText: metaText, didSelectMeta: meta)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, spoilerOverlayViewDidPressed: overlayView)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, spoilerBannerViewDidPressed: bannerView)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, accessibilityActivate: accessibilityActivate)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
|
@ -94,6 +95,30 @@ extension NotificationTimelineViewController {
|
|||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !viewModel.isLoadingLatest {
|
||||
let now = Date()
|
||||
if let timestamp = viewModel.lastAutomaticFetchTimestamp {
|
||||
if now.timeIntervalSince(timestamp) > 60 {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): auto fetch latest timeline…")
|
||||
Task {
|
||||
await viewModel.loadLatest()
|
||||
}
|
||||
viewModel.lastAutomaticFetchTimestamp = now
|
||||
} else {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): auto fetch latest timeline skip. Reason: updated in recent 60s")
|
||||
}
|
||||
} else {
|
||||
Task {
|
||||
await viewModel.loadLatest()
|
||||
}
|
||||
viewModel.lastAutomaticFetchTimestamp = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewController {
|
||||
|
@ -150,4 +175,46 @@ extension NotificationTimelineViewController: UITableViewDelegate, AutoGenerateT
|
|||
}
|
||||
|
||||
// MARK: - NotificationTableViewCellDelegate
|
||||
extension NotificationTimelineViewController: NotificationTableViewCellDelegate { }
|
||||
extension NotificationTimelineViewController: NotificationTableViewCellDelegate {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
statusView: StatusView,
|
||||
spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView
|
||||
) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
guard let reloadItem = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .notification(notification) = item else {
|
||||
assertionFailure("only works for notification item")
|
||||
return
|
||||
}
|
||||
let _status: ManagedObjectRecord<Status>? = try await self.context.managedObjectContext.perform {
|
||||
guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil }
|
||||
guard let status = notification.status else { return nil }
|
||||
return .init(objectID: status.objectID)
|
||||
}
|
||||
guard let status = _status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
try await DataSourceFacade.responseToToggleSensitiveAction(
|
||||
dependency: self,
|
||||
status: status
|
||||
)
|
||||
|
||||
// var snapshot = diffableDataSource.snapshot()
|
||||
// snapshot.reloadItems([reloadItem])
|
||||
// diffableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
} // end Task
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ final class NotificationTimelineViewModel {
|
|||
let scope: Scope
|
||||
let feedFetchedResultsController: FeedFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
@Published var isLoadingLatest = false
|
||||
@Published var lastAutomaticFetchTimestamp: Date?
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>?
|
||||
|
@ -144,6 +146,10 @@ extension NotificationTimelineViewModel {
|
|||
// load lastest
|
||||
func loadLatest() async {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
|
||||
isLoadingLatest = true
|
||||
defer{ isLoadingLatest = false }
|
||||
|
||||
do {
|
||||
_ = try await context.apiService.notifications(
|
||||
maxID: nil,
|
||||
|
|
|
@ -30,6 +30,7 @@ final class UserTimelineViewController: UIViewController, NeedsDependency, Media
|
|||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.register(TimelineHeaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineHeaderTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 100
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
return tableView
|
||||
|
|
|
@ -294,32 +294,19 @@ extension SettingsViewController {
|
|||
}
|
||||
|
||||
func signOut() {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
// clear badge before sign-out
|
||||
context.notificationService.clearNotificationCountForActiveUser()
|
||||
|
||||
context.authenticationService.signOutMastodonUser(
|
||||
domain: activeMastodonAuthenticationBox.domain,
|
||||
userID: activeMastodonAuthenticationBox.userID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isSignOut):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail")
|
||||
guard isSignOut else { return }
|
||||
Task { @MainActor in
|
||||
try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox)
|
||||
self.coordinator.setup()
|
||||
self.coordinator.setupOnboardingIfNeeds(animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -264,6 +264,7 @@ extension StatusView {
|
|||
.assign(to: \.isContentSensitiveToggled, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
// viewModel.source = status.source
|
||||
}
|
||||
|
||||
|
|
|
@ -53,9 +53,11 @@ extension StatusTableViewCell {
|
|||
|
||||
self.delegate = delegate
|
||||
|
||||
statusView.viewModel.isNeedsTableViewUpdate
|
||||
statusView.viewModel.$isContentReveal
|
||||
.removeDuplicates()
|
||||
.dropFirst()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak tableView, weak self] in
|
||||
.sink { [weak tableView, weak self] _ in
|
||||
guard let tableView = tableView else { return }
|
||||
guard let _ = self else { return }
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ extension APIService {
|
|||
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
|
||||
assertionFailure()
|
||||
// assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -50,20 +50,18 @@ extension APIService {
|
|||
}
|
||||
|
||||
func cancelSubscription(
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.EmptySubscription>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
|
||||
return Mastodon.API.Subscriptions.removeSubscription(
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.EmptySubscription> {
|
||||
let response = try await Mastodon.API.Subscriptions.removeSubscription(
|
||||
session: session,
|
||||
domain: domain,
|
||||
authorization: authorization
|
||||
)
|
||||
.handleEvents(receiveOutput: { _ in
|
||||
).singleOutput()
|
||||
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: cancel subscription successful", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -137,58 +137,41 @@ extension AuthenticationService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func signOutMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
|
||||
var isSignOut = false
|
||||
|
||||
var _mastodonAuthenticationBox: MastodonAuthenticationBox?
|
||||
func signOutMastodonUser(
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws {
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
return managedObjectContext.performChanges {
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
||||
request.fetchLimit = 1
|
||||
guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else {
|
||||
return
|
||||
}
|
||||
_mastodonAuthenticationBox = MastodonAuthenticationBox(
|
||||
authenticationRecord: .init(objectID: mastodonAuthentication.objectID),
|
||||
domain: mastodonAuthentication.domain,
|
||||
userID: mastodonAuthentication.userID,
|
||||
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken),
|
||||
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
|
||||
try await managedObjectContext.performChanges {
|
||||
// remove Feed
|
||||
let request = Feed.sortedFetchRequest
|
||||
request.predicate = Feed.predicate(
|
||||
acct: .mastodon(
|
||||
domain: authenticationBox.domain,
|
||||
userID: authenticationBox.userID
|
||||
)
|
||||
|
||||
// remove home timeline indexes
|
||||
let homeTimelineIndexRequest = HomeTimelineIndex.sortedFetchRequest
|
||||
homeTimelineIndexRequest.predicate = HomeTimelineIndex.predicate(
|
||||
domain: mastodonAuthentication.domain,
|
||||
userID: mastodonAuthentication.userID
|
||||
)
|
||||
let homeTimelineIndexes = managedObjectContext.safeFetch(homeTimelineIndexRequest)
|
||||
for homeTimelineIndex in homeTimelineIndexes {
|
||||
managedObjectContext.delete(homeTimelineIndex)
|
||||
let feeds = managedObjectContext.safeFetch(request)
|
||||
for feed in feeds {
|
||||
managedObjectContext.delete(feed)
|
||||
}
|
||||
|
||||
// remove user authentication
|
||||
managedObjectContext.delete(mastodonAuthentication)
|
||||
isSignOut = true
|
||||
}
|
||||
.flatMap { result -> AnyPublisher<Result<Void, Error>, Never> in
|
||||
guard let apiService = self.apiService,
|
||||
let mastodonAuthenticationBox = _mastodonAuthenticationBox else {
|
||||
return Just(result).eraseToAnyPublisher()
|
||||
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) else {
|
||||
assertionFailure()
|
||||
throw APIService.APIError.implicit(.authenticationMissing)
|
||||
}
|
||||
|
||||
return apiService.cancelSubscription(
|
||||
mastodonAuthenticationBox: mastodonAuthenticationBox
|
||||
managedObjectContext.delete(authentication)
|
||||
}
|
||||
|
||||
// cancel push notification subscription
|
||||
do {
|
||||
_ = try await apiService?.cancelSubscription(
|
||||
domain: authenticationBox.domain,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
)
|
||||
.map { _ in result }
|
||||
.catch { _ in Just(result).eraseToAnyPublisher() }
|
||||
.eraseToAnyPublisher()
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
.map { result in
|
||||
return result.map { isSignOut }
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -121,57 +121,20 @@ extension NotificationService {
|
|||
return _notificationSubscription
|
||||
}
|
||||
|
||||
func handle(mastodonPushNotification: MastodonPushNotification) {
|
||||
func handle(
|
||||
pushNotification: MastodonPushNotification
|
||||
) {
|
||||
defer {
|
||||
unreadNotificationCountDidUpdate.send()
|
||||
}
|
||||
|
||||
// Subscription maybe failed to cancel when sign-out
|
||||
// Try cancel again if receive that kind push notification
|
||||
guard let managedObjectContext = authenticationService?.managedObjectContext else { return }
|
||||
guard let apiService = apiService else { return }
|
||||
Task {
|
||||
// trigger notification timeline update
|
||||
try? await fetchLatestNotifications(pushNotification: pushNotification)
|
||||
|
||||
managedObjectContext.perform {
|
||||
let subscriptionRequest = NotificationSubscription.sortedFetchRequest
|
||||
subscriptionRequest.predicate = NotificationSubscription.predicate(userToken: mastodonPushNotification.accessToken)
|
||||
let subscriptions = managedObjectContext.safeFetch(subscriptionRequest)
|
||||
|
||||
// note: assert setting remove after cancel subscription
|
||||
guard let subscription = subscriptions.first else { return }
|
||||
guard let setting = subscription.setting else { return }
|
||||
let domain = setting.domain
|
||||
let userID = setting.userID
|
||||
|
||||
let authenticationRequest = MastodonAuthentication.sortedFetchRequest
|
||||
authenticationRequest.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
||||
guard let authentication = managedObjectContext.safeFetch(authenticationRequest).first else {
|
||||
// do nothing if still sign-in
|
||||
return
|
||||
}
|
||||
|
||||
// cancel subscription if sign-out
|
||||
let accessToken = mastodonPushNotification.accessToken
|
||||
let mastodonAuthenticationBox = MastodonAuthenticationBox(
|
||||
authenticationRecord: .init(objectID: authentication.objectID),
|
||||
domain: domain,
|
||||
userID: userID,
|
||||
appAuthorization: .init(accessToken: accessToken),
|
||||
userAuthorization: .init(accessToken: accessToken)
|
||||
)
|
||||
apiService
|
||||
.cancelSubscription(mastodonAuthenticationBox: mastodonAuthenticationBox)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] failed to cancel sign-out user subscription: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] cancel sign-out user subscription", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
}
|
||||
// cancel sign-out account push notification subscription
|
||||
try? await cancelSubscriptionForDetachedAccount(pushNotification: pushNotification)
|
||||
} // end Task
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -187,6 +150,92 @@ extension NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
extension NotificationService {
|
||||
private func fetchLatestNotifications(
|
||||
pushNotification: MastodonPushNotification
|
||||
) async throws {
|
||||
guard let apiService = apiService else { return }
|
||||
guard let authenticationBox = try await authenticationBox(for: pushNotification) else { return }
|
||||
|
||||
_ = try await apiService.notifications(
|
||||
maxID: nil,
|
||||
scope: .everything,
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
}
|
||||
|
||||
private func cancelSubscriptionForDetachedAccount(
|
||||
pushNotification: MastodonPushNotification
|
||||
) async throws {
|
||||
// Subscription maybe failed to cancel when sign-out
|
||||
// Try cancel again if receive that kind push notification
|
||||
guard let managedObjectContext = authenticationService?.managedObjectContext else { return }
|
||||
guard let apiService = apiService else { return }
|
||||
|
||||
let userAccessToken = pushNotification.accessToken
|
||||
|
||||
let needsCancelSubscription: Bool = try await managedObjectContext.perform {
|
||||
// check authentication exists
|
||||
let authenticationRequest = MastodonAuthentication.sortedFetchRequest
|
||||
authenticationRequest.predicate = MastodonAuthentication.predicate(userAccessToken: userAccessToken)
|
||||
return managedObjectContext.safeFetch(authenticationRequest).first == nil
|
||||
}
|
||||
|
||||
guard needsCancelSubscription else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let domain = try await domain(for: pushNotification) else { return }
|
||||
|
||||
do {
|
||||
_ = try await apiService.cancelSubscription(
|
||||
domain: domain,
|
||||
authorization: .init(accessToken: userAccessToken)
|
||||
)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] cancel sign-out user subscription", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
} catch {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] failed to cancel sign-out user subscription: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func domain(for pushNotification: MastodonPushNotification) async throws -> String? {
|
||||
guard let authenticationService = self.authenticationService else { return nil }
|
||||
let managedObjectContext = authenticationService.managedObjectContext
|
||||
return try await managedObjectContext.perform {
|
||||
let subscriptionRequest = NotificationSubscription.sortedFetchRequest
|
||||
subscriptionRequest.predicate = NotificationSubscription.predicate(userToken: pushNotification.accessToken)
|
||||
let subscriptions = managedObjectContext.safeFetch(subscriptionRequest)
|
||||
|
||||
// note: assert setting not remove after sign-out
|
||||
guard let subscription = subscriptions.first else { return nil }
|
||||
guard let setting = subscription.setting else { return nil }
|
||||
let domain = setting.domain
|
||||
|
||||
return domain
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? {
|
||||
guard let authenticationService = self.authenticationService else { return nil }
|
||||
let managedObjectContext = authenticationService.managedObjectContext
|
||||
return try await managedObjectContext.perform {
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
request.predicate = MastodonAuthentication.predicate(userAccessToken: pushNotification.accessToken)
|
||||
request.fetchLimit = 1
|
||||
guard let authentication = managedObjectContext.safeFetch(request).first else { return nil }
|
||||
|
||||
return MastodonAuthenticationBox(
|
||||
authenticationRecord: .init(objectID: authentication.objectID),
|
||||
domain: authentication.domain,
|
||||
userID: authentication.userID,
|
||||
appAuthorization: .init(accessToken: authentication.appAccessToken),
|
||||
userAuthorization: .init(accessToken: authentication.userAccessToken)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NotificationViewModel
|
||||
|
||||
extension NotificationService {
|
||||
|
|
|
@ -90,19 +90,19 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification]", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let mastodonPushNotification = AppDelegate.mastodonPushNotification(from: notification) else {
|
||||
guard let pushNotification = AppDelegate.mastodonPushNotification(from: notification) else {
|
||||
completionHandler([])
|
||||
return
|
||||
}
|
||||
|
||||
let notificationID = String(mastodonPushNotification.notificationID)
|
||||
let notificationID = String(pushNotification.notificationID)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID)
|
||||
|
||||
let accessToken = mastodonPushNotification.accessToken
|
||||
let accessToken = pushNotification.accessToken
|
||||
UserDefaults.shared.increaseNotificationCount(accessToken: accessToken)
|
||||
appContext.notificationService.applicationIconBadgeNeedsUpdate.send()
|
||||
|
||||
appContext.notificationService.handle(mastodonPushNotification: mastodonPushNotification)
|
||||
appContext.notificationService.handle(pushNotification: pushNotification)
|
||||
completionHandler([.sound])
|
||||
}
|
||||
|
||||
|
@ -114,15 +114,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification]", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
guard let mastodonPushNotification = AppDelegate.mastodonPushNotification(from: response.notification) else {
|
||||
guard let pushNotification = AppDelegate.mastodonPushNotification(from: response.notification) else {
|
||||
completionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
let notificationID = String(mastodonPushNotification.notificationID)
|
||||
let notificationID = String(pushNotification.notificationID)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID)
|
||||
appContext.notificationService.handle(mastodonPushNotification: mastodonPushNotification)
|
||||
appContext.notificationService.requestRevealNotificationPublisher.send(mastodonPushNotification)
|
||||
appContext.notificationService.handle(pushNotification: pushNotification)
|
||||
appContext.notificationService.requestRevealNotificationPublisher.send(pushNotification)
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
|
|
|
@ -39,15 +39,6 @@
|
|||
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="feeds" inverseEntity="Notification"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="feeds" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="HomeTimelineIndex" representedClassName="CoreDataStack.HomeTimelineIndex" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="hasMore" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="userID" attributeType="String"/>
|
||||
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="Instance" representedClassName="CoreDataStack.Instance" syncable="YES">
|
||||
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
@ -262,7 +253,6 @@
|
|||
<element name="DomainBlock" positionX="45" positionY="162" width="128" height="89"/>
|
||||
<element name="Emoji" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Feed" positionX="54" positionY="171" width="128" height="149"/>
|
||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Instance" positionX="45" positionY="162" width="128" height="104"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="224"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="734"/>
|
||||
|
|
|
@ -74,7 +74,7 @@ extension Feed {
|
|||
return NSPredicate(format: "%K == %@", #keyPath(Feed.kindRaw), kind.rawValue)
|
||||
}
|
||||
|
||||
static func predicate(acct: Acct) -> NSPredicate {
|
||||
public static func predicate(acct: Acct) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(Feed.acctRaw), acct.rawValue)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
//
|
||||
// HomeTimelineIndex.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final public class HomeTimelineIndex: NSManagedObject {
|
||||
|
||||
public typealias ID = String
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var domain: String
|
||||
@NSManaged public private(set) var userID: String
|
||||
|
||||
@NSManaged public private(set) var hasMore: Bool // default NO
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var deletedAt: Date?
|
||||
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var status: Status
|
||||
|
||||
}
|
||||
|
||||
extension HomeTimelineIndex {
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
status: Status
|
||||
) -> HomeTimelineIndex {
|
||||
let index: HomeTimelineIndex = context.insertObject()
|
||||
|
||||
index.identifier = property.identifier
|
||||
index.domain = property.domain
|
||||
index.userID = property.userID
|
||||
index.createdAt = status.createdAt
|
||||
|
||||
index.status = status
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
public func update(hasMore: Bool) {
|
||||
if self.hasMore != hasMore {
|
||||
self.hasMore = hasMore
|
||||
}
|
||||
}
|
||||
|
||||
// internal method for status call
|
||||
func softDelete() {
|
||||
deletedAt = Date()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HomeTimelineIndex {
|
||||
public struct Property {
|
||||
public let identifier: String
|
||||
public let domain: String
|
||||
public let userID: String
|
||||
|
||||
public init(domain: String, userID: String) {
|
||||
self.identifier = UUID().uuidString + "@" + domain
|
||||
self.domain = domain
|
||||
self.userID = userID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineIndex: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \HomeTimelineIndex.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
extension HomeTimelineIndex {
|
||||
|
||||
static func predicate(domain: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(HomeTimelineIndex.domain), domain)
|
||||
}
|
||||
|
||||
static func predicate(userID: MastodonUser.ID) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(HomeTimelineIndex.userID), userID)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, userID: MastodonUser.ID) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
predicate(domain: domain),
|
||||
predicate(userID: userID)
|
||||
])
|
||||
}
|
||||
|
||||
public static func notDeleted() -> NSPredicate {
|
||||
return NSPredicate(format: "%K == nil", #keyPath(HomeTimelineIndex.deletedAt))
|
||||
}
|
||||
|
||||
}
|
|
@ -18,10 +18,15 @@ public protocol NotificationViewDelegate: AnyObject {
|
|||
func notificationView(_ notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action)
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||
|
||||
// a11y
|
||||
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void)
|
||||
|
@ -384,12 +389,26 @@ extension NotificationView: StatusViewDelegate {
|
|||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) {
|
||||
switch statusView {
|
||||
case self.statusView:
|
||||
delegate?.notificationView(self, statusView: statusView, spoilerOverlayViewDidPressed: overlayView)
|
||||
case quoteStatusView:
|
||||
delegate?.notificationView(self, quoteStatusView: statusView, spoilerOverlayViewDidPressed: overlayView)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView) {
|
||||
switch statusView {
|
||||
case self.statusView:
|
||||
delegate?.notificationView(self, statusView: statusView, spoilerBannerViewDidPressed: bannerView)
|
||||
case quoteStatusView:
|
||||
delegate?.notificationView(self, quoteStatusView: statusView, spoilerBannerViewDidPressed: bannerView)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
||||
assertionFailure()
|
||||
|
|
|
@ -89,8 +89,6 @@ extension StatusView {
|
|||
@Published public var reblogCount: Int = 0
|
||||
@Published public var favoriteCount: Int = 0
|
||||
|
||||
public let isNeedsTableViewUpdate = PassthroughSubject<Void, Never>()
|
||||
|
||||
@Published public var groupedAccessibilityLabel = ""
|
||||
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
|
@ -136,8 +134,22 @@ extension StatusView {
|
|||
|
||||
init() {
|
||||
// isReblogEnabled
|
||||
$locked
|
||||
.map { !$0 }
|
||||
Publishers.CombineLatest(
|
||||
$visibility,
|
||||
$isMyself
|
||||
)
|
||||
.map { visibility, isMyself in
|
||||
if isMyself {
|
||||
return true
|
||||
}
|
||||
|
||||
switch visibility {
|
||||
case .public, .unlisted:
|
||||
return true
|
||||
case .private, .direct, ._other:
|
||||
return false
|
||||
}
|
||||
}
|
||||
.assign(to: &$isReblogEnabled)
|
||||
// isContentSensitive
|
||||
$spoilerContent
|
||||
|
@ -292,7 +304,7 @@ extension StatusView.ViewModel {
|
|||
|
||||
statusView.setSpoilerOverlayViewHidden(isHidden: isContentReveal)
|
||||
|
||||
self.isNeedsTableViewUpdate.send()
|
||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): isContentReveal: \(isContentReveal)")
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// visibility
|
||||
|
|
|
@ -571,7 +571,6 @@ extension StatusView.Style {
|
|||
statusView.headerContainerView.removeFromSuperview()
|
||||
statusView.authorContainerView.removeFromSuperview()
|
||||
statusView.statusVisibilityView.removeFromSuperview()
|
||||
statusView.spoilerBannerView.removeFromSuperview()
|
||||
}
|
||||
|
||||
func notificationQuote(statusView: StatusView) {
|
||||
|
@ -580,7 +579,6 @@ extension StatusView.Style {
|
|||
statusView.contentContainer.layoutMargins.bottom = 16 // fix contentText align to edge issue
|
||||
statusView.menuButton.removeFromSuperview()
|
||||
statusView.statusVisibilityView.removeFromSuperview()
|
||||
statusView.spoilerBannerView.removeFromSuperview()
|
||||
statusView.actionToolbarContainer.removeFromSuperview()
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ public final class SpoilerBannerView: UIView {
|
|||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
label.numberOfLines = 0
|
||||
label.text = "Hide" // TODO: i18n
|
||||
return label
|
||||
}()
|
||||
|
@ -75,8 +74,8 @@ extension SpoilerBannerView {
|
|||
])
|
||||
|
||||
labelContainer.addArrangedSubview(label)
|
||||
labelContainer.addArrangedSubview(UIView())
|
||||
labelContainer.addArrangedSubview(hideLabel)
|
||||
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
hideLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
hideLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
|
||||
|
|
Loading…
Reference in New Issue