Merge branch /develop into feature/compose
# Conflicts: # Mastodon.xcodeproj/project.pbxproj
This commit is contained in:
commit
c5582c7aaf
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.";
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
])
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue