mastodon-ios/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift

243 lines
10 KiB
Swift
Raw Normal View History

2021-09-22 13:08:09 +02:00
//
// SidebarViewModel.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-9-22.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
2021-09-24 13:58:50 +02:00
import Meta
import MastodonMeta
Release v1.3.0 (#347) * New translations app.json (Thai) * New translations app.json (Spanish) * New translations Localizable.stringsdict (Spanish) * New translations app.json (Thai) * New translations app.json (Thai) * feat: adapt the app to async & await. Update timeline UI * fix: update the Xcode version to fix the CI failure * fix: remove unavailable framework import * fix: project dependency issue * feat: add content warning for post spoiler * feat: add content warning for post media * chore: update version to 1.3.0 (92) * New translations app.json (French) * New translations Intents.strings (French) * New translations app.json (Thai) * feat: update report flow * feat: update setting scene UI * feat: update status content warning UI * feat: add notification gap fetcher * chore: update version to 1.3.0 (93) * feat: add video player for audio/video kind media * chore: update version to 1.3.0 (94) * fix: text strip wrong color in the Dark Mode issue * chore: remove spoiler toggle animation for table cell * fix: add missing shadow for compose publish button * fix: add missing margin for timeline with horizontal regular size class * fix: profile segmented controls missing margin issue * fix: the profile segmented control use wrong selection tint color under force light UI style issue * fix: add notification count clear logic back * fix: add missing home timeline bottom fetcher * fix: [WIP] add suggestion account scene back * New translations app.json (Kabyle) * New translations ios-infoPlist.json (Kabyle) * New translations Localizable.stringsdict (Kabyle) * New translations Intents.strings (Kabyle) * New translations Intents.stringsdict (Kabyle) * feat: make the home timeline readable for VoiceOver * chore: update version to 1.3.0 (95) * New translations app.json (French) * New translations Intents.strings (French) * New translations app.json (Kabyle) * New translations ios-infoPlist.json (Kabyle) * New translations Localizable.stringsdict (Kabyle) * New translations Intents.strings (Kabyle) * New translations Intents.stringsdict (Kabyle) * New translations Localizable.stringsdict (French) * New translations app.json (Kabyle) * New translations app.json (French) * chore: update action toolbar icons * fix: instal state missing issue * fix: follow push notification deep-link not works issue * fix: foreground notification not trigger tab bell icon update issue * feat: add notification timeline fetcher * feat: add content warning toggle button * chore: update version to 1.3.0 (96) * New translations app.json (Thai) * New translations app.json (Russian) * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Scottish Gaelic) * New translations app.json (Welsh) * New translations app.json (Hindi) * New translations app.json (Spanish, Argentina) * New translations app.json (Indonesian) * New translations app.json (Portuguese, Brazilian) * New translations app.json (English) * New translations app.json (Chinese Traditional) * New translations app.json (Chinese Simplified) * New translations app.json (Swedish) * New translations app.json (Portuguese) * New translations app.json (Dutch) * New translations app.json (Korean) * New translations app.json (Japanese) * New translations app.json (Basque) * New translations app.json (German) * New translations app.json (Danish) * New translations app.json (Catalan) * New translations app.json (Arabic) * New translations app.json (Spanish) * New translations app.json (Romanian) * New translations app.json (Kabyle) * New translations app.json (French) * New translations app.json (Swedish, Finland) * New translations app.json (Spanish, Argentina) * New translations app.json (Kurmanji (Kurdish)) * fix: notification i18n word typo * New translations app.json (Thai) * New translations app.json (Swedish) * New translations Localizable.stringsdict (Swedish) * New translations app.json (Swedish, Finland) * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Scottish Gaelic) * New translations app.json (Welsh) * New translations app.json (Hindi) * New translations app.json (Indonesian) * New translations app.json (Portuguese, Brazilian) * New translations app.json (English) * New translations app.json (Chinese Traditional) * New translations app.json (Chinese Simplified) * New translations app.json (Russian) * New translations app.json (Portuguese) * New translations app.json (Dutch) * New translations app.json (Korean) * New translations app.json (Japanese) * New translations app.json (Basque) * New translations app.json (German) * New translations app.json (Danish) * New translations app.json (Catalan) * New translations app.json (Arabic) * New translations app.json (Spanish) * New translations app.json (Romanian) * New translations app.json (Kabyle) * New translations app.json (French) * New translations Intents.strings (Swedish) * New translations app.json (Swedish) * New translations Localizable.stringsdict (Japanese) * New translations app.json (Thai) * New translations app.json (Thai) * New translations Localizable.stringsdict (Swedish) * New translations app.json (Kabyle) * New translations ios-infoPlist.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (French) * New translations app.json (French) * feat: restore scroll-to-top tap gesture for TabBar * feat: add cell height cache for user timeline * feat: display no results when profile field empty * New translations app.json (Chinese Traditional) * New translations app.json (Chinese Traditional) * New translations Intents.strings (Japanese) * feat: make status detail accessible * chore: restore the appearance settings * chore: update version to 1.3.0 (97) * New translations app.json (Kabyle) * New translations Intents.strings (Japanese) * New translations app.json (Swedish) * New translations app.json (Basque) * New translations app.json (Basque) * chore: add a11y hint for profile dashboard * feat: add media interaction for notification timeline * New translations app.json (Chinese Simplified) * New translations app.json (Chinese Simplified) * chore: update i18n strings * fix: setting switch use wrong tint color issue * chore: restore RTL layout for post content * chore: update profile relationship button UI * chore: update color panel * fix: post reblog header may display empty reblogger name issue * fix: wrong reply header redirect logic issue * feat: restore post filter supports * chore: update version to 1.3.0 (98) * chore: update post content sensitive style * fix: blurhash image not display during image loading issue * chore: update version to 1.3.0 (99) * feat: restore user recommend scene * chore: update badge tint color * feat: restore keyboard shortcut supports * chore: update version to 1.3.0 (100) * fix: relationship background use wrong color when force dark style * fix: player button icon not reset issue * chore: update version to 1.3.0 (101) * fix: profile relationship button fill the width on iPad issue * fix: inputAssistantItem duplicate setup issue * chore: update textView minimum height from 88 to 64 * chore: update version to 1.3.0 (102) * chore: update status timeline margin * chore: update sidebar background color * fix: split view column state after size class transition not stable issue * chore: update notification timeline margin * chore: update profile header and segmented bar margin * fix: profile segmented bar use wrong tint color when force Dark Mode issue * chore: update horizontal compact mode notification timeline margin looks like * chore: update version to 1.3.0 (103) * feat: dismiss image preview when tap empty area * chore: update version to 1.3.0 (104) * New translations app.json (Italian) * New translations ios-infoPlist.json (Italian) * New translations Localizable.stringsdict (Italian) * New translations Intents.strings (Italian) * New translations Intents.stringsdict (Italian) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Japanese) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Spanish) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations ios-infoPlist.json (Kabyle) * New translations Localizable.stringsdict (Kabyle) * New translations Localizable.stringsdict (Kabyle) * New translations Intents.strings (Kabyle) * New translations app.json (Kabyle) * New translations Intents.strings (Kabyle) * New translations Intents.stringsdict (Kabyle) * New translations app.json (Kabyle) * New translations app.json (Scottish Gaelic) * New translations app.json (Scottish Gaelic) * New translations app.json (Thai) * New translations app.json (Thai) * feat: add UITests for snapshots * feat: add snapshot UITest and document * New translations app.json (Thai) * feat: add notification snapshot * chore: add domain and update guide for the snapshot UITest * chore: use the first photo for compose snapshot * New translations app.json (Thai) * New translations app.json (German) * New translations app.json (German) * chore: update settings scene UI * chore: update i18n for open link words * chore: update i18n resources * fix: share extension not accept plaintext content issue. resolve #335 * chore: update version to 1.3.0 (105) * New translations app.json (Japanese) * New translations app.json (Japanese) * New translations app.json (Japanese) * feat: add onion domain ATS exception rule. resolve #338 * chore: update app version footer and i18n strings * chore: update version to 1.3.0 (106) * chore: update version to 1.3.0 (108) * Handle onboarding authentication errors in /api/v1/instance * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Kurmanji (Kurdish)) * chore: update Xcode schemes index * chore: update the snapshot documents and UITests * chore: update i18n resources. resolve #343 * chore: retain the API model semantic * fix: force LTR for some text fields. #318 * fix: textView break IME input issue. resolve #342 * chore: update version to 1.3.0 (109) * chore: update README * chore: fix typo * chore: add bug report template and contributing document Co-authored-by: Eugen Rochko <eugen@zeonfederated.com> Co-authored-by: Zac West <zacwest@gmail.com>
2022-03-29 11:51:14 +02:00
import MastodonAsset
import MastodonLocalization
2021-09-22 13:08:09 +02:00
final class SidebarViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
@Published private var isSidebarDataSourceReady = false
@Published private var isAvatarButtonDataReady = false
@Published var currentTab: MainTabBarController.Tab = .home
2021-09-22 13:08:09 +02:00
// output
var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
2021-10-28 13:17:41 +02:00
var secondaryDiffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
@Published private(set) var isReadyForWizardAvatarButton = false
2021-10-28 13:17:41 +02:00
let activeMastodonAuthenticationObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil)
2021-09-22 13:08:09 +02:00
init(context: AppContext) {
self.context = context
Publishers.CombineLatest(
$isSidebarDataSourceReady,
$isAvatarButtonDataReady
)
.map { $0 && $1 }
.assign(to: &$isReadyForWizardAvatarButton)
context.authenticationService.activeMastodonAuthentication
.sink { [weak self] authentication in
guard let self = self else { return }
// bind objectID
self.activeMastodonAuthenticationObjectID.value = authentication?.objectID
self.isAvatarButtonDataReady = authentication != nil
}
.store(in: &disposeBag)
2021-09-22 13:08:09 +02:00
}
}
extension SidebarViewModel {
enum Section: Int, Hashable, CaseIterable {
2021-10-28 13:17:41 +02:00
case main
case secondary
2021-09-22 13:08:09 +02:00
}
enum Item: Hashable {
case tab(MainTabBarController.Tab)
2021-10-28 13:17:41 +02:00
case setting
case compose
2021-09-22 13:08:09 +02:00
}
}
extension SidebarViewModel {
func setupDiffableDataSource(
2021-10-28 13:17:41 +02:00
collectionView: UICollectionView,
secondaryCollectionView: UICollectionView
2021-09-22 13:08:09 +02:00
) {
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { [weak self] cell, indexPath, item in
guard let self = self else { return }
2021-09-24 13:58:50 +02:00
let imageURL: URL? = {
switch item {
case .me:
let authentication = self.context.authenticationService.activeMastodonAuthentication.value
return authentication?.user.avatarImageURL()
default:
return nil
}
}()
cell.item = SidebarListContentView.Item(
isActive: false,
2021-10-28 13:17:41 +02:00
title: item.title,
image: item.image,
activeImage: item.selectedImage,
2021-10-28 13:17:41 +02:00
imageURL: imageURL
2021-09-24 13:58:50 +02:00
)
cell.setNeedsUpdateConfiguration()
cell.isAccessibilityElement = true
cell.accessibilityLabel = item.title
self.$currentTab
.receive(on: DispatchQueue.main)
.sink { [weak cell] currentTab in
guard let cell = cell else { return }
cell.item?.isActive = currentTab == item
cell.setNeedsUpdateConfiguration()
}
.store(in: &cell.disposeBag)
switch item {
case .notification:
Publishers.CombineLatest3(
self.context.authenticationService.activeMastodonAuthentication,
self.context.notificationService.unreadNotificationCountDidUpdate,
self.$currentTab
)
.receive(on: DispatchQueue.main)
.sink { [weak cell] authentication, _, currentTab in
guard let cell = cell else { return }
let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken)
return count > 0
} ?? false
let image: UIImage = {
if currentTab == .notification {
return hasUnreadPushNotification ? Asset.ObjectsAndTools.bellBadgeFill.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectsAndTools.bellFill.image.withRenderingMode(.alwaysTemplate)
} else {
return hasUnreadPushNotification ? Asset.ObjectsAndTools.bellBadge.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate)
}
}()
cell.item?.image = image
cell.item?.activeImage = image
cell.setNeedsUpdateConfiguration()
}
.store(in: &cell.disposeBag)
case .me:
guard let authentication = self.context.authenticationService.activeMastodonAuthentication.value else { break }
let currentUserDisplayName = authentication.user.displayNameWithFallback
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
default:
break
}
}
2021-10-28 13:17:41 +02:00
let cellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, SidebarListContentView.Item> { [weak self] cell, indexPath, item in
guard let _ = self else { return }
2021-10-28 13:17:41 +02:00
cell.item = item
cell.setNeedsUpdateConfiguration()
cell.isAccessibilityElement = true
cell.accessibilityLabel = item.title
2021-09-22 13:08:09 +02:00
}
2021-10-28 13:17:41 +02:00
// header
let headerRegistration = UICollectionView.SupplementaryRegistration<SidebarListHeaderView>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
// do nothing
2021-09-22 13:08:09 +02:00
}
let _diffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
2021-09-22 13:08:09 +02:00
switch item {
case .tab(let tab):
2021-09-24 13:58:50 +02:00
return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab)
2021-10-28 13:17:41 +02:00
case .setting:
let item = SidebarListContentView.Item(
isActive: false,
2021-10-28 13:17:41 +02:00
title: L10n.Common.Controls.Actions.settings,
image: Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate),
activeImage: Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate),
2021-10-28 13:17:41 +02:00
imageURL: nil
)
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
case .compose:
let item = SidebarListContentView.Item(
isActive: false,
2021-11-10 11:00:16 +01:00
title: L10n.Common.Controls.Actions.compose,
image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate),
activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate),
2021-10-28 13:17:41 +02:00
imageURL: nil
)
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
}
_diffableDataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
switch elementKind {
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
default:
assertionFailure()
return UICollectionReusableView()
2021-09-22 13:08:09 +02:00
}
}
diffableDataSource = _diffableDataSource
2021-09-22 13:08:09 +02:00
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
2021-10-28 13:17:41 +02:00
snapshot.appendSections([.main])
2021-09-22 13:08:09 +02:00
2021-10-28 13:17:41 +02:00
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let items: [Item] = [
.tab(.home),
.tab(.search),
.tab(.notification),
.tab(.me),
.setting,
]
sectionSnapshot.append(items, to: nil)
// animatingDifferences must to be `true`
// otherwise the UI layout will infinity loop
_diffableDataSource.apply(sectionSnapshot, to: .main, animatingDifferences: true) { [weak self] in
guard let self = self else { return }
self.isSidebarDataSourceReady = true
}
2021-10-28 13:17:41 +02:00
// secondary
let _secondaryDiffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: secondaryCollectionView) { collectionView, indexPath, item in
guard case .compose = item else {
assertionFailure()
return UICollectionViewCell()
2021-09-22 13:08:09 +02:00
}
2021-10-28 13:17:41 +02:00
let item = SidebarListContentView.Item(
isActive: false,
2021-11-10 10:58:37 +01:00
title: L10n.Common.Controls.Actions.compose,
image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate),
activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate),
2021-10-28 13:17:41 +02:00
imageURL: nil
)
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
2021-09-22 13:08:09 +02:00
}
2021-10-28 13:17:41 +02:00
secondaryDiffableDataSource = _secondaryDiffableDataSource
2021-09-22 13:08:09 +02:00
2021-10-28 13:17:41 +02:00
var secondarySnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
secondarySnapshot.appendSections([.secondary])
2021-10-28 13:17:41 +02:00
var secondarySectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let secondarySectionItems: [Item] = [
.compose,
]
secondarySectionSnapshot.append(secondarySectionItems, to: nil)
_secondaryDiffableDataSource.apply(secondarySectionSnapshot, to: .secondary)
2021-09-22 13:08:09 +02:00
}
}