Merge branch /develop into feature/compose

# Conflicts:
#	Mastodon.xcodeproj/project.pbxproj
This commit is contained in:
CMK 2021-03-16 15:59:26 +08:00
commit c5582c7aaf
24 changed files with 520 additions and 64 deletions

View File

@ -174,7 +174,13 @@
}
},
"home_timeline": {
"title": "Home"
"title": "Home",
"navigation_bar_state": {
"offline": "Offline",
"new_posts": "See new posts",
"published": "Published!",
"Publishing": "Publishing post..."
},
},
"public_timeline": {
"title": "Public"
@ -188,4 +194,4 @@
"compose_action": "Publish"
}
}
}
}

View File

@ -49,6 +49,7 @@
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; };
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; };
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; };
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; };
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; };
@ -72,6 +73,9 @@
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */; };
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */; };
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; };
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; };
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
@ -314,6 +318,7 @@
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = "<group>"; };
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = "<group>"; };
2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = "<group>"; };
2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = "<group>"; };
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = "<group>"; };
@ -335,6 +340,9 @@
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarState.swift; sourceTree = "<group>"; };
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarView.swift; sourceTree = "<group>"; };
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = "<group>"; };
2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
@ -619,6 +627,7 @@
children = (
2D152A8B25C295CC009AA50C /* StatusView.swift */,
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
);
path = Content;
sourceTree = "<group>";
@ -644,6 +653,8 @@
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */,
2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */,
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */,
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */,
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */,
);
path = HomeTimeline;
sourceTree = "<group>";
@ -1217,6 +1228,7 @@
2D206B9125F60EA700143C56 /* UIControl.swift */,
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */,
2D84350425FF858100EECE90 /* UIScrollView.swift */,
);
path = Extension;
sourceTree = "<group>";
@ -1678,6 +1690,7 @@
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */,
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
@ -1685,7 +1698,9 @@
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
@ -1725,6 +1740,7 @@
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,

View File

@ -0,0 +1,32 @@
//
// UIScrollView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/15.
//
import UIKit
extension UIScrollView {
public enum ScrollDirection {
case top
case bottom
case left
case right
}
public func scroll(to direction: ScrollDirection, animated: Bool) {
let offset: CGPoint
switch direction {
case .top:
offset = CGPoint(x: contentOffset.x, y: -adjustedContentInset.top)
case .bottom:
offset = CGPoint(x: contentOffset.x, y: max(-adjustedContentInset.top, contentSize.height - frame.height + adjustedContentInset.bottom))
case .left:
offset = CGPoint(x: -adjustedContentInset.left, y: contentOffset.y)
case .right:
offset = CGPoint(x: max(-adjustedContentInset.left, contentSize.width - frame.width + adjustedContentInset.right), y: contentOffset.y)
}
setContentOffset(offset, animated: animated)
}
}

View File

@ -68,6 +68,10 @@ internal enum Asset {
internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
internal static let valid = ColorAsset(name: "Colors/TextField/valid")
}
internal static let backgroundLight = ColorAsset(name: "Colors/backgroundLight")
internal static let buttonDefault = ColorAsset(name: "Colors/buttonDefault")
internal static let buttonDisabled = ColorAsset(name: "Colors/buttonDisabled")
internal static let buttonInactive = ColorAsset(name: "Colors/buttonInactive")
internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow")
internal static let lightBackground = ColorAsset(name: "Colors/lightBackground")
internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue")

View File

@ -182,6 +182,16 @@ internal enum L10n {
internal enum HomeTimeline {
/// Home
internal static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title")
internal enum NavigationBarState {
/// See new posts
internal static let newPosts = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.NewPosts")
/// Offline
internal static let offline = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Offline")
/// Published!
internal static let published = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Published")
/// Publishing post...
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
}
}
internal enum PublicTimeline {
/// Public

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.910",
"green" : "0.882",
"red" : "0.851"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.851",
"green" : "0.565",
"red" : "0.169"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.784",
"green" : "0.682",
"red" : "0.608"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.549",
"green" : "0.510",
"red" : "0.431"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,20 +1,20 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"red" : "0.792",
"blue" : "0.016",
"green" : "0.561",
"red" : "0.792"
"alpha" : "1.000"
}
},
"idiom" : "universal"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
"version" : 1,
"author" : "xcode"
}
}
}

View File

@ -1,20 +1,20 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"red" : "0.875",
"blue" : "0.353",
"green" : "0.251",
"red" : "0.875"
"green" : "0.251"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,6 +1,11 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
@ -9,12 +14,7 @@
"green" : "0.137",
"red" : "0.122"
}
},
"idiom" : "universal"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,20 +1,20 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"blue" : "0.263",
"green" : "0.235",
"alpha" : "0.600",
"blue" : "67",
"green" : "60",
"red" : "60"
}
},
"idiom" : "universal"
"red" : "0.235"
},
"color-space" : "srgb"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,20 +1,20 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.604",
"green" : "0.741",
"red" : "0.475"
"red" : "0.475",
"blue" : "0.604"
}
},
"idiom" : "universal"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,20 +1,20 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.996",
"alpha" : "1.000",
"blue" : "0.996",
"green" : "1.000",
"red" : "0.996"
}
},
"idiom" : "universal"
"green" : "1.000"
},
"color-space" : "srgb"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
"version" : 1,
"author" : "xcode"
}
}
}

