Merge pull request #77 from tootsuite/feature/publish-title-view
Update navigation title view to display publish progress
This commit is contained in:
commit
ff850301df
|
@ -18,6 +18,10 @@
|
||||||
"discard_post_content": {
|
"discard_post_content": {
|
||||||
"title": "Discard Publish",
|
"title": "Discard Publish",
|
||||||
"message": "Confirm discard composed post content."
|
"message": "Confirm discard composed post content."
|
||||||
|
},
|
||||||
|
"publish_post_failure": {
|
||||||
|
"title": "Publish Failure",
|
||||||
|
"message": "Failed to publish the post.\nPlease check your internet connection."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
|
@ -32,6 +36,7 @@
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"discard": "Discard",
|
"discard": "Discard",
|
||||||
|
"try_again": "Try Again",
|
||||||
"take_photo": "Take photo",
|
"take_photo": "Take photo",
|
||||||
"save_photo": "Save photo",
|
"save_photo": "Save photo",
|
||||||
"sign_in": "Sign In",
|
"sign_in": "Sign In",
|
||||||
|
|
|
@ -73,8 +73,8 @@
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
||||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */; };
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */; };
|
||||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */; };
|
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */; };
|
||||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; };
|
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; };
|
||||||
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; };
|
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; };
|
||||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
|
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
|
||||||
|
@ -246,6 +246,7 @@
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
||||||
|
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
||||||
|
@ -370,8 +371,8 @@
|
||||||
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = "<group>"; };
|
||||||
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarView.swift; sourceTree = "<group>"; };
|
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleView.swift; sourceTree = "<group>"; };
|
||||||
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.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>"; };
|
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>"; };
|
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||||
|
@ -555,6 +556,7 @@
|
||||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||||
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
||||||
|
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
|
||||||
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
||||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
||||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -707,6 +709,7 @@
|
||||||
2D38F1D325CD463600561493 /* HomeTimeline */ = {
|
2D38F1D325CD463600561493 /* HomeTimeline */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB1F239626117C360057430E /* View */,
|
||||||
2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */,
|
2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */,
|
||||||
2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */,
|
2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */,
|
||||||
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */,
|
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */,
|
||||||
|
@ -715,8 +718,6 @@
|
||||||
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */,
|
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */,
|
||||||
2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */,
|
2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */,
|
||||||
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */,
|
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */,
|
||||||
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */,
|
|
||||||
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */,
|
|
||||||
);
|
);
|
||||||
path = HomeTimeline;
|
path = HomeTimeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -785,6 +786,7 @@
|
||||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
||||||
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
|
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
|
||||||
|
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
|
||||||
);
|
);
|
||||||
path = Service;
|
path = Service;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -970,6 +972,15 @@
|
||||||
path = TableView;
|
path = TableView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB1F239626117C360057430E /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */,
|
||||||
|
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB3D0FF725BAA68500EAA174 /* Supporting Files */ = {
|
DB3D0FF725BAA68500EAA174 /* Supporting Files */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1832,12 +1843,12 @@
|
||||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
|
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
||||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||||
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||||
|
@ -1925,6 +1936,7 @@
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||||
|
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||||
|
@ -2246,7 +2258,7 @@
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||||
|
@ -2254,7 +2266,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.1.0;
|
MARKETING_VERSION = 0.3.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -2273,7 +2285,7 @@
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||||
|
@ -2281,7 +2293,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.1.0;
|
MARKETING_VERSION = 0.3.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
|
@ -44,6 +44,7 @@ internal enum Asset {
|
||||||
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
||||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||||
|
internal static let success = ColorAsset(name: "Colors/Background/success")
|
||||||
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
||||||
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
|
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
|
||||||
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
||||||
|
|
|
@ -25,6 +25,12 @@ internal enum L10n {
|
||||||
/// Discard Publish
|
/// Discard Publish
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title")
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title")
|
||||||
}
|
}
|
||||||
|
internal enum PublishPostFailure {
|
||||||
|
/// Failed to publish the post.\nPlease check your internet connection.
|
||||||
|
internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message")
|
||||||
|
/// Publish Failure
|
||||||
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title")
|
||||||
|
}
|
||||||
internal enum ServerError {
|
internal enum ServerError {
|
||||||
/// Server Error
|
/// Server Error
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
||||||
|
@ -76,6 +82,8 @@ internal enum L10n {
|
||||||
internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp")
|
internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp")
|
||||||
/// Take photo
|
/// Take photo
|
||||||
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
|
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
|
||||||
|
/// Try Again
|
||||||
|
internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain")
|
||||||
}
|
}
|
||||||
internal enum Status {
|
internal enum Status {
|
||||||
/// Tap to reveal that may be sensitive
|
/// Tap to reveal that may be sensitive
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.604",
|
||||||
|
"green" : "0.741",
|
||||||
|
"red" : "0.475"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,9 @@
|
||||||
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
|
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
|
||||||
"Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content.";
|
"Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content.";
|
||||||
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
|
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
|
||||||
|
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
|
||||||
|
Please check your internet connection.";
|
||||||
|
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure";
|
||||||
"Common.Alerts.ServerError.Title" = "Server Error";
|
"Common.Alerts.ServerError.Title" = "Server Error";
|
||||||
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
||||||
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
||||||
|
@ -23,6 +26,7 @@
|
||||||
"Common.Controls.Actions.SignIn" = "Sign In";
|
"Common.Controls.Actions.SignIn" = "Sign In";
|
||||||
"Common.Controls.Actions.SignUp" = "Sign Up";
|
"Common.Controls.Actions.SignUp" = "Sign Up";
|
||||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||||
|
"Common.Controls.Actions.TryAgain" = "Try Again";
|
||||||
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
||||||
"Common.Controls.Status.Poll.Closed" = "Closed";
|
"Common.Controls.Status.Poll.Closed" = "Closed";
|
||||||
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
||||||
|
|
|
@ -483,7 +483,7 @@ extension ComposeViewController {
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
context.statusPublishService.publish(composeViewModel: viewModel)
|
||||||
dismiss(animated: true, completion: nil)
|
dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ extension ComposeViewModel {
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
viewModel?.publishStateMachinePublisher.value = self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +49,8 @@ extension ComposeViewModel.PublishState {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.updatePublishDate()
|
||||||
|
|
||||||
let domain = mastodonAuthenticationBox.domain
|
let domain = mastodonAuthenticationBox.domain
|
||||||
let attachmentServices = viewModel.attachmentServices.value
|
let attachmentServices = viewModel.attachmentServices.value
|
||||||
let mediaIDs = attachmentServices.compactMap { attachmentService in
|
let mediaIDs = attachmentServices.compactMap { attachmentService in
|
||||||
|
@ -131,7 +134,13 @@ extension ComposeViewModel.PublishState {
|
||||||
class Fail: ComposeViewModel.PublishState {
|
class Fail: ComposeViewModel.PublishState {
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
// allow discard publishing
|
// allow discard publishing
|
||||||
return stateClass == Publishing.self || stateClass == Finish.self
|
return stateClass == Publishing.self || stateClass == Discard.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Discard: ComposeViewModel.PublishState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,12 +39,15 @@ final class ComposeViewModel {
|
||||||
PublishState.Initial(viewModel: self),
|
PublishState.Initial(viewModel: self),
|
||||||
PublishState.Publishing(viewModel: self),
|
PublishState.Publishing(viewModel: self),
|
||||||
PublishState.Fail(viewModel: self),
|
PublishState.Fail(viewModel: self),
|
||||||
|
PublishState.Discard(viewModel: self),
|
||||||
PublishState.Finish(viewModel: self),
|
PublishState.Finish(viewModel: self),
|
||||||
])
|
])
|
||||||
stateMachine.enter(PublishState.Initial.self)
|
stateMachine.enter(PublishState.Initial.self)
|
||||||
return stateMachine
|
return stateMachine
|
||||||
}()
|
}()
|
||||||
|
private(set) lazy var publishStateMachinePublisher = CurrentValueSubject<PublishState?, Never>(nil)
|
||||||
|
private(set) var publishDate = Date() // update it when enter Publishing state
|
||||||
|
|
||||||
// UI & UX
|
// UI & UX
|
||||||
let title: CurrentValueSubject<String, Never>
|
let title: CurrentValueSubject<String, Never>
|
||||||
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
||||||
|
@ -316,6 +319,10 @@ extension ComposeViewModel {
|
||||||
let attribute = ComposeStatusItem.ComposePollOptionAttribute()
|
let attribute = ComposeStatusItem.ComposePollOptionAttribute()
|
||||||
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
|
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updatePublishDate() {
|
||||||
|
publishDate = Date()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MastodonAttachmentServiceDelegate
|
// MARK: - MastodonAttachmentServiceDelegate
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
//
|
|
||||||
// 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.danger.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.normal.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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,6 +23,8 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
private(set) lazy var viewModel = HomeTimelineViewModel(context: context)
|
private(set) lazy var viewModel = HomeTimelineViewModel(context: context)
|
||||||
|
|
||||||
|
let titleView = HomeTimelineNavigationBarTitleView()
|
||||||
|
|
||||||
let settingBarButtonItem: UIBarButtonItem = {
|
let settingBarButtonItem: UIBarButtonItem = {
|
||||||
let barButtonItem = UIBarButtonItem()
|
let barButtonItem = UIBarButtonItem()
|
||||||
barButtonItem.tintColor = Asset.Colors.Label.highlight.color
|
barButtonItem.tintColor = Asset.Colors.Label.highlight.color
|
||||||
|
@ -49,6 +51,12 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency {
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let publishProgressView: UIProgressView = {
|
||||||
|
let progressView = UIProgressView(progressViewStyle: .bar)
|
||||||
|
progressView.alpha = 0
|
||||||
|
return progressView
|
||||||
|
}()
|
||||||
|
|
||||||
let refreshControl = UIRefreshControl()
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -64,8 +72,19 @@ extension HomeTimelineViewController {
|
||||||
|
|
||||||
title = L10n.Scene.HomeTimeline.title
|
title = L10n.Scene.HomeTimeline.title
|
||||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView
|
|
||||||
navigationItem.leftBarButtonItem = settingBarButtonItem
|
navigationItem.leftBarButtonItem = settingBarButtonItem
|
||||||
|
navigationItem.titleView = titleView
|
||||||
|
titleView.delegate = self
|
||||||
|
|
||||||
|
viewModel.homeTimelineNavigationBarTitleViewModel.state
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] state in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.titleView.configure(state: state)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// long press to trigger debug menu
|
// long press to trigger debug menu
|
||||||
settingBarButtonItem.menu = debugMenu
|
settingBarButtonItem.menu = debugMenu
|
||||||
|
@ -95,9 +114,16 @@ extension HomeTimelineViewController {
|
||||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
publishProgressView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(publishProgressView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||||
|
publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
viewModel.tableView = tableView
|
viewModel.tableView = tableView
|
||||||
viewModel.viewController = self
|
|
||||||
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
tableView.prefetchDataSource = self
|
tableView.prefetchDataSource = self
|
||||||
|
@ -121,9 +147,35 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.homeTimelineNavigationBarTitleViewModel.publishingProgress
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] progress in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard progress > 0 else {
|
||||||
|
let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut)
|
||||||
|
dismissAnimator.addAnimations {
|
||||||
|
self.publishProgressView.alpha = 0
|
||||||
|
}
|
||||||
|
dismissAnimator.addCompletion { _ in
|
||||||
|
self.publishProgressView.setProgress(0, animated: false)
|
||||||
|
}
|
||||||
|
dismissAnimator.startAnimation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.publishProgressView.alpha == 0 {
|
||||||
|
let progressAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut)
|
||||||
|
progressAnimator.addAnimations {
|
||||||
|
self.publishProgressView.alpha = 1
|
||||||
|
}
|
||||||
|
progressAnimator.startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.publishProgressView.setProgress(progress, animated: true)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
@ -207,7 +259,7 @@ extension HomeTimelineViewController {
|
||||||
extension HomeTimelineViewController {
|
extension HomeTimelineViewController {
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
handleScrollViewDidScroll(scrollView)
|
handleScrollViewDidScroll(scrollView)
|
||||||
self.viewModel.homeTimelineNavigationBarState.handleScrollViewDidScroll(scrollView)
|
self.viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,8 +273,9 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
extension HomeTimelineViewController: UITableViewDelegate {
|
extension HomeTimelineViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
return 200
|
||||||
// TODO:
|
// TODO:
|
||||||
// func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
||||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
// guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
||||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
||||||
//
|
//
|
||||||
|
@ -232,7 +285,7 @@ extension HomeTimelineViewController: UITableViewDelegate {
|
||||||
// // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription)
|
// // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription)
|
||||||
//
|
//
|
||||||
// return ceil(frame.height)
|
// return ceil(frame.height)
|
||||||
// }
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||||
|
@ -364,3 +417,23 @@ extension HomeTimelineViewController: StatusTableViewCellDelegate {
|
||||||
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
||||||
func parent() -> UIViewController { return self }
|
func parent() -> UIViewController { return self }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - HomeTimelineNavigationBarTitleViewDelegate
|
||||||
|
extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate {
|
||||||
|
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) {
|
||||||
|
switch titleView.state {
|
||||||
|
case .newPostButton:
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
let indexPath = IndexPath(row: 0, section: 0)
|
||||||
|
guard diffableDataSource.itemIdentifier(for: indexPath) != nil else { return }
|
||||||
|
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
|
||||||
|
case .offlineButton:
|
||||||
|
// TODO: retry
|
||||||
|
break
|
||||||
|
case .publishedButton:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewModel.homeTimelineNavigationBarState.hasContentBeforeFetching = !latestTootIDs.isEmpty
|
|
||||||
let end = CACurrentMediaTime()
|
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)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect toots id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
|
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
|
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion)
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
|
@ -102,9 +102,10 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
|
|
||||||
if newToots.isEmpty {
|
if newToots.isEmpty {
|
||||||
viewModel.isFetchingLatestTimeline.value = false
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
viewModel.homeTimelineNavigationBarState.newTopContent.value = false
|
|
||||||
} else {
|
} else {
|
||||||
viewModel.homeTimelineNavigationBarState.newTopContent.value = true
|
if !latestTootIDs.isEmpty {
|
||||||
|
viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &viewModel.disposeBag)
|
.store(in: &viewModel.disposeBag)
|
||||||
|
|
|
@ -68,7 +68,7 @@ extension HomeTimelineViewModel.LoadMiddleState {
|
||||||
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
|
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion)
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
|
|
|
@ -58,7 +58,7 @@ extension HomeTimelineViewModel.LoadOldestState {
|
||||||
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
|
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion)
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
|
|
@ -28,17 +28,11 @@ final class HomeTimelineViewModel: NSObject {
|
||||||
let fetchedResultsController: NSFetchedResultsController<HomeTimelineIndex>
|
let fetchedResultsController: NSFetchedResultsController<HomeTimelineIndex>
|
||||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||||
|
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
||||||
let homeTimelineNavigationBarState = HomeTimelineNavigationBarState()
|
|
||||||
|
|
||||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||||
weak var tableView: UITableView?
|
weak var tableView: UITableView?
|
||||||
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||||
weak var viewController: HomeTimelineViewController? {
|
|
||||||
willSet(value) {
|
|
||||||
self.homeTimelineNavigationBarState.viewController = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// output
|
// output
|
||||||
// top loader
|
// top loader
|
||||||
|
@ -90,6 +84,7 @@ final class HomeTimelineViewModel: NSObject {
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}()
|
}()
|
||||||
|
self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context)
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
fetchedResultsController.delegate = self
|
fetchedResultsController.delegate = self
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineNavigationBarTitleView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/3/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol HomeTimelineNavigationBarTitleViewDelegate: class {
|
||||||
|
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class HomeTimelineNavigationBarTitleView: UIView {
|
||||||
|
|
||||||
|
let containerView = UIStackView()
|
||||||
|
|
||||||
|
let imageView = UIImageView()
|
||||||
|
let button = RoundedEdgesButton()
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
// input
|
||||||
|
private var blockingState: HomeTimelineNavigationBarTitleViewModel.State?
|
||||||
|
weak var delegate: HomeTimelineNavigationBarTitleViewDelegate?
|
||||||
|
|
||||||
|
// output
|
||||||
|
private(set) var state: HomeTimelineNavigationBarTitleViewModel.State = .logoImage
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineNavigationBarTitleView {
|
||||||
|
private func _init() {
|
||||||
|
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(containerView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
containerView.addArrangedSubview(imageView)
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerView.addArrangedSubview(button)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
button.heightAnchor.constraint(equalToConstant: 24).priority(.defaultHigh)
|
||||||
|
])
|
||||||
|
containerView.addArrangedSubview(label)
|
||||||
|
|
||||||
|
configure(state: .logoImage)
|
||||||
|
button.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.buttonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineNavigationBarTitleView {
|
||||||
|
@objc private func buttonDidPressed(_ sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.homeTimelineNavigationBarTitleView(self, buttonDidPressed: sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineNavigationBarTitleView {
|
||||||
|
|
||||||
|
func resetContainer() {
|
||||||
|
imageView.isHidden = true
|
||||||
|
button.isHidden = true
|
||||||
|
label.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(state: HomeTimelineNavigationBarTitleViewModel.State) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: configure title view: %s", ((#file as NSString).lastPathComponent), #line, #function, state.rawValue)
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
// check state block or not
|
||||||
|
guard blockingState == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetContainer()
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case .logoImage:
|
||||||
|
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||||
|
imageView.image = Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate)
|
||||||
|
imageView.contentMode = .center
|
||||||
|
imageView.isHidden = false
|
||||||
|
case .newPostButton:
|
||||||
|
configureButton(
|
||||||
|
title: L10n.Scene.HomeTimeline.NavigationBarState.newPosts,
|
||||||
|
textColor: .white,
|
||||||
|
backgroundColor: Asset.Colors.Button.normal.color
|
||||||
|
)
|
||||||
|
button.isHidden = false
|
||||||
|
case .offlineButton:
|
||||||
|
configureButton(
|
||||||
|
title: L10n.Scene.HomeTimeline.NavigationBarState.offline,
|
||||||
|
textColor: .white,
|
||||||
|
backgroundColor: Asset.Colors.Background.danger.color
|
||||||
|
)
|
||||||
|
button.isHidden = false
|
||||||
|
case .publishingPostLabel:
|
||||||
|
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.text = L10n.Scene.HomeTimeline.NavigationBarState.publishing
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.isHidden = false
|
||||||
|
case .publishedButton:
|
||||||
|
blockingState = state
|
||||||
|
configureButton(
|
||||||
|
title: L10n.Scene.HomeTimeline.NavigationBarState.published,
|
||||||
|
textColor: .white,
|
||||||
|
backgroundColor: Asset.Colors.Background.success.color
|
||||||
|
)
|
||||||
|
button.isHidden = false
|
||||||
|
|
||||||
|
let presentDuration: TimeInterval = 0.33
|
||||||
|
let scaleAnimator = UIViewPropertyAnimator(duration: presentDuration, timingParameters: UISpringTimingParameters())
|
||||||
|
button.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||||
|
scaleAnimator.addAnimations {
|
||||||
|
self.button.transform = .identity
|
||||||
|
}
|
||||||
|
let alphaAnimator = UIViewPropertyAnimator(duration: presentDuration, curve: .easeInOut)
|
||||||
|
button.alpha = 0.3
|
||||||
|
alphaAnimator.addAnimations {
|
||||||
|
self.button.alpha = 1
|
||||||
|
}
|
||||||
|
scaleAnimator.startAnimation()
|
||||||
|
alphaAnimator.startAnimation()
|
||||||
|
|
||||||
|
let dismissDuration: TimeInterval = 3
|
||||||
|
let dissolveAnimator = UIViewPropertyAnimator(duration: dismissDuration, curve: .easeInOut)
|
||||||
|
dissolveAnimator.addAnimations({
|
||||||
|
self.button.alpha = 0
|
||||||
|
}, delayFactor: 0.9) // at 2.7s
|
||||||
|
dissolveAnimator.addCompletion { _ in
|
||||||
|
self.blockingState = nil
|
||||||
|
self.configure(state: self.state)
|
||||||
|
self.button.alpha = 1
|
||||||
|
}
|
||||||
|
dissolveAnimator.startAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureButton(title: String, textColor: UIColor, backgroundColor: UIColor) {
|
||||||
|
button.setBackgroundImage(.placeholder(color: backgroundColor), for: .normal)
|
||||||
|
button.setBackgroundImage(.placeholder(color: backgroundColor.withAlphaComponent(0.5)), for: .highlighted)
|
||||||
|
button.setTitleColor(textColor, for: .normal)
|
||||||
|
button.setTitleColor(textColor.withAlphaComponent(0.5), for: .highlighted)
|
||||||
|
button.setTitle(title, for: .normal)
|
||||||
|
button.contentEdgeInsets = UIEdgeInsets(top: 1, left: 16, bottom: 1, right: 16)
|
||||||
|
button.titleLabel?.font = .systemFont(ofSize: 15, weight: .bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct HomeTimelineNavigationBarTitleView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let titleView = HomeTimelineNavigationBarTitleView()
|
||||||
|
titleView.configure(state: .logoImage)
|
||||||
|
return titleView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 150) {
|
||||||
|
let titleView = HomeTimelineNavigationBarTitleView()
|
||||||
|
titleView.configure(state: .newPostButton)
|
||||||
|
return titleView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 150, height: 24))
|
||||||
|
UIViewPreview(width: 120) {
|
||||||
|
let titleView = HomeTimelineNavigationBarTitleView()
|
||||||
|
titleView.configure(state: .offlineButton)
|
||||||
|
return titleView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 120, height: 24))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let titleView = HomeTimelineNavigationBarTitleView()
|
||||||
|
titleView.configure(state: .publishingPostLabel)
|
||||||
|
return titleView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 120) {
|
||||||
|
let titleView = HomeTimelineNavigationBarTitleView()
|
||||||
|
titleView.configure(state: .publishedButton)
|
||||||
|
return titleView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 120, height: 24))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineNavigationBarTitleViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/3/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class HomeTimelineNavigationBarTitleViewModel {
|
||||||
|
|
||||||
|
static let offlineCounterLimit = 3
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
private(set) var publishingProgressSubscription: AnyCancellable?
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
var networkErrorCount = CurrentValueSubject<Int, Never>(0)
|
||||||
|
var networkErrorPublisher = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
// output
|
||||||
|
let state = CurrentValueSubject<State, Never>(.logoImage)
|
||||||
|
let hasNewPosts = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let isOffline = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let isPublishingPost = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let isPublished = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let publishingProgress = PassthroughSubject<Float, Never>()
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
networkErrorPublisher
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.networkErrorCount.value += self.networkErrorCount.value + 1
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
networkErrorCount
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.map { count in
|
||||||
|
return count >= HomeTimelineNavigationBarTitleViewModel.offlineCounterLimit
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: isOffline)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
context.statusPublishService.latestPublishingComposeViewModel
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] composeViewModel in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let composeViewModel = composeViewModel,
|
||||||
|
let state = composeViewModel.publishStateMachine.currentState else {
|
||||||
|
self.isPublishingPost.value = false
|
||||||
|
self.isPublished.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isPublishingPost.value = state is ComposeViewModel.PublishState.Publishing || state is ComposeViewModel.PublishState.Fail
|
||||||
|
self.isPublished.value = state is ComposeViewModel.PublishState.Finish
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
Publishers.CombineLatest4(
|
||||||
|
hasNewPosts.eraseToAnyPublisher(),
|
||||||
|
isOffline.eraseToAnyPublisher(),
|
||||||
|
isPublishingPost.eraseToAnyPublisher(),
|
||||||
|
isPublished.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.map { hasNewPosts, isOffline, isPublishingPost, isPublished -> State in
|
||||||
|
guard !isPublished else { return .publishedButton }
|
||||||
|
guard !isPublishingPost else { return .publishingPostLabel }
|
||||||
|
guard !isOffline else { return .offlineButton }
|
||||||
|
guard !hasNewPosts else { return .newPostButton }
|
||||||
|
return .logoImage
|
||||||
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.value, on: state)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
state
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] state in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch state {
|
||||||
|
case .publishingPostLabel:
|
||||||
|
self.setupPublishingProgress()
|
||||||
|
default:
|
||||||
|
self.suspendPublishingProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineNavigationBarTitleViewModel {
|
||||||
|
// state order by priority from low to high
|
||||||
|
enum State: String {
|
||||||
|
case logoImage
|
||||||
|
case newPostButton
|
||||||
|
case offlineButton
|
||||||
|
case publishingPostLabel
|
||||||
|
case publishedButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - New post state
|
||||||
|
extension HomeTimelineNavigationBarTitleViewModel {
|
||||||
|
|
||||||
|
func newPostsIncoming() {
|
||||||
|
hasNewPosts.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetNewPostState() {
|
||||||
|
hasNewPosts.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Offline state
|
||||||
|
extension HomeTimelineNavigationBarTitleViewModel {
|
||||||
|
|
||||||
|
func resetOfflineCounterListener() {
|
||||||
|
networkErrorCount.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveLoadingStateCompletion(_ completion: Subscribers.Completion<Error>) {
|
||||||
|
switch completion {
|
||||||
|
case .failure:
|
||||||
|
networkErrorPublisher.send()
|
||||||
|
case .finished:
|
||||||
|
resetOfflineCounterListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleScrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard hasNewPosts.value else { return }
|
||||||
|
|
||||||
|
let contentOffsetY = scrollView.contentOffset.y
|
||||||
|
let isScrollToTop = contentOffsetY < -scrollView.contentInset.top
|
||||||
|
guard isScrollToTop else { return }
|
||||||
|
resetNewPostState()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Publish post state
|
||||||
|
extension HomeTimelineNavigationBarTitleViewModel {
|
||||||
|
|
||||||
|
func setupPublishingProgress() {
|
||||||
|
let progressUpdatePublisher = Timer.publish(every: 0.016, on: .main, in: .common) // ~ 60FPS
|
||||||
|
.autoconnect()
|
||||||
|
.share()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
publishingProgressSubscription = progressUpdatePublisher
|
||||||
|
.map { _ in Float(0) }
|
||||||
|
.scan(0.0) { progress, _ -> Float in
|
||||||
|
return 0.95 * progress + 0.05 // progress + 0.05 * (1.0 - progress). ~ 1 sec to 0.95 (under 60FPS)
|
||||||
|
}
|
||||||
|
.subscribe(publishingProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func suspendPublishingProgress() {
|
||||||
|
publishingProgressSubscription = nil
|
||||||
|
publishingProgress.send(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -123,6 +123,32 @@ extension MainTabBarController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// handle post failure
|
||||||
|
context.statusPublishService
|
||||||
|
.latestPublishingComposeViewModel
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] composeViewModel in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let composeViewModel = composeViewModel else { return }
|
||||||
|
guard let currentState = composeViewModel.publishStateMachine.currentState else { return }
|
||||||
|
guard currentState is ComposeViewModel.PublishState.Fail else { return }
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: L10n.Common.Alerts.PublishPostFailure.title, message: L10n.Common.Alerts.PublishPostFailure.message, preferredStyle: .alert)
|
||||||
|
let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self, weak composeViewModel] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let composeViewModel = composeViewModel else { return }
|
||||||
|
self.context.statusPublishService.remove(composeViewModel: composeViewModel)
|
||||||
|
}
|
||||||
|
alertController.addAction(discardAction)
|
||||||
|
let retryAction = UIAlertAction(title: L10n.Common.Controls.Actions.tryAgain, style: .default) { [weak composeViewModel] _ in
|
||||||
|
guard let composeViewModel = composeViewModel else { return }
|
||||||
|
composeViewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self)
|
||||||
|
}
|
||||||
|
alertController.addAction(retryAction)
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// selectedIndex = 1
|
// selectedIndex = 1
|
||||||
|
|
|
@ -16,8 +16,8 @@ final class StatusPrefetchingService {
|
||||||
|
|
||||||
typealias TaskID = String
|
typealias TaskID = String
|
||||||
|
|
||||||
let workingQueue = DispatchQueue(label: "status-prefetching-service-working-queue")
|
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.StatusPrefetchingService.working-queue")
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]
|
private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
//
|
||||||
|
// StatusPublishService.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class StatusPublishService {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.StatusPublishService.working-queue")
|
||||||
|
|
||||||
|
// input
|
||||||
|
var viewModels = CurrentValueSubject<[ComposeViewModel], Never>([]) // use strong reference to retain the view models
|
||||||
|
|
||||||
|
// output
|
||||||
|
let composeViewModelDidUpdatePublisher = PassthroughSubject<Void, Never>()
|
||||||
|
let latestPublishingComposeViewModel = CurrentValueSubject<ComposeViewModel?, Never>(nil)
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
viewModels.eraseToAnyPublisher(),
|
||||||
|
composeViewModelDidUpdatePublisher.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.map { viewModels, _ in viewModels.last }
|
||||||
|
.assign(to: \.value, on: latestPublishingComposeViewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusPublishService {
|
||||||
|
|
||||||
|
func publish(composeViewModel: ComposeViewModel) {
|
||||||
|
workingQueue.sync {
|
||||||
|
guard !self.viewModels.value.contains(where: { $0 === composeViewModel }) else { return }
|
||||||
|
self.viewModels.value = self.viewModels.value + [composeViewModel]
|
||||||
|
|
||||||
|
composeViewModel.publishStateMachinePublisher
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self, weak composeViewModel] state in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let composeViewModel = composeViewModel else { return }
|
||||||
|
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModelDidUpdate", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
self.composeViewModelDidUpdatePublisher.send()
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case is ComposeViewModel.PublishState.Finish:
|
||||||
|
self.remove(composeViewModel: composeViewModel)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &composeViewModel.disposeBag) // cancel subscription when viewModel dealloc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(composeViewModel: ComposeViewModel) {
|
||||||
|
workingQueue.async {
|
||||||
|
var viewModels = self.viewModels.value
|
||||||
|
viewModels.removeAll(where: { $0 === composeViewModel })
|
||||||
|
self.viewModels.value = viewModels
|
||||||
|
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModel removed", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import os.log
|
||||||
final class VideoPlaybackService {
|
final class VideoPlaybackService {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
let workingQueue = DispatchQueue(label: "com.twidere.twiderex.video-playback-service.working-queue")
|
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.VideoPlaybackService.working-queue")
|
||||||
private(set) var viewPlayerViewModelDict: [URL: VideoPlayerViewModel] = [:]
|
private(set) var viewPlayerViewModelDict: [URL: VideoPlayerViewModel] = [:]
|
||||||
|
|
||||||
// only for video kind
|
// only for video kind
|
||||||
|
|
|
@ -27,6 +27,7 @@ class AppContext: ObservableObject {
|
||||||
let audioPlaybackService = AudioPlaybackService()
|
let audioPlaybackService = AudioPlaybackService()
|
||||||
let videoPlaybackService = VideoPlaybackService()
|
let videoPlaybackService = VideoPlaybackService()
|
||||||
let statusPrefetchingService: StatusPrefetchingService
|
let statusPrefetchingService: StatusPrefetchingService
|
||||||
|
let statusPublishService = StatusPublishService()
|
||||||
|
|
||||||
let documentStore: DocumentStore
|
let documentStore: DocumentStore
|
||||||
private var documentStoreSubscription: AnyCancellable!
|
private var documentStoreSubscription: AnyCancellable!
|
||||||
|
|
Loading…
Reference in New Issue