Compare commits
6 Commits
348c306858
...
75d26e613b
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 75d26e613b | |
Shadowfacts | 904ff4eecf | |
Shadowfacts | 0249207dcc | |
Shadowfacts | 366378f267 | |
Shadowfacts | 80cca7673a | |
Shadowfacts | fc888b168c |
|
@ -76,7 +76,6 @@ public class Client {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let result = try? Client.decoder.decode(Result.self, from: data) else {
|
guard let result = try? Client.decoder.decode(Result.self, from: data) else {
|
||||||
print(request)
|
|
||||||
completion(.failure(.invalidModel))
|
completion(.failure(.invalidModel))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,25 @@ public class Emoji: Codable {
|
||||||
public let staticURL: URL
|
public let staticURL: URL
|
||||||
public let visibleInPicker: Bool
|
public let visibleInPicker: Bool
|
||||||
|
|
||||||
|
public required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
||||||
|
if let url = try? container.decode(URL.self, forKey: .url) {
|
||||||
|
self.url = url
|
||||||
|
} else {
|
||||||
|
let str = try container.decode(String.self, forKey: .url)
|
||||||
|
self.url = URL(string: str.replacingOccurrences(of: " ", with: "%20"))!
|
||||||
|
}
|
||||||
|
if let url = try? container.decode(URL.self, forKey: .staticURL) {
|
||||||
|
self.staticURL = url
|
||||||
|
} else {
|
||||||
|
let staticStr = try container.decode(String.self, forKey: .staticURL)
|
||||||
|
self.staticURL = URL(string: staticStr.replacingOccurrences(of: " ", with: "%20"))!
|
||||||
|
}
|
||||||
|
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
|
||||||
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case shortcode
|
case shortcode
|
||||||
case url
|
case url
|
||||||
|
|
|
@ -211,6 +211,10 @@
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */; };
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */; };
|
||||||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; };
|
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; };
|
||||||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */; };
|
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */; };
|
||||||
|
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */; };
|
||||||
|
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; };
|
||||||
|
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */; };
|
||||||
|
D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */; };
|
||||||
D6A5BB2B23BAEF61003BF21D /* APIMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */; };
|
D6A5BB2B23BAEF61003BF21D /* APIMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */; };
|
||||||
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
||||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
||||||
|
@ -293,6 +297,7 @@
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
||||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
|
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
||||||
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -556,6 +561,10 @@
|
||||||
D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = "<group>"; };
|
D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = "<group>"; };
|
D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListTableViewController.swift; sourceTree = "<group>"; };
|
D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListTableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FastAccountSwitcherViewController.xib; sourceTree = "<group>"; };
|
||||||
|
D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastSwitchingAccountView.swift; sourceTree = "<group>"; };
|
||||||
|
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTextViewCaretScrolling.swift; sourceTree = "<group>"; };
|
||||||
D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIMocks.swift; sourceTree = "<group>"; };
|
D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIMocks.swift; sourceTree = "<group>"; };
|
||||||
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
|
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
|
||||||
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
||||||
|
@ -641,6 +650,7 @@
|
||||||
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; };
|
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; };
|
||||||
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||||
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -932,6 +942,7 @@
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
D641C783213DD7FE004B4513 /* Onboarding */,
|
D641C783213DD7FE004B4513 /* Onboarding */,
|
||||||
|
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||||
D641C781213DD7DD004B4513 /* Timeline */,
|
D641C781213DD7DD004B4513 /* Timeline */,
|
||||||
D641C784213DD819004B4513 /* Profile */,
|
D641C784213DD819004B4513 /* Profile */,
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
|
@ -966,6 +977,7 @@
|
||||||
D641C782213DD7F0004B4513 /* Main */ = {
|
D641C782213DD7F0004B4513 /* Main */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||||
|
@ -1020,6 +1032,7 @@
|
||||||
D677284724ECBCB100C732D3 /* ComposeView.swift */,
|
D677284724ECBCB100C732D3 /* ComposeView.swift */,
|
||||||
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */,
|
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */,
|
||||||
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */,
|
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */,
|
||||||
|
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */,
|
||||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
||||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
||||||
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
||||||
|
@ -1227,6 +1240,16 @@
|
||||||
path = "Status Action Account List";
|
path = "Status Action Account List";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */,
|
||||||
|
D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */,
|
||||||
|
D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */,
|
||||||
|
);
|
||||||
|
path = "Fast Account Switcher";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6A5BB2623BAC88E003BF21D /* Preferences */ = {
|
D6A5BB2623BAC88E003BF21D /* Preferences */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1691,6 +1714,7 @@
|
||||||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||||
|
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1830,7 +1854,9 @@
|
||||||
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */,
|
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||||
|
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
||||||
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */,
|
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */,
|
||||||
|
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||||
|
@ -1841,6 +1867,7 @@
|
||||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */,
|
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */,
|
||||||
|
D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */,
|
||||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
|
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
|
||||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||||
|
@ -1947,6 +1974,7 @@
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
|
|
||||||
extension UIFont {
|
extension UIFont {
|
||||||
|
|
||||||
func addingTraits(_ traits: UIFontDescriptor.SymbolicTraits, size: CGFloat? = nil) -> UIFont? {
|
func withTraits(_ traits: UIFontDescriptor.SymbolicTraits, size: CGFloat? = nil) -> UIFont? {
|
||||||
let descriptor = self.fontDescriptor
|
let descriptor = self.fontDescriptor
|
||||||
guard let newDescriptor = descriptor.withSymbolicTraits([descriptor.symbolicTraits, traits]) else {
|
guard let newDescriptor = descriptor.withSymbolicTraits([descriptor.symbolicTraits, traits]) else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -37,10 +37,10 @@ public struct LazilyDecoding<Enclosing, Value: Codable> {
|
||||||
} else {
|
} else {
|
||||||
guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback }
|
guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback }
|
||||||
do {
|
do {
|
||||||
let value = try decoder.decode(Value.self, from: data)
|
let value = try decoder.decode(Box<Value>.self, from: data)
|
||||||
wrapper.value = value
|
wrapper.value = value.value
|
||||||
instance[keyPath: storageKeyPath] = wrapper
|
instance[keyPath: storageKeyPath] = wrapper
|
||||||
return value
|
return value.value
|
||||||
} catch {
|
} catch {
|
||||||
return wrapper.fallback
|
return wrapper.fallback
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ public struct LazilyDecoding<Enclosing, Value: Codable> {
|
||||||
var wrapper = instance[keyPath: storageKeyPath]
|
var wrapper = instance[keyPath: storageKeyPath]
|
||||||
wrapper.value = newValue
|
wrapper.value = newValue
|
||||||
instance[keyPath: storageKeyPath] = wrapper
|
instance[keyPath: storageKeyPath] = wrapper
|
||||||
let newData = try? encoder.encode(newValue)
|
let newData = try! encoder.encode(Box(value: newValue))
|
||||||
instance[keyPath: wrapper.keyPath] = newData
|
instance[keyPath: wrapper.keyPath] = newData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,3 +62,11 @@ extension LazilyDecoding {
|
||||||
self.init(from: keyPath, fallback: [] as! Value)
|
self.init(from: keyPath, fallback: [] as! Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LazilyDecoding {
|
||||||
|
// PropertyListEncoder only allows top-level types to be dicts or arrays, which breaks encoding nil-able values.
|
||||||
|
// Wrapping everything in a Box ensures that it's always a dict.
|
||||||
|
private struct Box<T: Codable>: Codable {
|
||||||
|
let value: T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ class LocalData: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private let mostRecentAccountKey = "mostRecentAccount"
|
private let mostRecentAccountKey = "mostRecentAccount"
|
||||||
private var mostRecentAccount: String? {
|
private(set) var mostRecentAccountID: String? {
|
||||||
get {
|
get {
|
||||||
return defaults.string(forKey: mostRecentAccountKey)
|
return defaults.string(forKey: mostRecentAccountKey)
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ class LocalData: ObservableObject {
|
||||||
func getMostRecentAccount() -> UserAccountInfo? {
|
func getMostRecentAccount() -> UserAccountInfo? {
|
||||||
guard onboardingComplete else { return nil }
|
guard onboardingComplete else { return nil }
|
||||||
let mostRecent: UserAccountInfo?
|
let mostRecent: UserAccountInfo?
|
||||||
if let id = mostRecentAccount {
|
if let id = mostRecentAccountID {
|
||||||
mostRecent = accounts.first { $0.id == id }
|
mostRecent = accounts.first { $0.id == id }
|
||||||
} else {
|
} else {
|
||||||
mostRecent = nil
|
mostRecent = nil
|
||||||
|
@ -140,7 +140,7 @@ class LocalData: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMostRecentAccount(_ account: UserAccountInfo?) {
|
func setMostRecentAccount(_ account: UserAccountInfo?) {
|
||||||
mostRecentAccount = account?.id
|
mostRecentAccountID = account?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,50 +130,64 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
||||||
let session = session ?? window!.windowScene!.session
|
let session = session ?? window!.windowScene!.session
|
||||||
if LocalData.shared.onboardingComplete {
|
if LocalData.shared.onboardingComplete {
|
||||||
if session.mastodonController == nil {
|
|
||||||
let account = LocalData.shared.getMostRecentAccount()!
|
let account = LocalData.shared.getMostRecentAccount()!
|
||||||
|
if session.mastodonController == nil {
|
||||||
session.mastodonController = MastodonController.getForAccount(account)
|
session.mastodonController = MastodonController.getForAccount(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
showAppUI()
|
activateAccount(account, animated: false)
|
||||||
} else {
|
} else {
|
||||||
showOnboardingUI()
|
window!.rootViewController = createOnboardingUI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func activateAccount(_ account: LocalData.UserAccountInfo) {
|
func activateAccount(_ account: LocalData.UserAccountInfo, animated: Bool) {
|
||||||
|
let oldIndex = LocalData.shared.accounts.firstIndex(where: { $0.id == LocalData.shared.mostRecentAccountID })!
|
||||||
|
let newIndex = LocalData.shared.accounts.firstIndex(of: account)!
|
||||||
|
|
||||||
LocalData.shared.setMostRecentAccount(account)
|
LocalData.shared.setMostRecentAccount(account)
|
||||||
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
||||||
showAppUI()
|
|
||||||
|
let newRoot = createAppUI()
|
||||||
|
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
|
||||||
|
let direction: AccountSwitchingContainerViewController.AnimationDirection
|
||||||
|
if animated {
|
||||||
|
direction = newIndex > oldIndex ? .upwards : .downwards
|
||||||
|
} else {
|
||||||
|
direction = .none
|
||||||
|
}
|
||||||
|
container.setRoot(newRoot, animating: direction)
|
||||||
|
} else {
|
||||||
|
window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutCurrent() {
|
func logoutCurrent() {
|
||||||
LocalData.shared.removeAccount(LocalData.shared.getMostRecentAccount()!)
|
LocalData.shared.removeAccount(LocalData.shared.getMostRecentAccount()!)
|
||||||
if LocalData.shared.onboardingComplete {
|
if LocalData.shared.onboardingComplete {
|
||||||
activateAccount(LocalData.shared.accounts.first!)
|
activateAccount(LocalData.shared.accounts.first!, animated: false)
|
||||||
} else {
|
} else {
|
||||||
showOnboardingUI()
|
window!.rootViewController = createOnboardingUI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAppUI() {
|
func createAppUI() -> TuskerRootViewController {
|
||||||
let mastodonController = window!.windowScene!.session.mastodonController!
|
let mastodonController = window!.windowScene!.session.mastodonController!
|
||||||
mastodonController.getOwnAccount()
|
mastodonController.getOwnAccount()
|
||||||
mastodonController.getOwnInstance()
|
mastodonController.getOwnInstance()
|
||||||
|
|
||||||
let rootController: UIViewController
|
if #available(iOS 14.0, *),
|
||||||
if #available(iOS 14.0, *) {
|
UIDevice.current.userInterfaceIdiom != .phone {
|
||||||
rootController = MainSplitViewController(mastodonController: mastodonController)
|
return MainSplitViewController(mastodonController: mastodonController)
|
||||||
} else {
|
} else {
|
||||||
rootController = MainTabBarViewController(mastodonController: mastodonController)
|
return MainTabBarViewController(mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
window!.rootViewController = rootController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showOnboardingUI() {
|
func createOnboardingUI() -> UIViewController {
|
||||||
let onboarding = OnboardingViewController()
|
let onboarding = OnboardingViewController()
|
||||||
onboarding.onboardingDelegate = self
|
onboarding.onboardingDelegate = self
|
||||||
window!.rootViewController = onboarding
|
return onboarding
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func themePrefChanged() {
|
@objc func themePrefChanged() {
|
||||||
|
@ -184,7 +198,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
extension SceneDelegate: OnboardingViewControllerDelegate {
|
extension SceneDelegate: OnboardingViewControllerDelegate {
|
||||||
func didFinishOnboarding(account: LocalData.UserAccountInfo) {
|
func didFinishOnboarding(account: LocalData.UserAccountInfo) {
|
||||||
activateAccount(account)
|
activateAccount(account, animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,7 @@ struct ComposeAttachmentsList: View {
|
||||||
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
||||||
// enable drag and drop to reorder on iPhone
|
// enable drag and drop to reorder on iPhone
|
||||||
proxy.dragInteractionEnabled = true
|
proxy.dragInteractionEnabled = true
|
||||||
|
proxy.isScrollEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
||||||
|
|
|
@ -101,9 +101,10 @@ struct WrappedTextView: UIViewRepresentable {
|
||||||
return Coordinator(text: $text, didChange: textDidChange)
|
return Coordinator(text: $text, didChange: textDidChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextViewDelegate {
|
class Coordinator: NSObject, UITextViewDelegate, ComposeTextViewCaretScrolling {
|
||||||
var text: Binding<String>
|
var text: Binding<String>
|
||||||
var didChange: ((UITextView) -> Void)?
|
var didChange: ((UITextView) -> Void)?
|
||||||
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
init(text: Binding<String>, didChange: ((UITextView) -> Void)?) {
|
init(text: Binding<String>, didChange: ((UITextView) -> Void)?) {
|
||||||
self.text = text
|
self.text = text
|
||||||
|
@ -113,6 +114,8 @@ struct WrappedTextView: UIViewRepresentable {
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
text.wrappedValue = textView.text
|
text.wrappedValue = textView.text
|
||||||
didChange?(textView)
|
didChange?(textView)
|
||||||
|
|
||||||
|
ensureCursorVisible(textView: textView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
//
|
||||||
|
// ComposeTextViewCaretScrolling.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/11/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ComposeTextViewCaretScrolling: class {
|
||||||
|
var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeTextViewCaretScrolling {
|
||||||
|
func ensureCursorVisible(textView: UITextView) {
|
||||||
|
guard textView.isFirstResponder,
|
||||||
|
let range = textView.selectedTextRange,
|
||||||
|
let scrollView = findParentScrollView(of: textView) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use a UIViewProperty animator to change the scroll view position so that we can store the currently
|
||||||
|
// running one on the Coordinator. This allows us to cancel the running one, preventing multiple animations
|
||||||
|
// from attempting to change the scroll view offset simultaneously, causing it to jitter around. This can
|
||||||
|
// happen if the user is pressing return and quickly creating many new lines.
|
||||||
|
|
||||||
|
if let existing = caretScrollPositionAnimator {
|
||||||
|
existing.stopAnimation(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursorRect = textView.caretRect(for: range.start)
|
||||||
|
var rectToMakeVisible = textView.convert(cursorRect, to: scrollView)
|
||||||
|
|
||||||
|
// expand the rect to be three times the cursor height centered on the cursor so that there's
|
||||||
|
// some space between the bottom of the line of text being edited and the top of the keyboard
|
||||||
|
rectToMakeVisible.origin.y -= cursorRect.height
|
||||||
|
rectToMakeVisible.size.height *= 3
|
||||||
|
|
||||||
|
let animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
|
||||||
|
scrollView.scrollRectToVisible(rectToMakeVisible, animated: false)
|
||||||
|
}
|
||||||
|
self.caretScrollPositionAnimator = animator
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findParentScrollView(of view: UIView) -> UIScrollView? {
|
||||||
|
var current: UIView = view
|
||||||
|
while let superview = current.superview {
|
||||||
|
if let scrollView = superview as? UIScrollView,
|
||||||
|
scrollView.isScrollEnabled {
|
||||||
|
return scrollView
|
||||||
|
} else {
|
||||||
|
current = superview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -171,12 +171,12 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
return Coordinator(text: $text, uiState: uiState, didChange: textDidChange)
|
return Coordinator(text: $text, uiState: uiState, didChange: textDidChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler {
|
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler, ComposeTextViewCaretScrolling {
|
||||||
weak var textView: UITextView?
|
weak var textView: UITextView?
|
||||||
var text: Binding<String>
|
var text: Binding<String>
|
||||||
var didChange: (UITextView) -> Void
|
var didChange: (UITextView) -> Void
|
||||||
var uiState: ComposeUIState
|
var uiState: ComposeUIState
|
||||||
private var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||||
self.text = text
|
self.text = text
|
||||||
|
@ -188,53 +188,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
text.wrappedValue = textView.text
|
text.wrappedValue = textView.text
|
||||||
didChange(textView)
|
didChange(textView)
|
||||||
|
|
||||||
ensureCursorVisible()
|
ensureCursorVisible(textView: textView)
|
||||||
}
|
|
||||||
|
|
||||||
private func ensureCursorVisible() {
|
|
||||||
guard let textView = textView,
|
|
||||||
textView.isFirstResponder,
|
|
||||||
let range = textView.selectedTextRange,
|
|
||||||
let scrollView = findParentScrollView() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use a UIViewProperty animator to change the scroll view position so that we can store the currently
|
|
||||||
// running one on the Coordinator. This allows us to cancel the running one, preventing multiple animations
|
|
||||||
// from attempting to change the scroll view offset simultaneously, causing it to jitter around. This can
|
|
||||||
// happen if the user is pressing return and quickly creating many new lines.
|
|
||||||
|
|
||||||
if let existing = caretScrollPositionAnimator {
|
|
||||||
existing.stopAnimation(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
let cursorRect = textView.caretRect(for: range.start)
|
|
||||||
var rectToMakeVisible = textView.convert(cursorRect, to: scrollView)
|
|
||||||
|
|
||||||
// move Y position of the rect that will be made visible down by the cursor's height so that there's
|
|
||||||
// some space between the bottom of the line of text being edited and the top of the keyboard
|
|
||||||
rectToMakeVisible.origin.y += cursorRect.height
|
|
||||||
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
|
|
||||||
scrollView.scrollRectToVisible(rectToMakeVisible, animated: false)
|
|
||||||
}
|
|
||||||
self.caretScrollPositionAnimator = animator
|
|
||||||
animator.startAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func findParentScrollView() -> UIScrollView? {
|
|
||||||
guard let textView = textView else { return nil }
|
|
||||||
|
|
||||||
var current: UIView = textView
|
|
||||||
while let superview = current.superview {
|
|
||||||
if let scrollView = superview as? UIScrollView {
|
|
||||||
return scrollView
|
|
||||||
} else {
|
|
||||||
current = superview
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
||||||
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
//
|
||||||
|
// FastAccountSwitcherViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/4/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol FastAccountSwitcherViewControllerDelegate: class {
|
||||||
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
|
weak var delegate: FastAccountSwitcherViewControllerDelegate?
|
||||||
|
|
||||||
|
@IBOutlet weak var dimmingView: UIView!
|
||||||
|
@IBOutlet weak var blurContentView: UIView!
|
||||||
|
@IBOutlet weak var accountsStack: UIStackView!
|
||||||
|
|
||||||
|
private var accountViews: [FastSwitchingAccountView] = []
|
||||||
|
private var lastSelectedAccountViewIndex: Int?
|
||||||
|
private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator?
|
||||||
|
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.isHidden = true
|
||||||
|
|
||||||
|
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))))
|
||||||
|
accountsStack.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSwitcherGesture() -> UIGestureRecognizer {
|
||||||
|
let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
|
||||||
|
recognizer.delegate = self
|
||||||
|
recognizer.minimumPressDuration = 0.25
|
||||||
|
return recognizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func show() {
|
||||||
|
createAccountViews()
|
||||||
|
|
||||||
|
view.isHidden = false
|
||||||
|
|
||||||
|
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
||||||
|
view.alpha = 0
|
||||||
|
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
||||||
|
self.view.alpha = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let totalDuration: TimeInterval = 0.5
|
||||||
|
UIView.animateKeyframes(withDuration: totalDuration, delay: 0, options: .allowUserInteraction) {
|
||||||
|
self.view.alpha = 0
|
||||||
|
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
|
||||||
|
self.view.alpha = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, accountView) in self.accountViews.reversed().enumerated() {
|
||||||
|
let relStart = 0.5 * Double(index) / Double(self.accountsStack.arrangedSubviews.count)
|
||||||
|
let relDuration = 0.5 * 2 / Double(self.accountsStack.arrangedSubviews.count)
|
||||||
|
|
||||||
|
accountView.alpha = 0
|
||||||
|
accountView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
|
||||||
|
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration) {
|
||||||
|
accountView.alpha = 1
|
||||||
|
accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.addKeyframe(withRelativeStartTime: relStart + relDuration, relativeDuration: relDuration) {
|
||||||
|
accountView.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hide(completion: (() -> Void)? = nil) {
|
||||||
|
lastSelectedAccountViewIndex = nil
|
||||||
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.view.alpha = 0
|
||||||
|
} completion: { (_) in
|
||||||
|
self.view.alpha = 1
|
||||||
|
self.view.isHidden = true
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createAccountViews() {
|
||||||
|
accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
accountViews = []
|
||||||
|
|
||||||
|
for account in LocalData.shared.accounts {
|
||||||
|
let accountView = FastSwitchingAccountView(account: account)
|
||||||
|
accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID
|
||||||
|
accountsStack.addArrangedSubview(accountView)
|
||||||
|
accountViews.append(accountView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func accountViewIndex(at point: CGPoint) -> Int? {
|
||||||
|
for (index, accountView) in accountViews.enumerated() {
|
||||||
|
let pointInAccountView = accountView.convert(point, from: view)
|
||||||
|
if accountView.bounds.contains(pointInAccountView) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
||||||
|
let account = LocalData.shared.accounts[newIndex]
|
||||||
|
|
||||||
|
if account.id != LocalData.shared.mostRecentAccountID {
|
||||||
|
if hapticFeedback {
|
||||||
|
selectionChangedFeedbackGenerator?.impactOccurred()
|
||||||
|
}
|
||||||
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
(self.view.window!.windowScene!.delegate as! SceneDelegate).activateAccount(account, animated: true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
@objc private func handleLongPress(_ recognizer: UIGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
selectionChangedFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
selectionChangedFeedbackGenerator?.impactOccurred()
|
||||||
|
selectionChangedFeedbackGenerator?.prepare()
|
||||||
|
|
||||||
|
show()
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
let location = recognizer.location(in: view)
|
||||||
|
|
||||||
|
handleGestureMoved(to: location)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
let location = recognizer.location(in: view)
|
||||||
|
if let index = lastSelectedAccountViewIndex {
|
||||||
|
switchAccount(newIndex: index)
|
||||||
|
} else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: location) ?? false) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .changed:
|
||||||
|
let location = recognizer.location(in: view)
|
||||||
|
|
||||||
|
handleGestureMoved(to: location)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
if let index = lastSelectedAccountViewIndex {
|
||||||
|
switchAccount(newIndex: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGestureMoved(to location: CGPoint, hapticFeedback: Bool = true) {
|
||||||
|
let selectedAccountViewIndex = accountViewIndex(at: location)
|
||||||
|
|
||||||
|
if selectedAccountViewIndex != lastSelectedAccountViewIndex {
|
||||||
|
if let lastSelected = lastSelectedAccountViewIndex {
|
||||||
|
accountViews[lastSelected].isSelected = false
|
||||||
|
}
|
||||||
|
if let newSelected = selectedAccountViewIndex {
|
||||||
|
accountViews[newSelected].isSelected = true
|
||||||
|
}
|
||||||
|
lastSelectedAccountViewIndex = selectedAccountViewIndex
|
||||||
|
|
||||||
|
if hapticFeedback {
|
||||||
|
selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5)
|
||||||
|
selectionChangedFeedbackGenerator?.prepare()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleTap(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if let tappedIndex = accountViewIndex(at: recognizer.location(in: view)) {
|
||||||
|
// cancel the selection-changed feedback initiated in touchesBegan so that,
|
||||||
|
// upon switching, we don't trigger a double feedback
|
||||||
|
touchBeganFeedbackWorkItem?.cancel()
|
||||||
|
touchBeganFeedbackWorkItem = nil
|
||||||
|
|
||||||
|
switchAccount(newIndex: tappedIndex)
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
if touches.count == 1,
|
||||||
|
let touch = touches.first,
|
||||||
|
accountsStack.bounds.contains(touch.location(in: accountsStack)) {
|
||||||
|
handleGestureMoved(to: touch.location(in: view), hapticFeedback: false)
|
||||||
|
|
||||||
|
// don't trigger the haptic feedback immedaitely
|
||||||
|
// if the user is merely tapping, not initiating a pan, we don't want to trigger a double-impact
|
||||||
|
// if the tap ends very quickly, this will be cancelled
|
||||||
|
touchBeganFeedbackWorkItem = DispatchWorkItem {
|
||||||
|
self.selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5)
|
||||||
|
self.selectionChangedFeedbackGenerator?.prepare()
|
||||||
|
self.touchBeganFeedbackWorkItem = nil
|
||||||
|
}
|
||||||
|
// 100ms determined experimentally to be fast enough that there's not a hugely-perceivable delay when beginning a pan gesture
|
||||||
|
// and slow enough that it's longer than most reasonable-speed taps
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: touchBeganFeedbackWorkItem!)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
let point = gestureRecognizer.location(in: view)
|
||||||
|
return delegate?.fastAccountSwitcher(self, triggerZoneContains: point) ?? false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="FastAccountSwitcherViewController" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="accountsStack" destination="lYU-Bb-3Wi" id="Dxs-ta-ORu"/>
|
||||||
|
<outlet property="blurContentView" destination="1Gd-Da-Vab" id="JqT-uq-1o2"/>
|
||||||
|
<outlet property="dimmingView" destination="Lul-oI-bZ7" id="JhP-ZX-8fb"/>
|
||||||
|
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||||
|
</connections>
|
||||||
|
</placeholder>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<view alpha="0.25" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Lul-oI-bZ7">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</view>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5fd-Ni-Owc">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="1Gd-Da-Vab">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="lYU-Bb-3Wi">
|
||||||
|
<rect key="frame" x="8" y="8" width="398" height="880"/>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="top" secondItem="1Gd-Da-Vab" secondAttribute="topMargin" placeholder="YES" id="KQs-d5-U3f"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="lYU-Bb-3Wi" secondAttribute="trailingMargin" constant="8" id="UZh-xR-XVt"/>
|
||||||
|
<constraint firstAttribute="bottomMargin" secondItem="lYU-Bb-3Wi" secondAttribute="bottom" id="j6f-r5-NNI"/>
|
||||||
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="leading" secondItem="1Gd-Da-Vab" secondAttribute="leading" constant="8" id="sae-ga-MGE"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<blurEffect style="systemThinMaterialDark"/>
|
||||||
|
</visualEffectView>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||||
|
<gestureRecognizers/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="Lul-oI-bZ7" secondAttribute="trailing" id="9Fp-IG-O9W"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="5fd-Ni-Owc" secondAttribute="trailing" id="c27-P9-lLK"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Lul-oI-bZ7" secondAttribute="bottom" id="o6y-tG-MwH"/>
|
||||||
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="phf-PC-bdH"/>
|
||||||
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="rz7-cQ-PIC"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="5fd-Ni-Owc" secondAttribute="bottom" id="sHl-iD-kGi"/>
|
||||||
|
<constraint firstItem="Lul-oI-bZ7" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="tfE-Xr-YBo"/>
|
||||||
|
<constraint firstItem="Lul-oI-bZ7" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="ua7-DO-kdp"/>
|
||||||
|
</constraints>
|
||||||
|
<variation key="default">
|
||||||
|
<mask key="subviews">
|
||||||
|
<exclude reference="Lul-oI-bZ7"/>
|
||||||
|
</mask>
|
||||||
|
</variation>
|
||||||
|
<point key="canvasLocation" x="140.57971014492756" y="144.64285714285714"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
</document>
|
|
@ -0,0 +1,116 @@
|
||||||
|
//
|
||||||
|
// FastSwitchingAccountView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/4/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FastSwitchingAccountView: UIView {
|
||||||
|
|
||||||
|
let account: LocalData.UserAccountInfo
|
||||||
|
|
||||||
|
private static let selectedColor = UIColor { (traits) in
|
||||||
|
if traits.userInterfaceStyle == .dark {
|
||||||
|
return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 100 / 100, alpha: 1)
|
||||||
|
} else {
|
||||||
|
return UIColor(hue: 211 / 360, saturation: 70 / 100, brightness: 100 / 100, alpha: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static let currentColor = UIColor { (traits) in
|
||||||
|
if traits.userInterfaceStyle == .dark {
|
||||||
|
return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 85 / 100, alpha: 1)
|
||||||
|
} else {
|
||||||
|
return UIColor(hue: 211 / 360, saturation: 50 / 100, brightness: 100 / 100, alpha: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var isSelected = false {
|
||||||
|
didSet {
|
||||||
|
updateLabelColors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var isCurrent = false {
|
||||||
|
didSet {
|
||||||
|
updateLabelColors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let usernameLabel = UILabel()
|
||||||
|
private let instanceLabel = UILabel()
|
||||||
|
|
||||||
|
private var avatarRequest: ImageCache.Request?
|
||||||
|
|
||||||
|
init(account: LocalData.UserAccountInfo) {
|
||||||
|
self.account = account
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
usernameLabel.textColor = .white
|
||||||
|
usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0)
|
||||||
|
usernameLabel.text = account.username
|
||||||
|
|
||||||
|
instanceLabel.textColor = .white
|
||||||
|
instanceLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .subheadline), size: 0)
|
||||||
|
instanceLabel.text = account.instanceURL.host!
|
||||||
|
|
||||||
|
let stackView = UIStackView(arrangedSubviews: [
|
||||||
|
usernameLabel,
|
||||||
|
instanceLabel
|
||||||
|
])
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.alignment = .trailing
|
||||||
|
addSubview(stackView)
|
||||||
|
|
||||||
|
let avatarImageView = UIImageView()
|
||||||
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarImageView.layer.masksToBounds = true
|
||||||
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 40
|
||||||
|
avatarImageView.image = UIImage(systemName: Preferences.shared.avatarStyle == .circle ? "person.crop.circle" : "person.crop.square")
|
||||||
|
avatarImageView.contentMode = .scaleAspectFit
|
||||||
|
addSubview(avatarImageView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||||
|
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
||||||
|
avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
avatarImageView.widthAnchor.constraint(equalToConstant: 40),
|
||||||
|
avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor),
|
||||||
|
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: avatarImageView.leadingAnchor, constant: -8),
|
||||||
|
stackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let controller = MastodonController.getForAccount(account)
|
||||||
|
controller.getOwnAccount { [weak self] (result) in
|
||||||
|
guard let self = self, case let .success(account) = result else { return }
|
||||||
|
self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (data) in
|
||||||
|
guard let avatarImageView = avatarImageView, let data = data, let image = UIImage(data: data) else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
avatarImageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLabelColors() {
|
||||||
|
let color: UIColor
|
||||||
|
if isSelected {
|
||||||
|
color = FastSwitchingAccountView.selectedColor
|
||||||
|
} else if isCurrent {
|
||||||
|
color = FastSwitchingAccountView.currentColor
|
||||||
|
} else {
|
||||||
|
color = .white
|
||||||
|
}
|
||||||
|
usernameLabel.textColor = color
|
||||||
|
instanceLabel.textColor = color
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
//
|
||||||
|
// AccountSwitchingContainerViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/11/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class AccountSwitchingContainerViewController: UIViewController {
|
||||||
|
|
||||||
|
private(set) var root: TuskerRootViewController
|
||||||
|
|
||||||
|
init(root: TuskerRootViewController) {
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
embedChild(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRoot(_ newRoot: TuskerRootViewController, animating direction: AnimationDirection) {
|
||||||
|
let oldRoot = self.root
|
||||||
|
if direction == .none {
|
||||||
|
oldRoot.removeViewAndController()
|
||||||
|
}
|
||||||
|
self.root = newRoot
|
||||||
|
embedChild(newRoot)
|
||||||
|
|
||||||
|
if direction != .none {
|
||||||
|
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
||||||
|
newRoot.view.alpha = 0
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseInOut) {
|
||||||
|
newRoot.view.alpha = 1
|
||||||
|
oldRoot.view.alpha = 0
|
||||||
|
} completion: { (_) in
|
||||||
|
oldRoot.removeViewAndController()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let sign: CGFloat = direction == .downwards ? -1 : 1
|
||||||
|
let newInitialOffset = sign * view.bounds.height
|
||||||
|
|
||||||
|
newRoot.view.transform = CGAffineTransform(translationX: 0, y: newInitialOffset)
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
|
||||||
|
newRoot.view.transform = .identity
|
||||||
|
oldRoot.view.transform = CGAffineTransform(translationX: 0, y: -newInitialOffset)
|
||||||
|
} completion: { (_) in
|
||||||
|
oldRoot.removeViewAndController()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AccountSwitchingContainerViewController {
|
||||||
|
enum AnimationDirection {
|
||||||
|
case none, downwards, upwards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
|
func presentCompose() {
|
||||||
|
root.presentCompose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func select(tab: MainTabBarViewController.Tab) {
|
||||||
|
root.select(tab: tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||||
|
root.getTabController(tab: tab)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var composePlaceholder: UIViewController!
|
private var composePlaceholder: UIViewController!
|
||||||
|
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||||
|
|
||||||
var selectedTab: Tab {
|
var selectedTab: Tab {
|
||||||
return Tab(rawValue: selectedIndex)!
|
return Tab(rawValue: selectedIndex)!
|
||||||
|
@ -53,6 +54,26 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
embedInNavigationController(Tab.explore.createViewController(mastodonController)),
|
embedInNavigationController(Tab.explore.createViewController(mastodonController)),
|
||||||
embedInNavigationController(Tab.myProfile.createViewController(mastodonController)),
|
embedInNavigationController(Tab.myProfile.createViewController(mastodonController)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
fastAccountSwitcher = FastAccountSwitcherViewController()
|
||||||
|
fastAccountSwitcher.delegate = self
|
||||||
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
||||||
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
||||||
|
tapRecognizer.cancelsTouchesInView = false
|
||||||
|
tabBar.addGestureRecognizer(tapRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
fastAccountSwitcher.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
func embedInNavigationController(_ vc: UIViewController) -> UINavigationController {
|
func embedInNavigationController(_ vc: UIViewController) -> UINavigationController {
|
||||||
|
@ -120,6 +141,20 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
|
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
|
||||||
|
// sanity check that there is 1 button per VC
|
||||||
|
guard tabBarButtons.count == viewControllers!.count,
|
||||||
|
let myProfileButton = tabBarButtons.last else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view)
|
||||||
|
return myProfileButton.bounds.contains(locationInButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: TuskerRootViewController {
|
extension MainTabBarViewController: TuskerRootViewController {
|
||||||
func presentCompose() {
|
func presentCompose() {
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||||
|
|
|
@ -69,7 +69,7 @@ class PreferencesNavigationController: UINavigationController {
|
||||||
let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo
|
let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo
|
||||||
isSwitchingAccounts = true
|
isSwitchingAccounts = true
|
||||||
dismiss(animated: true) { // dismiss preferences
|
dismiss(animated: true) { // dismiss preferences
|
||||||
sceneDelegate.activateAccount(account)
|
sceneDelegate.activateAccount(account, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ extension PreferencesNavigationController: OnboardingViewControllerDelegate {
|
||||||
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
|
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
|
||||||
self.dismiss(animated: true) { // dismiss instance selector
|
self.dismiss(animated: true) { // dismiss instance selector
|
||||||
self.dismiss(animated: true) { // dismiss preferences
|
self.dismiss(animated: true) { // dismiss preferences
|
||||||
sceneDelegate.activateAccount(account)
|
sceneDelegate.activateAccount(account, animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,10 +137,10 @@ class ContentTextView: LinkTextView {
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
case "em", "i":
|
case "em", "i":
|
||||||
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
|
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
|
||||||
attributed.addAttribute(.font, value: currentFont.addingTraits(.traitItalic)!, range: attributed.fullRange)
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
|
||||||
case "strong", "b":
|
case "strong", "b":
|
||||||
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
|
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
|
||||||
attributed.addAttribute(.font, value: currentFont.addingTraits(.traitBold)!, range: attributed.fullRange)
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange)
|
||||||
case "del":
|
case "del":
|
||||||
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
||||||
case "code":
|
case "code":
|
||||||
|
|
Loading…
Reference in New Issue