View File

@ -53,6 +53,10 @@
"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@,
tap the link to confirm your account.";
"Scene.ConfirmEmail.Title" = "One last thing.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
"Scene.HomeTimeline.Title" = "Home";
"Scene.PublicTimeline.Title" = "Public";
"Scene.Register.Error.Item.Agreement" = "Agreement";
@ -99,4 +103,4 @@ any server.";
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
"Scene.ServerRules.Title" = "Some ground rules.";
"Scene.Welcome.Slogan" = "Social networking
back in your hands.";
back in your hands.";

View File

@ -0,0 +1,156 @@
//
// HomeTimelineNavigationBarState.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/15.
//
import Combine
import Foundation
import UIKit
final class HomeTimelineNavigationBarState {
static let errorCountMax: Int = 3
var disposeBag = Set<AnyCancellable>()
var errorCountDownDispose: AnyCancellable?
var timerDispose: AnyCancellable?
var networkErrorCountSubject = PassthroughSubject<Bool, Never>()
var newTopContent = CurrentValueSubject<Bool, Never>(false)
var hasContentBeforeFetching: Bool = true
weak var viewController: HomeTimelineViewController?
let timestampUpdatePublisher = Timer.publish(every: NavigationBarProgressView.progressAnimationDuration, on: .main, in: .common)
.autoconnect()
.share()
.eraseToAnyPublisher()
init() {
reCountdown()
subscribeNewContent()
addGesture()
}
}
extension HomeTimelineNavigationBarState {
func showOfflineInNavigationBar() {
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.offlineView
}
func showNewPostsInNavigationBar() {
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.newPostsView
}
func showPublishingNewPostInNavigationBar() {
let progressView = HomeTimelineNavigationBarView.progressView
if let navigationBar = viewController?.navigationBar(), progressView.superview == nil {
navigationBar.addSubview(progressView)
NSLayoutConstraint.activate([
progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
progressView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor),
progressView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
progressView.heightAnchor.constraint(equalToConstant: 3)
])
}
progressView.layoutIfNeeded()
progressView.progress = 0
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.publishingLabel
var times: Int = 0
timerDispose = timestampUpdatePublisher
.map { _ in
times += 1
return Double(times)
}
.scan(0) { value, count in
value + 1 / pow(Double(2), count)
}
.receive(on: DispatchQueue.main)
.sink { value in
print(value)
progressView.progress = CGFloat(value)
}
}
func showPublishedInNavigationBar() {
timerDispose = nil
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.publishedView
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.showMastodonLogoInNavigationBar()
}
}
func showMastodonLogoInNavigationBar() {
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView
}
}
extension HomeTimelineNavigationBarState {
func handleScrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffsetY = scrollView.contentOffset.y
let isShowingNewPostsNew = viewController?.navigationItem.titleView === HomeTimelineNavigationBarView.newPostsView
if !isShowingNewPostsNew {
return
}
let isTop = contentOffsetY < -scrollView.contentInset.top
if isTop {
newTopContent.value = false
showMastodonLogoInNavigationBar()
}
}
func addGesture() {
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
tapGesture.addTarget(self, action: #selector(HomeTimelineNavigationBarState.newPostsNewDidPressed(_:)))
HomeTimelineNavigationBarView.newPostsView.addGestureRecognizer(tapGesture)
}
@objc func newPostsNewDidPressed(_ sender: UITapGestureRecognizer) {
if newTopContent.value == true {
viewController?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
}
}
extension HomeTimelineNavigationBarState {
func subscribeNewContent() {
newTopContent
.receive(on: DispatchQueue.main)
.sink { [weak self] newContent in
guard let self = self else { return }
if self.hasContentBeforeFetching, newContent {
self.showNewPostsInNavigationBar()
}
}
.store(in: &disposeBag)
}
func reCountdown() {
errorCountDownDispose = networkErrorCountSubject
.scan(0) { value, _ in value + 1 }
.sink(receiveValue: { [weak self] errorCount in
guard let self = self else { return }
if errorCount >= HomeTimelineNavigationBarState.errorCountMax {
self.showOfflineInNavigationBar()
}
})
}
func receiveCompletion(completion: Subscribers.Completion<Error>) {
switch completion {
case .failure:
networkErrorCountSubject.send(false)
case .finished:
reCountdown()
let isShowingOfflineView = viewController?.navigationItem.titleView === HomeTimelineNavigationBarView.offlineView
if isShowingOfflineView {
showMastodonLogoInNavigationBar()
}
}
}
}

View File

@ -0,0 +1,80 @@
//
// HomeTimelineNavigationBarView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/15.
//
import UIKit
final class HomeTimelineNavigationBarView {
static let mastodonLogoTitleView: UIImageView = {
let imageView = UIImageView(image: Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate))
imageView.tintColor = Asset.Colors.Label.primary.color
return imageView
}()
static let offlineView: UIView = {
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightDangerRed.color)
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.offline)
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
return view
}()
static let newPostsView: UIView = {
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.Button.highlight.color)
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.newPosts)
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
return view
}()
static var publishedView: UIView = {
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightSuccessGreen.color)
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.published)
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
return view
}()
static var progressView: NavigationBarProgressView = {
let view = NavigationBarProgressView()
return view
}()
static var publishingLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .black
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
label.text = L10n.Scene.HomeTimeline.NavigationBarState.publishing
return label
}()
static func addLabelToView(label: UILabel, view: UIView) {
view.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
view.trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: 16),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 1),
view.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 1),
view.heightAnchor.constraint(equalToConstant: 24),
])
}
static func backgroundViewWithColor(color: UIColor) -> UIView {
let view = UIView()
view.backgroundColor = color
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 12
view.clipsToBounds = true
return view
}
static func contentLabel(text: String) -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .white
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))
label.text = text
return label
}
}

View File

@ -64,11 +64,7 @@ extension HomeTimelineViewController {
title = L10n.Scene.HomeTimeline.title
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
navigationItem.titleView = {
let imageView = UIImageView(image: Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate))
imageView.tintColor = Asset.Colors.Label.primary.color
return imageView
}()
navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView
navigationItem.leftBarButtonItem = settingBarButtonItem
#if DEBUG
// long press to trigger debug menu
@ -101,6 +97,7 @@ extension HomeTimelineViewController {
])
viewModel.tableView = tableView
viewModel.viewController = self
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
tableView.delegate = self
viewModel.setupDiffableDataSource(
@ -209,6 +206,7 @@ extension HomeTimelineViewController {
extension HomeTimelineViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
handleScrollViewDidScroll(scrollView)
self.viewModel.homeTimelineNavigationBarState.handleScrollViewDidScroll(scrollView)
}
}

View File

@ -73,6 +73,7 @@ extension HomeTimelineViewModel.LoadLatestState {
stateMachine.enter(Fail.self)
return
}
viewModel.homeTimelineNavigationBarState.hasContentBeforeFetching = !latestTootIDs.isEmpty
let end = CACurrentMediaTime()
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect toots id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
@ -80,6 +81,7 @@ extension HomeTimelineViewModel.LoadLatestState {
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
.receive(on: DispatchQueue.main)
.sink { completion in
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
switch completion {
case .failure(let error):
// TODO: handle error
@ -97,9 +99,12 @@ extension HomeTimelineViewModel.LoadLatestState {
let toots = response.value
let newToots = toots.filter { !latestTootIDs.contains($0.id) }
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new toots", ((#file as NSString).lastPathComponent), #line, #function, newToots.count)
if newToots.isEmpty {
viewModel.isFetchingLatestTimeline.value = false
viewModel.homeTimelineNavigationBarState.newTopContent.value = false
} else {
viewModel.homeTimelineNavigationBarState.newTopContent.value = true
}
}
.store(in: &viewModel.disposeBag)

View File

@ -68,6 +68,7 @@ extension HomeTimelineViewModel.LoadMiddleState {
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { completion in
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
switch completion {
case .failure(let error):
// TODO: handle error

View File

@ -58,6 +58,7 @@ extension HomeTimelineViewModel.LoadOldestState {
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { completion in
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
switch completion {
case .failure(let error):
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)

View File

@ -29,9 +29,16 @@ final class HomeTimelineViewModel: NSObject {
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
let viewDidAppear = PassthroughSubject<Void, Never>()
let homeTimelineNavigationBarState = HomeTimelineNavigationBarState()
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
weak var tableView: UITableView?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
weak var viewController: HomeTimelineViewController? {
willSet(value) {
self.homeTimelineNavigationBarState.viewController = value
}
}
// output
// top loader

View File

@ -0,0 +1,56 @@
//
// NavigationBarProgressView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/16.
//
import UIKit
class NavigationBarProgressView: UIView {
static let progressAnimationDuration: TimeInterval = 0.3
let sliderView: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.buttonDefault.color
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var sliderTrailingAnchor: NSLayoutConstraint!
var progress: CGFloat = 0 {
willSet(value) {
sliderTrailingAnchor.constant = (1 - progress) * bounds.width
UIView.animate(withDuration: NavigationBarProgressView.progressAnimationDuration) {
self.setNeedsLayout()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension NavigationBarProgressView {
func _init() {
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = .clear
addSubview(sliderView)
sliderTrailingAnchor = trailingAnchor.constraint(equalTo: sliderView.trailingAnchor)
NSLayoutConstraint.activate([
sliderView.topAnchor.constraint(equalTo: topAnchor),
sliderView.leadingAnchor.constraint(equalTo: leadingAnchor),
sliderView.bottomAnchor.constraint(equalTo: bottomAnchor),
sliderTrailingAnchor
])
}
}