Re-write stuff in UIKit
This commit is contained in:
parent
02747215c5
commit
2389e1b25c
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
extension Timeline {
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .home:
|
||||||
|
return NSLocalizedString("timelines.home", comment: "")
|
||||||
|
case .local:
|
||||||
|
return NSLocalizedString("timelines.local", comment: "")
|
||||||
|
case .federated:
|
||||||
|
return NSLocalizedString("timelines.federated", comment: "")
|
||||||
|
case let .list(list):
|
||||||
|
return list.title
|
||||||
|
case let .tag(tag):
|
||||||
|
return "#".appending(tag)
|
||||||
|
case .profile:
|
||||||
|
return ""
|
||||||
|
case .favorites:
|
||||||
|
return NSLocalizedString("favorites", comment: "")
|
||||||
|
case .bookmarks:
|
||||||
|
return NSLocalizedString("bookmarks", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .home: return "house"
|
||||||
|
case .local: return "building.2.crop.circle"
|
||||||
|
case .federated: return "network"
|
||||||
|
case .list: return "scroll"
|
||||||
|
case .tag: return "number"
|
||||||
|
case .profile: return "person"
|
||||||
|
case .favorites: return "star"
|
||||||
|
case .bookmarks: return "bookmark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -205,8 +205,5 @@
|
||||||
"status.visibility.direct.description" = "Visible for mentioned users only";
|
"status.visibility.direct.description" = "Visible for mentioned users only";
|
||||||
"submit" = "Submit";
|
"submit" = "Submit";
|
||||||
"timelines.home" = "Home";
|
"timelines.home" = "Home";
|
||||||
"timelines.home.description" = "Posts from accounts you're following";
|
|
||||||
"timelines.local" = "Local";
|
"timelines.local" = "Local";
|
||||||
"timelines.local.description-%@" = "Public posts on %@";
|
|
||||||
"timelines.federated" = "Federated";
|
"timelines.federated" = "Federated";
|
||||||
"timelines.federated.description-%@" = "Public posts on instances known by %@";
|
|
||||||
|
|
|
@ -27,7 +27,9 @@
|
||||||
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */; };
|
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */; };
|
||||||
D035F87D25B7F61600DC75ED /* TimelinesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */; };
|
D035F87D25B7F61600DC75ED /* TimelinesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */; };
|
||||||
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */; };
|
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */; };
|
||||||
D035F89125B8067100DC75ED /* TimelinesTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F89025B8067100DC75ED /* TimelinesTitleView.swift */; };
|
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */; };
|
||||||
|
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */; };
|
||||||
|
D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F8C625B96A4000DC75ED /* SecondaryNavigationButton.swift */; };
|
||||||
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; };
|
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; };
|
||||||
D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; };
|
D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; };
|
||||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; };
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; };
|
||||||
|
@ -117,7 +119,6 @@
|
||||||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */; };
|
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */; };
|
||||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */; };
|
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */; };
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */; };
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */; };
|
||||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */; };
|
|
||||||
D0C7D4C224F7616A001EBDBB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45224F76169001EBDBB /* Assets.xcassets */; };
|
D0C7D4C224F7616A001EBDBB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45224F76169001EBDBB /* Assets.xcassets */; };
|
||||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45424F76169001EBDBB /* MetatextApp.swift */; };
|
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45424F76169001EBDBB /* MetatextApp.swift */; };
|
||||||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45524F76169001EBDBB /* AppDelegate.swift */; };
|
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45524F76169001EBDBB /* AppDelegate.swift */; };
|
||||||
|
@ -213,7 +214,9 @@
|
||||||
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationView.swift; sourceTree = "<group>"; };
|
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationView.swift; sourceTree = "<group>"; };
|
||||||
D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesViewController.swift; sourceTree = "<group>"; };
|
D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesViewController.swift; sourceTree = "<group>"; };
|
||||||
D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationViewModel+Extensions.swift"; sourceTree = "<group>"; };
|
D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationViewModel+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D035F89025B8067100DC75ED /* TimelinesTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesTitleView.swift; sourceTree = "<group>"; };
|
D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusButtonView.swift; sourceTree = "<group>"; };
|
||||||
|
D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timeline+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D035F8C625B96A4000DC75ED /* SecondaryNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationButton.swift; sourceTree = "<group>"; };
|
||||||
D036AA01254B6101009094DF /* NotificationListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListCell.swift; sourceTree = "<group>"; };
|
D036AA01254B6101009094DF /* NotificationListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListCell.swift; sourceTree = "<group>"; };
|
||||||
D036AA06254B6118009094DF /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
|
D036AA06254B6118009094DF /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
|
||||||
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentConfiguration.swift; sourceTree = "<group>"; };
|
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
@ -290,7 +293,6 @@
|
||||||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesView.swift; sourceTree = "<group>"; };
|
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationView.swift; sourceTree = "<group>"; };
|
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationView.swift; sourceTree = "<group>"; };
|
||||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesView.swift; sourceTree = "<group>"; };
|
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigationView.swift; sourceTree = "<group>"; };
|
|
||||||
D0C7D45224F76169001EBDBB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
D0C7D45224F76169001EBDBB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
D0C7D45424F76169001EBDBB /* MetatextApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
|
D0C7D45424F76169001EBDBB /* MetatextApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
|
||||||
D0C7D45524F76169001EBDBB /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
D0C7D45524F76169001EBDBB /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
@ -517,6 +519,7 @@
|
||||||
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */,
|
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */,
|
||||||
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
||||||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
||||||
|
D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */,
|
||||||
D0FCC10F259C4F20000B67DF /* NewStatusView.swift */,
|
D0FCC10F259C4F20000B67DF /* NewStatusView.swift */,
|
||||||
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */,
|
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */,
|
||||||
D036AA01254B6101009094DF /* NotificationListCell.swift */,
|
D036AA01254B6101009094DF /* NotificationListCell.swift */,
|
||||||
|
@ -532,12 +535,11 @@
|
||||||
D0DD50CA256B1F24004A04F7 /* ReportView.swift */,
|
D0DD50CA256B1F24004A04F7 /* ReportView.swift */,
|
||||||
D0C7D42724F76169001EBDBB /* RootView.swift */,
|
D0C7D42724F76169001EBDBB /* RootView.swift */,
|
||||||
D02E1F94250B13210071AD56 /* SafariView.swift */,
|
D02E1F94250B13210071AD56 /* SafariView.swift */,
|
||||||
|
D035F8C625B96A4000DC75ED /* SecondaryNavigationButton.swift */,
|
||||||
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */,
|
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */,
|
||||||
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */,
|
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */,
|
||||||
D0625E55250F086B00502611 /* Status */,
|
D0625E55250F086B00502611 /* Status */,
|
||||||
D0C7D42524F76169001EBDBB /* TableView.swift */,
|
D0C7D42524F76169001EBDBB /* TableView.swift */,
|
||||||
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */,
|
|
||||||
D035F89025B8067100DC75ED /* TimelinesTitleView.swift */,
|
|
||||||
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */,
|
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */,
|
||||||
D0EA59472522B8B600804347 /* ViewConstants.swift */,
|
D0EA59472522B8B600804347 /* ViewConstants.swift */,
|
||||||
D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */,
|
D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */,
|
||||||
|
@ -601,6 +603,7 @@
|
||||||
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
||||||
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
||||||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */,
|
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */,
|
||||||
|
D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -825,7 +828,6 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
||||||
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
||||||
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||||
|
@ -849,6 +851,7 @@
|
||||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
||||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
|
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
|
||||||
|
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */,
|
||||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||||
|
@ -884,18 +887,19 @@
|
||||||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
||||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||||
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
||||||
|
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */,
|
||||||
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
||||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
||||||
D035F89125B8067100DC75ED /* TimelinesTitleView.swift in Sources */,
|
|
||||||
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
||||||
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */,
|
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */,
|
||||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
||||||
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||||
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
||||||
|
D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */,
|
||||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
// Copyright © 2021 Metabolist. All rights reserved.
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import UIKit
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
final class MainNavigationViewController: UITabBarController {
|
final class MainNavigationViewController: UITabBarController {
|
||||||
private let viewModel: NavigationViewModel
|
private let viewModel: NavigationViewModel
|
||||||
private let rootViewModel: RootViewModel
|
private let rootViewModel: RootViewModel
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private weak var presentedSecondaryNavigation: UINavigationController?
|
||||||
|
|
||||||
init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) {
|
init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
@ -22,13 +25,40 @@ final class MainNavigationViewController: UITabBarController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
let timelinesViewController = TimelinesViewController(
|
setupViewControllers()
|
||||||
viewModel: viewModel,
|
|
||||||
rootViewModel: rootViewModel)
|
|
||||||
let timelinesNavigationController = UINavigationController(rootViewController: timelinesViewController)
|
|
||||||
|
|
||||||
if let notificationsViewModel = viewModel.notificationsViewModel,
|
if viewModel.identification.identity.authenticated {
|
||||||
let conversationsViewModel = viewModel.conversationsViewModel {
|
setupNewStatusButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.$presentingSecondaryNavigation.sink { [weak self] in
|
||||||
|
if $0 {
|
||||||
|
self?.presentSecondaryNavigation()
|
||||||
|
} else {
|
||||||
|
self?.dismissSecondaryNavigation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.timelineNavigations
|
||||||
|
.sink { [weak self] _ in self?.selectedIndex = 0 }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
viewModel.refreshIdentity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MainNavigationViewController {
|
||||||
|
func setupViewControllers() {
|
||||||
|
var controllers: [UIViewController] = [TimelinesViewController(
|
||||||
|
viewModel: viewModel,
|
||||||
|
rootViewModel: rootViewModel)]
|
||||||
|
|
||||||
|
if let notificationsViewModel = viewModel.notificationsViewModel {
|
||||||
let notificationsViewController = TableViewController(
|
let notificationsViewController = TableViewController(
|
||||||
viewModel: notificationsViewModel,
|
viewModel: notificationsViewModel,
|
||||||
rootViewModel: rootViewModel,
|
rootViewModel: rootViewModel,
|
||||||
|
@ -36,9 +66,10 @@ final class MainNavigationViewController: UITabBarController {
|
||||||
|
|
||||||
notificationsViewController.tabBarItem = NavigationViewModel.Tab.notifications.tabBarItem
|
notificationsViewController.tabBarItem = NavigationViewModel.Tab.notifications.tabBarItem
|
||||||
|
|
||||||
let notificationsNavigationViewController = UINavigationController(
|
controllers.append(notificationsViewController)
|
||||||
rootViewController: notificationsViewController)
|
}
|
||||||
|
|
||||||
|
if let conversationsViewModel = viewModel.conversationsViewModel {
|
||||||
let conversationsViewController = TableViewController(
|
let conversationsViewController = TableViewController(
|
||||||
viewModel: conversationsViewModel,
|
viewModel: conversationsViewModel,
|
||||||
rootViewModel: rootViewModel,
|
rootViewModel: rootViewModel,
|
||||||
|
@ -47,18 +78,64 @@ final class MainNavigationViewController: UITabBarController {
|
||||||
conversationsViewController.tabBarItem = NavigationViewModel.Tab.messages.tabBarItem
|
conversationsViewController.tabBarItem = NavigationViewModel.Tab.messages.tabBarItem
|
||||||
conversationsViewController.navigationItem.title = NavigationViewModel.Tab.messages.title
|
conversationsViewController.navigationItem.title = NavigationViewModel.Tab.messages.title
|
||||||
|
|
||||||
let conversationsNavigationViewController = UINavigationController(
|
controllers.append(conversationsViewController)
|
||||||
rootViewController: conversationsViewController)
|
}
|
||||||
|
|
||||||
viewControllers = [
|
let secondaryNavigationButton = SecondaryNavigationButton(viewModel: viewModel, rootViewModel: rootViewModel)
|
||||||
timelinesNavigationController,
|
|
||||||
notificationsNavigationViewController,
|
for controller in controllers {
|
||||||
conversationsNavigationViewController
|
controller.navigationItem.leftBarButtonItem = secondaryNavigationButton
|
||||||
]
|
}
|
||||||
} else {
|
|
||||||
viewControllers = [
|
viewControllers = controllers.map(UINavigationController.init(rootViewController:))
|
||||||
timelinesNavigationController
|
}
|
||||||
]
|
|
||||||
|
func setupNewStatusButton() {
|
||||||
|
let newStatusButtonView = NewStatusButtonView(primaryAction: UIAction { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let newStatusViewModel = self.rootViewModel.newStatusViewModel(
|
||||||
|
identification: self.viewModel.identification)
|
||||||
|
let newStatusViewController = NewStatusViewController(viewModel: newStatusViewModel)
|
||||||
|
let newStatusNavigationController = UINavigationController(rootViewController: newStatusViewController)
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
newStatusNavigationController.modalPresentationStyle = .overFullScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
self.present(newStatusNavigationController, animated: true)
|
||||||
|
})
|
||||||
|
|
||||||
|
view.addSubview(newStatusButtonView)
|
||||||
|
newStatusButtonView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
newStatusButtonView.widthAnchor.constraint(equalToConstant: .newStatusButtonDimension),
|
||||||
|
newStatusButtonView.heightAnchor.constraint(equalToConstant: .newStatusButtonDimension),
|
||||||
|
newStatusButtonView.trailingAnchor.constraint(
|
||||||
|
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
|
||||||
|
constant: -.defaultSpacing * 2),
|
||||||
|
newStatusButtonView.bottomAnchor.constraint(equalTo: tabBar.topAnchor, constant: -.defaultSpacing * 2)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentSecondaryNavigation() {
|
||||||
|
let secondaryNavigationView = SecondaryNavigationView(viewModel: viewModel)
|
||||||
|
.environmentObject(rootViewModel)
|
||||||
|
let hostingController = UIHostingController(rootView: secondaryNavigationView)
|
||||||
|
|
||||||
|
hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||||
|
systemItem: .close,
|
||||||
|
primaryAction: UIAction { [weak self] _ in self?.viewModel.presentingSecondaryNavigation = false })
|
||||||
|
|
||||||
|
let navigationController = UINavigationController(rootViewController: hostingController)
|
||||||
|
|
||||||
|
presentedSecondaryNavigation = navigationController
|
||||||
|
present(navigationController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissSecondaryNavigation() {
|
||||||
|
if presentedViewController == presentedSecondaryNavigation {
|
||||||
|
dismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,8 @@ final class NewStatusViewController: UIViewController {
|
||||||
activityIndicatorView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
|
activityIndicatorView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
setupBarButtonItems(identification: viewModel.identification)
|
||||||
|
|
||||||
postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
|
postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
|
||||||
self?.viewModel.post()
|
self?.viewModel.post()
|
||||||
}
|
}
|
||||||
|
@ -84,12 +86,6 @@ final class NewStatusViewController: UIViewController {
|
||||||
|
|
||||||
setupViewModelBindings()
|
setupViewModelBindings()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didMove(toParent parent: UIViewController?) {
|
|
||||||
super.didMove(toParent: parent)
|
|
||||||
|
|
||||||
setupBarButtonItems(identification: viewModel.identification)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NewStatusViewController: PHPickerViewControllerDelegate {
|
extension NewStatusViewController: PHPickerViewControllerDelegate {
|
||||||
|
@ -251,15 +247,15 @@ private extension NewStatusViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupBarButtonItems(identification: Identification) {
|
func setupBarButtonItems(identification: Identification) {
|
||||||
let closeButton = UIBarButtonItem(
|
let cancelButton = UIBarButtonItem(
|
||||||
systemItem: .close,
|
systemItem: .cancel,
|
||||||
primaryAction: UIAction { [weak self] _ in self?.dismiss() })
|
primaryAction: UIAction { [weak self] _ in self?.dismiss() })
|
||||||
|
|
||||||
parent?.navigationItem.leftBarButtonItem = closeButton
|
navigationItem.leftBarButtonItem = cancelButton
|
||||||
parent?.navigationItem.titleView = viewModel.canChangeIdentity
|
navigationItem.titleView = viewModel.canChangeIdentity
|
||||||
? changeIdentityButton(identification: identification)
|
? changeIdentityButton(identification: identification)
|
||||||
: nil
|
: nil
|
||||||
parent?.navigationItem.rightBarButtonItem = postButton
|
navigationItem.rightBarButtonItem = postButton
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentMediaPicker(compositionViewModel: CompositionViewModel) {
|
func presentMediaPicker(compositionViewModel: CompositionViewModel) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
final class TimelinesViewController: UIPageViewController {
|
final class TimelinesViewController: UIPageViewController {
|
||||||
private let titleView: TimelinesTitleView
|
private let segmentedControl = UISegmentedControl()
|
||||||
private let timelineViewControllers: [TableViewController]
|
private let timelineViewControllers: [TableViewController]
|
||||||
private let viewModel: NavigationViewModel
|
private let viewModel: NavigationViewModel
|
||||||
private let rootViewModel: RootViewModel
|
private let rootViewModel: RootViewModel
|
||||||
|
@ -15,31 +15,18 @@ final class TimelinesViewController: UIPageViewController {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.rootViewModel = rootViewModel
|
self.rootViewModel = rootViewModel
|
||||||
|
|
||||||
let timelineViewModels: [CollectionViewModel]
|
var timelineViewControllers = [TableViewController]()
|
||||||
|
|
||||||
if let homeTimelineViewModel = viewModel.homeTimelineViewModel {
|
for (index, timeline) in viewModel.timelines.enumerated() {
|
||||||
timelineViewModels = [
|
timelineViewControllers.append(
|
||||||
homeTimelineViewModel,
|
TableViewController(
|
||||||
viewModel.localTimelineViewModel,
|
viewModel: viewModel.viewModel(timeline: timeline),
|
||||||
viewModel.federatedTimelineViewModel]
|
rootViewModel: rootViewModel,
|
||||||
} else {
|
identification: viewModel.identification))
|
||||||
timelineViewModels = [
|
segmentedControl.insertSegment(withTitle: timeline.title, at: index, animated: false)
|
||||||
viewModel.localTimelineViewModel,
|
|
||||||
viewModel.federatedTimelineViewModel]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
titleView = TimelinesTitleView(
|
self.timelineViewControllers = timelineViewControllers
|
||||||
timelines: viewModel.identification.identity.authenticated
|
|
||||||
? Timeline.authenticatedDefaults
|
|
||||||
: Timeline.unauthenticatedDefaults,
|
|
||||||
identification: viewModel.identification)
|
|
||||||
|
|
||||||
timelineViewControllers = timelineViewModels.map {
|
|
||||||
TableViewController(
|
|
||||||
viewModel: $0,
|
|
||||||
rootViewModel: rootViewModel,
|
|
||||||
identification: viewModel.identification)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.init(transitionStyle: .scroll,
|
super.init(transitionStyle: .scroll,
|
||||||
navigationOrientation: .horizontal,
|
navigationOrientation: .horizontal,
|
||||||
|
@ -66,24 +53,35 @@ final class TimelinesViewController: UIPageViewController {
|
||||||
image: UIImage(systemName: "newspaper"),
|
image: UIImage(systemName: "newspaper"),
|
||||||
selectedImage: nil)
|
selectedImage: nil)
|
||||||
|
|
||||||
navigationItem.titleView = titleView
|
|
||||||
|
|
||||||
navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close)
|
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "megaphone"), primaryAction: nil)
|
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "megaphone"), primaryAction: nil)
|
||||||
|
navigationItem.titleView = segmentedControl
|
||||||
titleView.$selectedTimeline
|
segmentedControl.selectedSegmentIndex = 0
|
||||||
.compactMap { [weak self] in self?.titleView.timelines.firstIndex(of: $0) }
|
segmentedControl.addAction(
|
||||||
.sink { [weak self] index in
|
UIAction { [weak self] _ in
|
||||||
guard let self = self,
|
guard let self = self,
|
||||||
let currentViewController = self.viewControllers?.first as? TableViewController,
|
let currentViewController = self.viewControllers?.first as? TableViewController,
|
||||||
let currentIndex = self.timelineViewControllers.firstIndex(of: currentViewController),
|
let currentIndex = self.timelineViewControllers.firstIndex(of: currentViewController),
|
||||||
index != currentIndex
|
self.segmentedControl.selectedSegmentIndex != currentIndex
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
self.setViewControllers(
|
self.setViewControllers(
|
||||||
[self.timelineViewControllers[index]],
|
[self.timelineViewControllers[self.segmentedControl.selectedSegmentIndex]],
|
||||||
direction: index > currentIndex ? .forward : .reverse,
|
direction: self.segmentedControl.selectedSegmentIndex > currentIndex ? .forward : .reverse,
|
||||||
animated: !UIAccessibility.isReduceMotionEnabled)
|
animated: !UIAccessibility.isReduceMotionEnabled)
|
||||||
|
},
|
||||||
|
for: .valueChanged)
|
||||||
|
|
||||||
|
viewModel.timelineNavigations.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let vc = TableViewController(
|
||||||
|
viewModel: self.viewModel.viewModel(timeline: $0),
|
||||||
|
rootViewModel: self.rootViewModel,
|
||||||
|
identification: self.viewModel.identification)
|
||||||
|
|
||||||
|
vc.navigationItem.title = $0.title
|
||||||
|
|
||||||
|
self.show(vc, sender: self)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
@ -122,10 +120,6 @@ extension TimelinesViewController: UIPageViewControllerDelegate {
|
||||||
let index = timelineViewControllers.firstIndex(of: viewController)
|
let index = timelineViewControllers.firstIndex(of: viewController)
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
let timeline = titleView.timelines[index]
|
segmentedControl.selectedSegmentIndex = index
|
||||||
|
|
||||||
if titleView.selectedTimeline != timeline {
|
|
||||||
titleView.selectedTimeline = timeline
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,41 +7,11 @@ import ServiceLayer
|
||||||
|
|
||||||
public final class NavigationViewModel: ObservableObject {
|
public final class NavigationViewModel: ObservableObject {
|
||||||
public let identification: Identification
|
public let identification: Identification
|
||||||
|
public let timelineNavigations: AnyPublisher<Timeline, Never>
|
||||||
|
|
||||||
@Published public private(set) var recentIdentities = [Identity]()
|
@Published public private(set) var recentIdentities = [Identity]()
|
||||||
@Published public var timeline: Timeline {
|
|
||||||
didSet {
|
|
||||||
timelineViewModel = CollectionItemsViewModel(
|
|
||||||
collectionService: identification.service.service(timeline: timeline),
|
|
||||||
identification: identification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Published public private(set) var timelinesAndLists: [Timeline]
|
|
||||||
@Published public var presentingSecondaryNavigation = false
|
@Published public var presentingSecondaryNavigation = false
|
||||||
@Published public var presentingNewStatus = false
|
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public private(set) var timelineViewModel: CollectionItemsViewModel
|
|
||||||
|
|
||||||
public lazy var homeTimelineViewModel: CollectionViewModel? = {
|
|
||||||
if identification.identity.authenticated {
|
|
||||||
return CollectionItemsViewModel(
|
|
||||||
collectionService: identification.service.service(timeline: .home),
|
|
||||||
identification: identification)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
public lazy var localTimelineViewModel: CollectionViewModel = {
|
|
||||||
CollectionItemsViewModel(
|
|
||||||
collectionService: identification.service.service(timeline: .local),
|
|
||||||
identification: identification)
|
|
||||||
}()
|
|
||||||
|
|
||||||
public lazy var federatedTimelineViewModel: CollectionViewModel = {
|
|
||||||
CollectionItemsViewModel(
|
|
||||||
collectionService: identification.service.service(timeline: .federated),
|
|
||||||
identification: identification)
|
|
||||||
}()
|
|
||||||
|
|
||||||
public lazy var notificationsViewModel: CollectionViewModel? = {
|
public lazy var notificationsViewModel: CollectionViewModel? = {
|
||||||
if identification.identity.authenticated {
|
if identification.identity.authenticated {
|
||||||
|
@ -71,18 +41,12 @@ public final class NavigationViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private let timelineNavigationsSubject = PassthroughSubject<Timeline, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(identification: Identification) {
|
public init(identification: Identification) {
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
let timeline: Timeline = identification.identity.authenticated ? .home : .local
|
timelineNavigations = timelineNavigationsSubject.eraseToAnyPublisher()
|
||||||
self.timeline = timeline
|
|
||||||
timelineViewModel = CollectionItemsViewModel(
|
|
||||||
collectionService: identification.service.service(timeline: timeline),
|
|
||||||
identification: identification)
|
|
||||||
timelinesAndLists = identification.identity.authenticated
|
|
||||||
? Timeline.authenticatedDefaults
|
|
||||||
: Timeline.unauthenticatedDefaults
|
|
||||||
|
|
||||||
identification.$identity
|
identification.$identity
|
||||||
.sink { [weak self] _ in self?.objectWillChange.send() }
|
.sink { [weak self] _ in self?.objectWillChange.send() }
|
||||||
|
@ -91,17 +55,17 @@ public final class NavigationViewModel: ObservableObject {
|
||||||
identification.service.recentIdentitiesPublisher()
|
identification.service.recentIdentitiesPublisher()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$recentIdentities)
|
.assign(to: &$recentIdentities)
|
||||||
|
|
||||||
if identification.identity.authenticated {
|
|
||||||
identification.service.listsPublisher()
|
|
||||||
.map { Timeline.authenticatedDefaults + $0 }
|
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
|
||||||
.assign(to: &$timelinesAndLists)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension NavigationViewModel {
|
public extension NavigationViewModel {
|
||||||
|
enum Tab: CaseIterable {
|
||||||
|
case timelines
|
||||||
|
case explore
|
||||||
|
case notifications
|
||||||
|
case messages
|
||||||
|
}
|
||||||
|
|
||||||
var tabs: [Tab] {
|
var tabs: [Tab] {
|
||||||
if identification.identity.authenticated {
|
if identification.identity.authenticated {
|
||||||
return Tab.allCases
|
return Tab.allCases
|
||||||
|
@ -110,12 +74,11 @@ public extension NavigationViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var timelineSubtitle: String {
|
var timelines: [Timeline] {
|
||||||
switch timeline {
|
if identification.identity.authenticated {
|
||||||
case .home, .favorites, .bookmarks, .list:
|
return Timeline.authenticatedDefaults
|
||||||
return identification.identity.handle
|
} else {
|
||||||
case .local, .federated, .tag, .profile:
|
return Timeline.unauthenticatedDefaults
|
||||||
return identification.identity.instance?.uri ?? ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,29 +119,15 @@ public extension NavigationViewModel {
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public extension NavigationViewModel {
|
func navigate(timeline: Timeline) {
|
||||||
enum Tab: CaseIterable {
|
presentingSecondaryNavigation = false
|
||||||
case timelines
|
timelineNavigationsSubject.send(timeline)
|
||||||
case explore
|
|
||||||
case notifications
|
|
||||||
case messages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func favoritesViewModel() -> CollectionViewModel {
|
func viewModel(timeline: Timeline) -> CollectionItemsViewModel {
|
||||||
CollectionItemsViewModel(
|
CollectionItemsViewModel(
|
||||||
collectionService: identification.service.service(timeline: .favorites),
|
collectionService: identification.service.service(timeline: timeline),
|
||||||
identification: identification)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bookmarksViewModel() -> CollectionViewModel {
|
|
||||||
CollectionItemsViewModel(
|
|
||||||
collectionService: identification.service.service(timeline: .bookmarks),
|
|
||||||
identification: identification)
|
identification: identification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NavigationViewModel.Tab: Identifiable {
|
|
||||||
public var id: Self { self }
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Mastodon
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -27,9 +28,11 @@ struct ListsView: View {
|
||||||
}
|
}
|
||||||
Section {
|
Section {
|
||||||
ForEach(viewModel.lists) { list in
|
ForEach(viewModel.lists) { list in
|
||||||
Button(list.title) {
|
Button {
|
||||||
rootViewModel.navigationViewModel?.timeline = .list(list)
|
rootViewModel.navigationViewModel?.navigate(timeline: .list(list))
|
||||||
rootViewModel.navigationViewModel?.presentingSecondaryNavigation = false
|
} label: {
|
||||||
|
Text(list.title)
|
||||||
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDelete {
|
.onDelete {
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class NewStatusButtonView: UIView {
|
||||||
|
let button: UIButton
|
||||||
|
|
||||||
|
init(primaryAction: UIAction) {
|
||||||
|
button = UIButton(type: .custom, primaryAction: primaryAction)
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewStatusButtonView {
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
|
func initialSetup() {
|
||||||
|
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
|
||||||
|
let blurView = UIVisualEffectView(effect: blurEffect)
|
||||||
|
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect, style: .label))
|
||||||
|
|
||||||
|
backgroundColor = .clear
|
||||||
|
layer.cornerRadius = .newStatusButtonDimension / 2
|
||||||
|
layer.shadowPath = UIBezierPath(
|
||||||
|
ovalIn: .init(
|
||||||
|
origin: .zero,
|
||||||
|
size: .init(
|
||||||
|
width: .newStatusButtonDimension,
|
||||||
|
height: .newStatusButtonDimension)))
|
||||||
|
.cgPath
|
||||||
|
layer.shadowOffset = .zero
|
||||||
|
layer.shadowRadius = .defaultShadowRadius
|
||||||
|
layer.shadowOpacity = 0.25
|
||||||
|
|
||||||
|
addSubview(blurView)
|
||||||
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
blurView.layer.cornerRadius = .newStatusButtonDimension / 2
|
||||||
|
blurView.clipsToBounds = true
|
||||||
|
blurView.contentView.addSubview(vibrancyView)
|
||||||
|
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let touchStartAction = UIAction { [weak self] _ in self?.alpha = 0.75 }
|
||||||
|
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
button.addAction(touchStartAction, for: .touchDown)
|
||||||
|
button.addAction(touchStartAction, for: .touchDragEnter)
|
||||||
|
|
||||||
|
let touchEndAction = UIAction { [weak self] _ in self?.alpha = 1 }
|
||||||
|
|
||||||
|
button.addAction(touchEndAction, for: .touchDragExit)
|
||||||
|
button.addAction(touchEndAction, for: .touchUpInside)
|
||||||
|
button.addAction(touchEndAction, for: .touchUpOutside)
|
||||||
|
button.addAction(touchEndAction, for: .touchCancel)
|
||||||
|
|
||||||
|
button.setImage(
|
||||||
|
UIImage(systemName: "pencil",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: .newStatusButtonDimension / 2)),
|
||||||
|
for: .normal)
|
||||||
|
vibrancyView.contentView.addSubview(button)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
vibrancyView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||||
|
vibrancyView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||||
|
vibrancyView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||||
|
vibrancyView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
|
||||||
|
button.leadingAnchor.constraint(equalTo: vibrancyView.contentView.leadingAnchor),
|
||||||
|
button.topAnchor.constraint(equalTo: vibrancyView.contentView.topAnchor),
|
||||||
|
button.trailingAnchor.constraint(equalTo: vibrancyView.contentView.trailingAnchor),
|
||||||
|
button.bottomAnchor.constraint(equalTo: vibrancyView.contentView.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class SecondaryNavigationButton: UIBarButtonItem {
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
let button = UIButton(
|
||||||
|
type: .custom,
|
||||||
|
primaryAction: UIAction { _ in viewModel.presentingSecondaryNavigation = true })
|
||||||
|
let downsampled = KingfisherOptionsInfo.downsampled(
|
||||||
|
dimension: .barButtonItemDimension,
|
||||||
|
scaleFactor: UIScreen.main.scale)
|
||||||
|
|
||||||
|
button.imageView?.contentMode = .scaleAspectFill
|
||||||
|
button.layer.cornerRadius = .barButtonItemDimension / 2
|
||||||
|
button.clipsToBounds = true
|
||||||
|
|
||||||
|
customView = button
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
button.widthAnchor.constraint(equalToConstant: .barButtonItemDimension),
|
||||||
|
button.heightAnchor.constraint(equalToConstant: .barButtonItemDimension)
|
||||||
|
])
|
||||||
|
|
||||||
|
viewModel.identification.$identity.sink {
|
||||||
|
button.kf.setImage(
|
||||||
|
with: $0.image,
|
||||||
|
for: .normal,
|
||||||
|
placeholder: UIImage(systemName: "line.horizontal.3"),
|
||||||
|
options: downsampled)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$recentIdentities.sink { identities in
|
||||||
|
button.menu = UIMenu(children: identities.map { identity in
|
||||||
|
UIDeferredMenuElement { completion in
|
||||||
|
let action = UIAction(title: identity.handle) { _ in
|
||||||
|
rootViewModel.identitySelected(id: identity.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = identity.image {
|
||||||
|
KingfisherManager.shared.retrieveImage(with: image, options: downsampled) {
|
||||||
|
if case let .success(value) = $0 {
|
||||||
|
action.image = value.image
|
||||||
|
}
|
||||||
|
|
||||||
|
completion([action])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion([action])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,73 +10,76 @@ struct SecondaryNavigationView: View {
|
||||||
@Environment(\.displayScale) var displayScale: CGFloat
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Form {
|
||||||
Form {
|
Section {
|
||||||
Section {
|
NavigationLink(
|
||||||
NavigationLink(
|
destination: IdentitiesView(viewModel: .init(identification: viewModel.identification))
|
||||||
destination: IdentitiesView(viewModel: .init(identification: viewModel.identification)),
|
.environmentObject(rootViewModel)
|
||||||
label: {
|
.environmentObject(viewModel.identification),
|
||||||
HStack {
|
label: {
|
||||||
KFImage(viewModel.identification.identity.image)
|
HStack {
|
||||||
.downsampled(dimension: .avatarDimension, scaleFactor: displayScale)
|
KFImage(viewModel.identification.identity.image)
|
||||||
VStack(alignment: .leading) {
|
.downsampled(dimension: .avatarDimension, scaleFactor: displayScale)
|
||||||
if viewModel.identification.identity.authenticated {
|
VStack(alignment: .leading) {
|
||||||
if let account = viewModel.identification.identity.account {
|
if viewModel.identification.identity.authenticated {
|
||||||
CustomEmojiText(
|
if let account = viewModel.identification.identity.account {
|
||||||
text: account.displayName,
|
CustomEmojiText(
|
||||||
emojis: account.emojis,
|
text: account.displayName,
|
||||||
textStyle: .headline)
|
emojis: account.emojis,
|
||||||
}
|
textStyle: .headline)
|
||||||
Text(viewModel.identification.identity.handle)
|
}
|
||||||
|
Text(viewModel.identification.identity.handle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
} else {
|
||||||
|
Text(viewModel.identification.identity.handle)
|
||||||
|
.font(.headline)
|
||||||
|
if let instance = viewModel.identification.identity.instance {
|
||||||
|
Text(instance.uri)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.5)
|
.minimumScaleFactor(0.5)
|
||||||
} else {
|
|
||||||
Text(viewModel.identification.identity.handle)
|
|
||||||
.font(.headline)
|
|
||||||
if let instance = viewModel.identification.identity.instance {
|
|
||||||
Text(instance.uri)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
Text("secondary-navigation.manage-accounts")
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
|
Spacer()
|
||||||
|
Text("secondary-navigation.manage-accounts")
|
||||||
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
})
|
.padding()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
NavigationLink(destination: ListsView(viewModel: .init(identification: viewModel.identification))
|
||||||
|
.environmentObject(rootViewModel)
|
||||||
|
.environmentObject(viewModel.identification)) {
|
||||||
|
Label("secondary-navigation.lists", systemImage: "scroll")
|
||||||
}
|
}
|
||||||
Section {
|
ForEach([Timeline.favorites, Timeline.bookmarks]) { timeline in
|
||||||
NavigationLink(destination: ListsView(viewModel: .init(identification: viewModel.identification))) {
|
Button {
|
||||||
Label("secondary-navigation.lists", systemImage: "scroll")
|
viewModel.navigate(timeline: timeline)
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text(timeline.title).foregroundColor(.primary)
|
||||||
|
} icon: {
|
||||||
|
Image(systemName: timeline.systemImageName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Section {
|
|
||||||
NavigationLink(
|
|
||||||
"secondary-navigation.preferences",
|
|
||||||
destination: PreferencesView(
|
|
||||||
viewModel: .init(identification: viewModel.identification)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
Section {
|
||||||
.toolbar {
|
NavigationLink(
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
destination: PreferencesView(viewModel: .init(identification: viewModel.identification))
|
||||||
Button {
|
.environmentObject(rootViewModel)
|
||||||
viewModel.presentingSecondaryNavigation = false
|
.environmentObject(viewModel.identification)) {
|
||||||
} label: {
|
Label("secondary-navigation.preferences", systemImage: "gear")
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
.environmentObject(viewModel.identification)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,268 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import Kingfisher
|
|
||||||
import SwiftUI
|
|
||||||
import ViewModels
|
|
||||||
|
|
||||||
struct TabNavigationView: View {
|
|
||||||
@ObservedObject var viewModel: NavigationViewModel
|
|
||||||
@EnvironmentObject var rootViewModel: RootViewModel
|
|
||||||
@Environment(\.displayScale) var displayScale: CGFloat
|
|
||||||
@State var selectedTab = NavigationViewModel.Tab.timelines
|
|
||||||
|
|
||||||
@State private var contextMenuImages = [UUID: KFImage]()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
if viewModel.identification.identity.pending {
|
|
||||||
pendingView
|
|
||||||
} else {
|
|
||||||
TabView(selection: $selectedTab) {
|
|
||||||
ForEach(viewModel.tabs) { tab in
|
|
||||||
NavigationView {
|
|
||||||
view(tab: tab)
|
|
||||||
}
|
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
.tabItem {
|
|
||||||
Label(tab.title, systemImage: tab.systemImageName)
|
|
||||||
.accessibility(label: Text(tab.title))
|
|
||||||
}
|
|
||||||
.tag(tab)
|
|
||||||
.overlay(newStatusButton, alignment: .bottomTrailing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.environmentObject(viewModel.identification)
|
|
||||||
.sheet(isPresented: $viewModel.presentingSecondaryNavigation) {
|
|
||||||
SecondaryNavigationView(viewModel: viewModel)
|
|
||||||
.environmentObject(viewModel)
|
|
||||||
.environmentObject(rootViewModel)
|
|
||||||
}
|
|
||||||
.background(
|
|
||||||
EmptyView()
|
|
||||||
.fullScreenCover(isPresented: $viewModel.presentingNewStatus) {
|
|
||||||
NavigationView {
|
|
||||||
NewStatusView { rootViewModel.newStatusViewModel(identification: viewModel.identification) }
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
}
|
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
.environmentObject(viewModel)
|
|
||||||
.environmentObject(rootViewModel)
|
|
||||||
})
|
|
||||||
.alertItem($viewModel.alertItem)
|
|
||||||
.onAppear(perform: viewModel.refreshIdentity)
|
|
||||||
// Have to preload these, otherwise the context menu won't display them when first expanded
|
|
||||||
.onReceive(viewModel.$recentIdentities) {
|
|
||||||
contextMenuImages = Dictionary(uniqueKeysWithValues: $0.map {
|
|
||||||
($0.id, KFImage($0.image)
|
|
||||||
.downsampled(
|
|
||||||
dimension: .barButtonItemDimension,
|
|
||||||
scaleFactor: displayScale)
|
|
||||||
.renderingMode(.original))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.onReceive(NotificationCenter.default
|
|
||||||
.publisher(for: UIScene.willEnterForegroundNotification)
|
|
||||||
.map { _ in () },
|
|
||||||
perform: viewModel.refreshIdentity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension TabNavigationView {
|
|
||||||
@ViewBuilder
|
|
||||||
var pendingView: some View {
|
|
||||||
NavigationView {
|
|
||||||
Text("pending.pending-confirmation")
|
|
||||||
.navigationBarItems(leading: secondaryNavigationButton)
|
|
||||||
.navigationTitle(viewModel.identification.identity.handle)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
}
|
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
// swiftlint:disable:next function_body_length
|
|
||||||
func view(tab: NavigationViewModel.Tab) -> some View {
|
|
||||||
switch tab {
|
|
||||||
case .timelines:
|
|
||||||
TableView { viewModel.timelineViewModel }
|
|
||||||
.id(viewModel.timeline.id)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.navigationTitle(viewModel.timeline.title)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .principal) {
|
|
||||||
VStack {
|
|
||||||
Text(viewModel.timeline.title)
|
|
||||||
.font(.headline)
|
|
||||||
Text(viewModel.timelineSubtitle)
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarItems(
|
|
||||||
leading: secondaryNavigationButton,
|
|
||||||
trailing: Menu {
|
|
||||||
ForEach(viewModel.timelinesAndLists) { timeline in
|
|
||||||
Button {
|
|
||||||
viewModel.timeline = timeline
|
|
||||||
} label: {
|
|
||||||
Label(timeline.title,
|
|
||||||
systemImage: timeline.systemImageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: viewModel.timeline.systemImageName)
|
|
||||||
.padding([.leading, .top, .bottom])
|
|
||||||
})
|
|
||||||
case .notifications:
|
|
||||||
if let notificationsViewModel = viewModel.notificationsViewModel {
|
|
||||||
TableView { notificationsViewModel }
|
|
||||||
.id(tab)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.navigationTitle("notifications")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.navigationBarItems(leading: secondaryNavigationButton)
|
|
||||||
}
|
|
||||||
case .messages:
|
|
||||||
if let conversationsViewModel = viewModel.conversationsViewModel {
|
|
||||||
TableView { conversationsViewModel }
|
|
||||||
.id(tab)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.navigationTitle("messages")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.navigationBarItems(leading: secondaryNavigationButton)
|
|
||||||
}
|
|
||||||
default: Text(tab.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
var secondaryNavigationButton: some View {
|
|
||||||
Button {
|
|
||||||
viewModel.presentingSecondaryNavigation.toggle()
|
|
||||||
} label: {
|
|
||||||
KFImage(viewModel.identification.identity.image)
|
|
||||||
.downsampled(
|
|
||||||
dimension: .barButtonItemDimension,
|
|
||||||
scaleFactor: displayScale)
|
|
||||||
.placeholder { Image(systemName: "gear") }
|
|
||||||
.renderingMode(.original)
|
|
||||||
.contextMenu(ContextMenu {
|
|
||||||
ForEach(viewModel.recentIdentities) { recentIdentity in
|
|
||||||
Button {
|
|
||||||
rootViewModel.identitySelected(id: recentIdentity.id)
|
|
||||||
} label: {
|
|
||||||
Label(
|
|
||||||
title: { Text(recentIdentity.handle) },
|
|
||||||
icon: { contextMenuImages[recentIdentity.id] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.padding([.trailing, .top, .bottom])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
var newStatusButton: some View {
|
|
||||||
if viewModel.identification.identity.authenticated
|
|
||||||
&& !viewModel.identification.identity.pending {
|
|
||||||
Button {
|
|
||||||
viewModel.presentingNewStatus = true
|
|
||||||
} label: {
|
|
||||||
VisualEffectBlur(vibrancyStyle: .label) {
|
|
||||||
Image(systemName: "pencil")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: .newStatusButtonDimension / 2,
|
|
||||||
height: .newStatusButtonDimension / 2)
|
|
||||||
}
|
|
||||||
.clipShape(Circle())
|
|
||||||
.frame(width: .newStatusButtonDimension,
|
|
||||||
height: .newStatusButtonDimension)
|
|
||||||
.shadow(radius: .defaultShadowRadius)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move
|
|
||||||
extension Timeline {
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .home:
|
|
||||||
return NSLocalizedString("timelines.home", comment: "")
|
|
||||||
case .local:
|
|
||||||
return NSLocalizedString("timelines.local", comment: "")
|
|
||||||
case .federated:
|
|
||||||
return NSLocalizedString("timelines.federated", comment: "")
|
|
||||||
case let .list(list):
|
|
||||||
return list.title
|
|
||||||
case let .tag(tag):
|
|
||||||
return "#".appending(tag)
|
|
||||||
case .profile:
|
|
||||||
return ""
|
|
||||||
case .favorites:
|
|
||||||
return NSLocalizedString("favorites", comment: "")
|
|
||||||
case .bookmarks:
|
|
||||||
return NSLocalizedString("bookmarks", comment: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func subtitle(identification: Identification) -> String? {
|
|
||||||
switch self {
|
|
||||||
case .home:
|
|
||||||
return identification.identity.handle
|
|
||||||
default:
|
|
||||||
return identification.identity.instance?.uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func description(instanceName: String?) -> String? {
|
|
||||||
switch self {
|
|
||||||
case .home:
|
|
||||||
return NSLocalizedString("timelines.home.description", comment: "")
|
|
||||||
case .local:
|
|
||||||
guard let instanceName = instanceName else { return nil }
|
|
||||||
|
|
||||||
return String.localizedStringWithFormat(
|
|
||||||
NSLocalizedString("timelines.local.description-%@", comment: ""),
|
|
||||||
instanceName)
|
|
||||||
case .federated:
|
|
||||||
guard let instanceName = instanceName else { return nil }
|
|
||||||
|
|
||||||
return String.localizedStringWithFormat(
|
|
||||||
NSLocalizedString("timelines.federated.description-%@", comment: ""),
|
|
||||||
instanceName)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemImageName: String {
|
|
||||||
switch self {
|
|
||||||
case .home: return "house"
|
|
||||||
case .local: return "building.2.crop.circle"
|
|
||||||
case .federated: return "network"
|
|
||||||
case .list: return "scroll"
|
|
||||||
case .tag: return "number"
|
|
||||||
case .profile: return "person"
|
|
||||||
case .favorites: return "star"
|
|
||||||
case .bookmarks: return "bookmark"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import PreviewViewModels
|
|
||||||
|
|
||||||
struct TabNavigation_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
TabNavigationView(viewModel: NavigationViewModel(identification: .preview))
|
|
||||||
.environmentObject(Identification.preview)
|
|
||||||
.environmentObject(RootViewModel.preview)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -16,13 +16,3 @@ struct TableView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import PreviewViewModels
|
|
||||||
|
|
||||||
struct StatusListView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
TableView { NavigationViewModel(identification: .preview).timelineViewModel }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
// Copyright © 2021 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import UIKit
|
|
||||||
import ViewModels
|
|
||||||
|
|
||||||
final class TimelinesTitleView: UIControl {
|
|
||||||
let timelines: [Timeline]
|
|
||||||
private let titleLabel = UILabel()
|
|
||||||
private let subtitleLabel = UILabel()
|
|
||||||
private let imageView = UIImageView()
|
|
||||||
private let chevronImageView = UIImageView(image: TimelinesTitleView.closedImage)
|
|
||||||
private let identification: Identification
|
|
||||||
|
|
||||||
@Published var selectedTimeline: Timeline {
|
|
||||||
didSet { applyTimelineSelection() }
|
|
||||||
}
|
|
||||||
|
|
||||||
init(timelines: [Timeline], identification: Identification) {
|
|
||||||
self.timelines = timelines
|
|
||||||
self.identification = identification
|
|
||||||
|
|
||||||
guard let timeline = timelines.first else {
|
|
||||||
fatalError("TimelinesTitleView must be initialized with a non-empty timelines array")
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedTimeline = timeline
|
|
||||||
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
accessibilityTraits = .button
|
|
||||||
isAccessibilityElement = true
|
|
||||||
showsMenuAsPrimaryAction = true
|
|
||||||
isContextMenuInteractionEnabled = true
|
|
||||||
|
|
||||||
addSubview(imageView)
|
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
imageView.contentMode = .scaleAspectFit
|
|
||||||
imageView.setContentHuggingPriority(.required, for: .horizontal)
|
|
||||||
imageView.tintColor = .label
|
|
||||||
|
|
||||||
addSubview(chevronImageView)
|
|
||||||
chevronImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
chevronImageView.contentMode = .scaleAspectFit
|
|
||||||
chevronImageView.setContentHuggingPriority(.required, for: .horizontal)
|
|
||||||
|
|
||||||
addSubview(titleLabel)
|
|
||||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
titleLabel.adjustsFontForContentSizeCategory = true
|
|
||||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
|
||||||
titleLabel.adjustsFontSizeToFitWidth = true
|
|
||||||
titleLabel.minimumScaleFactor = 0.5
|
|
||||||
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
|
|
||||||
titleLabel.setContentHuggingPriority(.required, for: .vertical)
|
|
||||||
titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
||||||
|
|
||||||
addSubview(subtitleLabel)
|
|
||||||
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
subtitleLabel.adjustsFontForContentSizeCategory = true
|
|
||||||
subtitleLabel.font = .preferredFont(forTextStyle: .caption2)
|
|
||||||
subtitleLabel.adjustsFontSizeToFitWidth = true
|
|
||||||
subtitleLabel.textAlignment = .center
|
|
||||||
subtitleLabel.minimumScaleFactor = 0.5
|
|
||||||
subtitleLabel.textColor = .secondaryLabel
|
|
||||||
subtitleLabel.setContentHuggingPriority(.required, for: .vertical)
|
|
||||||
subtitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
||||||
subtitleLabel.setContentCompressionResistancePriority(.justBelowMax, for: .vertical)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
imageView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
|
|
||||||
imageView.topAnchor.constraint(equalTo: titleLabel.topAnchor),
|
|
||||||
imageView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor),
|
|
||||||
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
||||||
titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: .compactSpacing),
|
|
||||||
titleLabel.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
chevronImageView.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: .defaultSpacing),
|
|
||||||
chevronImageView.topAnchor.constraint(equalTo: titleLabel.topAnchor),
|
|
||||||
chevronImageView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor),
|
|
||||||
chevronImageView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
|
||||||
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
|
|
||||||
subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
||||||
])
|
|
||||||
|
|
||||||
applyTimelineSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(*, unavailable)
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override var isHighlighted: Bool {
|
|
||||||
didSet {
|
|
||||||
alpha = isHighlighted ? Self.highlightedAlpha : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func menuAttachmentPoint(for configuration: UIContextMenuConfiguration) -> CGPoint {
|
|
||||||
CGPoint(x: (bounds.width - .systemMenuWidth) / 2 + .systemMenuInset, y: bounds.maxY + .compactSpacing)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func contextMenuInteraction(
|
|
||||||
_ interaction: UIContextMenuInteraction,
|
|
||||||
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
|
|
||||||
guard let self = self else { return nil }
|
|
||||||
|
|
||||||
return UIMenu(children: self.timelines.map { timeline in
|
|
||||||
UIAction(
|
|
||||||
title: timeline.title,
|
|
||||||
image: UIImage(systemName: timeline.systemImageName),
|
|
||||||
attributes: timeline == self.selectedTimeline ? .disabled : [],
|
|
||||||
state: timeline == self.selectedTimeline ? .on : .off) { _ in
|
|
||||||
self.selectedTimeline = timeline
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func contextMenuInteraction(
|
|
||||||
_ interaction: UIContextMenuInteraction,
|
|
||||||
willDisplayMenuFor configuration: UIContextMenuConfiguration,
|
|
||||||
animator: UIContextMenuInteractionAnimating?) {
|
|
||||||
chevronImageView.image = Self.openImage
|
|
||||||
}
|
|
||||||
|
|
||||||
override func contextMenuInteraction(
|
|
||||||
_ interaction: UIContextMenuInteraction,
|
|
||||||
willEndFor configuration: UIContextMenuConfiguration,
|
|
||||||
animator: UIContextMenuInteractionAnimating?) {
|
|
||||||
chevronImageView.image = Self.closedImage
|
|
||||||
alpha = 1 // system bug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension TimelinesTitleView {
|
|
||||||
static let highlightedAlpha: CGFloat = 0.5
|
|
||||||
static let openImage = UIImage(
|
|
||||||
systemName: "chevron.compact.up",
|
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
|
||||||
static let closedImage = UIImage(
|
|
||||||
systemName: "chevron.compact.down",
|
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
|
||||||
func applyTimelineSelection() {
|
|
||||||
imageView.image = UIImage(
|
|
||||||
systemName: selectedTimeline.systemImageName,
|
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
|
||||||
titleLabel.text = selectedTimeline.title
|
|
||||||
subtitleLabel.text = selectedTimeline.subtitle(identification: identification)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ extension CGFloat {
|
||||||
static let hairline = 1 / UIScreen.main.scale
|
static let hairline = 1 / UIScreen.main.scale
|
||||||
static let minimumButtonDimension: Self = 44
|
static let minimumButtonDimension: Self = 44
|
||||||
static let barButtonItemDimension: Self = 28
|
static let barButtonItemDimension: Self = 28
|
||||||
static let newStatusButtonDimension: Self = 54
|
static let newStatusButtonDimension: Self = 58
|
||||||
static let defaultShadowRadius: Self = 2
|
static let defaultShadowRadius: Self = 2
|
||||||
static let systemMenuWidth: Self = 250
|
static let systemMenuWidth: Self = 250
|
||||||
static let systemMenuInset: Self = 15
|
static let systemMenuInset: Self = 15
|
||||||
|
|
Loading…
Reference in New Issue