New post view, follow unfollow, Nuke, better CompactPostView, fixes and improvements
This commit is contained in:
parent
17463147c7
commit
a2ee1a092b
|
@ -10,4 +10,5 @@ Threaded is a 100% free, made in SwiftUI, [#OpenSource](https://github.com/luma
|
||||||
|
|
||||||
- [Lumaa](https://lumaa.fr/)
|
- [Lumaa](https://lumaa.fr/)
|
||||||
- [IceCubesApp](https://github.com/dimillian/IceCubesApp) by [@dimillian](https://github.com/dimillian)
|
- [IceCubesApp](https://github.com/dimillian/IceCubesApp) by [@dimillian](https://github.com/dimillian)
|
||||||
- [SwiftSoup](https://github.com/scinfu/SwiftSoup)
|
- [SwiftSoup](https://github.com/scinfu/SwiftSoup)
|
||||||
|
- [Nuke](https://github.com/kean/Nuke)
|
||||||
|
|
|
@ -7,6 +7,14 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
B93B676D2B42C94F000892E9 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = B93B676C2B42C94F000892E9 /* Nuke */; };
|
||||||
|
B93B676F2B42C94F000892E9 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = B93B676E2B42C94F000892E9 /* NukeExtensions */; };
|
||||||
|
B93B67712B42C94F000892E9 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = B93B67702B42C94F000892E9 /* NukeUI */; };
|
||||||
|
B93B67732B42C94F000892E9 /* NukeVideo in Frameworks */ = {isa = PBXBuildFile; productRef = B93B67722B42C94F000892E9 /* NukeVideo */; };
|
||||||
|
B93B67762B42E8AB000892E9 /* EmojiText in Frameworks */ = {isa = PBXBuildFile; productRef = B93B67752B42E8AB000892E9 /* EmojiText */; };
|
||||||
|
B93B67782B42E8F0000892E9 /* TextEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B67772B42E8F0000892E9 /* TextEmoji.swift */; };
|
||||||
|
B93B677A2B42EC51000892E9 /* MetaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B67792B42EC51000892E9 /* MetaPicker.swift */; };
|
||||||
|
B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B677B2B433A6E000892E9 /* PostingView.swift */; };
|
||||||
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE232B3DD8400044756D /* HapticManager.swift */; };
|
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE232B3DD8400044756D /* HapticManager.swift */; };
|
||||||
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE252B3DE5A10044756D /* AccountView.swift */; };
|
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE252B3DE5A10044756D /* AccountView.swift */; };
|
||||||
B97BCE282B3ED2A80044756D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = B97BCE272B3ED2A80044756D /* .gitignore */; };
|
B97BCE282B3ED2A80044756D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = B97BCE272B3ED2A80044756D /* .gitignore */; };
|
||||||
|
@ -17,6 +25,7 @@
|
||||||
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */; };
|
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */; };
|
||||||
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */; };
|
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */; };
|
||||||
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C172B2F36F500D9F3C1 /* AccountsList.swift */; };
|
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C172B2F36F500D9F3C1 /* AccountsList.swift */; };
|
||||||
|
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */; };
|
||||||
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */; };
|
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */; };
|
||||||
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */; };
|
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */; };
|
||||||
B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945C2B2DEECE00D81C07 /* ContentView.swift */; };
|
B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945C2B2DEECE00D81C07 /* ContentView.swift */; };
|
||||||
|
@ -32,7 +41,6 @@
|
||||||
B9FB94842B2E20AF00D81C07 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = B9FB94832B2E20AF00D81C07 /* SwiftSoup */; };
|
B9FB94842B2E20AF00D81C07 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = B9FB94832B2E20AF00D81C07 /* SwiftSoup */; };
|
||||||
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94852B2E211200D81C07 /* Account+Elms.swift */; };
|
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94852B2E211200D81C07 /* Account+Elms.swift */; };
|
||||||
B9FB94882B2E223E00D81C07 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94872B2E223E00D81C07 /* Emoji.swift */; };
|
B9FB94882B2E223E00D81C07 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94872B2E223E00D81C07 /* Emoji.swift */; };
|
||||||
B9FB948A2B2E227000D81C07 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94892B2E227000D81C07 /* ProfileView.swift */; };
|
|
||||||
B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB948B2B2E232300D81C07 /* OnlineImage.swift */; };
|
B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB948B2B2E232300D81C07 /* OnlineImage.swift */; };
|
||||||
B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB948D2B2E28E800D81C07 /* ShareableImage.swift */; };
|
B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB948D2B2E28E800D81C07 /* ShareableImage.swift */; };
|
||||||
B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B9FB948F2B2E2B0E00D81C07 /* Localizable.xcstrings */; };
|
B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B9FB948F2B2E2B0E00D81C07 /* Localizable.xcstrings */; };
|
||||||
|
@ -75,6 +83,9 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
B93B67772B42E8F0000892E9 /* TextEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEmoji.swift; sourceTree = "<group>"; };
|
||||||
|
B93B67792B42EC51000892E9 /* MetaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaPicker.swift; sourceTree = "<group>"; };
|
||||||
|
B93B677B2B433A6E000892E9 /* PostingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingView.swift; sourceTree = "<group>"; };
|
||||||
B97BCE232B3DD8400044756D /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
|
B97BCE232B3DD8400044756D /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
|
||||||
B97BCE252B3DE5A10044756D /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
|
B97BCE252B3DE5A10044756D /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
|
||||||
B97BCE272B3ED2A80044756D /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
|
B97BCE272B3ED2A80044756D /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
|
||||||
|
@ -85,6 +96,7 @@
|
||||||
B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTimeline.swift; sourceTree = "<group>"; };
|
B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTimeline.swift; sourceTree = "<group>"; };
|
||||||
B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilter.swift; sourceTree = "<group>"; };
|
B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilter.swift; sourceTree = "<group>"; };
|
||||||
B9842C172B2F36F500D9F3C1 /* AccountsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsList.swift; sourceTree = "<group>"; };
|
B9842C172B2F36F500D9F3C1 /* AccountsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsList.swift; sourceTree = "<group>"; };
|
||||||
|
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTextEditor.swift; sourceTree = "<group>"; };
|
||||||
B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||||
B9CC45B92B40AA1E001E4FA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
B9CC45B92B40AA1E001E4FA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||||
B9FB94572B2DEECE00D81C07 /* Threaded.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Threaded.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
B9FB94572B2DEECE00D81C07 /* Threaded.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Threaded.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -101,7 +113,6 @@
|
||||||
B9FB94802B2E1FEF00D81C07 /* HTMLString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLString.swift; sourceTree = "<group>"; };
|
B9FB94802B2E1FEF00D81C07 /* HTMLString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLString.swift; sourceTree = "<group>"; };
|
||||||
B9FB94852B2E211200D81C07 /* Account+Elms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Elms.swift"; sourceTree = "<group>"; };
|
B9FB94852B2E211200D81C07 /* Account+Elms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Elms.swift"; sourceTree = "<group>"; };
|
||||||
B9FB94872B2E223E00D81C07 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
B9FB94872B2E223E00D81C07 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
||||||
B9FB94892B2E227000D81C07 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
|
||||||
B9FB948B2B2E232300D81C07 /* OnlineImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineImage.swift; sourceTree = "<group>"; };
|
B9FB948B2B2E232300D81C07 /* OnlineImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineImage.swift; sourceTree = "<group>"; };
|
||||||
B9FB948D2B2E28E800D81C07 /* ShareableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareableImage.swift; sourceTree = "<group>"; };
|
B9FB948D2B2E28E800D81C07 /* ShareableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareableImage.swift; sourceTree = "<group>"; };
|
||||||
B9FB948F2B2E2B0E00D81C07 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
B9FB948F2B2E2B0E00D81C07 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
|
@ -126,7 +137,12 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
B93B67762B42E8AB000892E9 /* EmojiText in Frameworks */,
|
||||||
B9FB94842B2E20AF00D81C07 /* SwiftSoup in Frameworks */,
|
B9FB94842B2E20AF00D81C07 /* SwiftSoup in Frameworks */,
|
||||||
|
B93B676D2B42C94F000892E9 /* Nuke in Frameworks */,
|
||||||
|
B93B67732B42C94F000892E9 /* NukeVideo in Frameworks */,
|
||||||
|
B93B676F2B42C94F000892E9 /* NukeExtensions in Frameworks */,
|
||||||
|
B93B67712B42C94F000892E9 /* NukeUI in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -212,11 +228,11 @@
|
||||||
children = (
|
children = (
|
||||||
B9FB94952B2EDAB600D81C07 /* Settings */,
|
B9FB94952B2EDAB600D81C07 /* Settings */,
|
||||||
B9FB94712B2DF49700D81C07 /* ConnectView.swift */,
|
B9FB94712B2DF49700D81C07 /* ConnectView.swift */,
|
||||||
B9FB94892B2E227000D81C07 /* ProfileView.swift */,
|
|
||||||
B9FB94982B2EEB9400D81C07 /* AddInstanceView.swift */,
|
B9FB94982B2EEB9400D81C07 /* AddInstanceView.swift */,
|
||||||
B9FB945C2B2DEECE00D81C07 /* ContentView.swift */,
|
B9FB945C2B2DEECE00D81C07 /* ContentView.swift */,
|
||||||
B9842C112B2F2A5800D9F3C1 /* TimelineView.swift */,
|
B9842C112B2F2A5800D9F3C1 /* TimelineView.swift */,
|
||||||
B97BCE252B3DE5A10044756D /* AccountView.swift */,
|
B97BCE252B3DE5A10044756D /* AccountView.swift */,
|
||||||
|
B93B677B2B433A6E000892E9 /* PostingView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -226,8 +242,11 @@
|
||||||
children = (
|
children = (
|
||||||
B9FB94752B2E023D00D81C07 /* TabsView.swift */,
|
B9FB94752B2E023D00D81C07 /* TabsView.swift */,
|
||||||
B9FB94732B2DF6A100D81C07 /* ButtonStyles.swift */,
|
B9FB94732B2DF6A100D81C07 /* ButtonStyles.swift */,
|
||||||
|
B93B67792B42EC51000892E9 /* MetaPicker.swift */,
|
||||||
B9FB948B2B2E232300D81C07 /* OnlineImage.swift */,
|
B9FB948B2B2E232300D81C07 /* OnlineImage.swift */,
|
||||||
B9842C0D2B2F21B700D9F3C1 /* CompactPostView.swift */,
|
B9842C0D2B2F21B700D9F3C1 /* CompactPostView.swift */,
|
||||||
|
B93B67772B42E8F0000892E9 /* TextEmoji.swift */,
|
||||||
|
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -291,6 +310,11 @@
|
||||||
name = Threaded;
|
name = Threaded;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
B9FB94832B2E20AF00D81C07 /* SwiftSoup */,
|
B9FB94832B2E20AF00D81C07 /* SwiftSoup */,
|
||||||
|
B93B676C2B42C94F000892E9 /* Nuke */,
|
||||||
|
B93B676E2B42C94F000892E9 /* NukeExtensions */,
|
||||||
|
B93B67702B42C94F000892E9 /* NukeUI */,
|
||||||
|
B93B67722B42C94F000892E9 /* NukeVideo */,
|
||||||
|
B93B67752B42E8AB000892E9 /* EmojiText */,
|
||||||
);
|
);
|
||||||
productName = Threaded;
|
productName = Threaded;
|
||||||
productReference = B9FB94572B2DEECE00D81C07 /* Threaded.app */;
|
productReference = B9FB94572B2DEECE00D81C07 /* Threaded.app */;
|
||||||
|
@ -342,6 +366,8 @@
|
||||||
mainGroup = B9FB944E2B2DEECE00D81C07;
|
mainGroup = B9FB944E2B2DEECE00D81C07;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||||
|
B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||||
|
B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */,
|
||||||
);
|
);
|
||||||
productRefGroup = B9FB94582B2DEECE00D81C07 /* Products */;
|
productRefGroup = B9FB94582B2DEECE00D81C07 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -384,18 +410,19 @@
|
||||||
B9FB94922B2E35D000D81C07 /* SettingsView.swift in Sources */,
|
B9FB94922B2E35D000D81C07 /* SettingsView.swift in Sources */,
|
||||||
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */,
|
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */,
|
||||||
B9FB94882B2E223E00D81C07 /* Emoji.swift in Sources */,
|
B9FB94882B2E223E00D81C07 /* Emoji.swift in Sources */,
|
||||||
|
B93B67782B42E8F0000892E9 /* TextEmoji.swift in Sources */,
|
||||||
B9FB94762B2E023D00D81C07 /* TabsView.swift in Sources */,
|
B9FB94762B2E023D00D81C07 /* TabsView.swift in Sources */,
|
||||||
B9FB947D2B2E19E300D81C07 /* AccountManager.swift in Sources */,
|
B9FB947D2B2E19E300D81C07 /* AccountManager.swift in Sources */,
|
||||||
B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */,
|
B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */,
|
||||||
B9842C0E2B2F21B700D9F3C1 /* CompactPostView.swift in Sources */,
|
B9842C0E2B2F21B700D9F3C1 /* CompactPostView.swift in Sources */,
|
||||||
B9FB94992B2EEB9400D81C07 /* AddInstanceView.swift in Sources */,
|
B9FB94992B2EEB9400D81C07 /* AddInstanceView.swift in Sources */,
|
||||||
B9FB94972B2EDABF00D81C07 /* PrivacyView.swift in Sources */,
|
B9FB94972B2EDABF00D81C07 /* PrivacyView.swift in Sources */,
|
||||||
B9FB948A2B2E227000D81C07 /* ProfileView.swift in Sources */,
|
|
||||||
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */,
|
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */,
|
||||||
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */,
|
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */,
|
||||||
B9FB949B2B2EF09A00D81C07 /* Client.swift in Sources */,
|
B9FB949B2B2EF09A00D81C07 /* Client.swift in Sources */,
|
||||||
B9FB949D2B2EF0D600D81C07 /* Instance.swift in Sources */,
|
B9FB949D2B2EF0D600D81C07 /* Instance.swift in Sources */,
|
||||||
B9842C102B2F228C00D9F3C1 /* Status.swift in Sources */,
|
B9842C102B2F228C00D9F3C1 /* Status.swift in Sources */,
|
||||||
|
B93B677A2B42EC51000892E9 /* MetaPicker.swift in Sources */,
|
||||||
B9FB94722B2DF49700D81C07 /* ConnectView.swift in Sources */,
|
B9FB94722B2DF49700D81C07 /* ConnectView.swift in Sources */,
|
||||||
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */,
|
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */,
|
||||||
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */,
|
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */,
|
||||||
|
@ -406,8 +433,10 @@
|
||||||
B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */,
|
B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */,
|
||||||
B9FB94742B2DF6A100D81C07 /* ButtonStyles.swift in Sources */,
|
B9FB94742B2DF6A100D81C07 /* ButtonStyles.swift in Sources */,
|
||||||
B9FB94702B2DF3CD00D81C07 /* Navigator.swift in Sources */,
|
B9FB94702B2DF3CD00D81C07 /* Navigator.swift in Sources */,
|
||||||
|
B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */,
|
||||||
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */,
|
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */,
|
||||||
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */,
|
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */,
|
||||||
|
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */,
|
||||||
B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */,
|
B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */,
|
||||||
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */,
|
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */,
|
||||||
B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */,
|
B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */,
|
||||||
|
@ -721,6 +750,22 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/kean/Nuke";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 12.2.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/divadretlaw/EmojiText";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMinorVersion;
|
||||||
|
minimumVersion = 3.3.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/scinfu/SwiftSoup";
|
repositoryURL = "https://github.com/scinfu/SwiftSoup";
|
||||||
|
@ -732,6 +777,31 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
B93B676C2B42C94F000892E9 /* Nuke */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||||
|
productName = Nuke;
|
||||||
|
};
|
||||||
|
B93B676E2B42C94F000892E9 /* NukeExtensions */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||||
|
productName = NukeExtensions;
|
||||||
|
};
|
||||||
|
B93B67702B42C94F000892E9 /* NukeUI */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||||
|
productName = NukeUI;
|
||||||
|
};
|
||||||
|
B93B67722B42C94F000892E9 /* NukeVideo */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||||
|
productName = NukeVideo;
|
||||||
|
};
|
||||||
|
B93B67752B42E8AB000892E9 /* EmojiText */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */;
|
||||||
|
productName = EmojiText;
|
||||||
|
};
|
||||||
B9FB94832B2E20AF00D81C07 /* SwiftSoup */ = {
|
B9FB94832B2E20AF00D81C07 /* SwiftSoup */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
package = B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
{
|
{
|
||||||
"pins" : [
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "emojitext",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/divadretlaw/EmojiText",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "e24d8c0def5c77c551fee34fca09c38baa0860e6",
|
||||||
|
"version" : "3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "nuke",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kean/Nuke",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "1694798e876113d44f6ec6ead965d7286695981d",
|
||||||
|
"version" : "12.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swiftsoup",
|
"identity" : "swiftsoup",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
@ -4,10 +4,12 @@ import SwiftUI
|
||||||
|
|
||||||
struct LargeButton: ButtonStyle {
|
struct LargeButton: ButtonStyle {
|
||||||
var filled: Bool = false
|
var filled: Bool = false
|
||||||
|
var height: CGFloat? = nil
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
.padding()
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, height)
|
||||||
.background {
|
.background {
|
||||||
if filled {
|
if filled {
|
||||||
Color(uiColor: UIColor.label)
|
Color(uiColor: UIColor.label)
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CompactPostView: View {
|
struct CompactPostView: View {
|
||||||
@Environment(Client.self) private var client: Client
|
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||||
var status: Status
|
var status: Status
|
||||||
var navigator: Navigator
|
@ObservedObject var navigator: Navigator
|
||||||
var pinned: Bool = false
|
var pinned: Bool = false
|
||||||
|
|
||||||
@State private var initialLike: Bool = false
|
@State private var initialLike: Bool = false
|
||||||
|
@ -14,22 +14,18 @@ struct CompactPostView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if status.reblog != nil {
|
VStack(alignment: .leading) {
|
||||||
VStack(alignment: .leading) {
|
if pinned {
|
||||||
|
pinnedNotice
|
||||||
|
.padding(.leading, 35)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.reblog != nil {
|
||||||
repostNotice
|
repostNotice
|
||||||
.padding(.leading, 30)
|
.padding(.leading, 30)
|
||||||
|
|
||||||
statusRepost
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
if pinned {
|
|
||||||
pinnedNotice
|
|
||||||
.padding(.leading, 15)
|
|
||||||
}
|
|
||||||
|
|
||||||
statusPost
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusPost(status.reblog ?? status)
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
@ -45,58 +41,67 @@ struct CompactPostView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func likePost() async throws {
|
func likePost() async throws {
|
||||||
guard client.isAuth else { fatalError("Client is not authenticated") }
|
if let client = accountManager.getClient() {
|
||||||
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
guard client.isAuth else { fatalError("Client is not authenticated") }
|
||||||
let endpoint = !isLiked ? Statuses.favorite(id: statusId) : Statuses.unfavorite(id: statusId)
|
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
||||||
|
let endpoint = !isLiked ? Statuses.favorite(id: statusId) : Statuses.unfavorite(id: statusId)
|
||||||
isLiked = !isLiked
|
|
||||||
let newStatus: Status = try await client.post(endpoint: endpoint)
|
isLiked = !isLiked
|
||||||
if isLiked != newStatus.favourited {
|
let newStatus: Status = try await client.post(endpoint: endpoint)
|
||||||
isLiked = newStatus.favourited ?? !isLiked
|
if isLiked != newStatus.favourited {
|
||||||
|
isLiked = newStatus.favourited ?? !isLiked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func repostPost() async throws {
|
func repostPost() async throws {
|
||||||
guard client.isAuth else { fatalError("Client is not authenticated") }
|
if let client = accountManager.getClient() {
|
||||||
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
guard client.isAuth else { fatalError("Client is not authenticated") }
|
||||||
let endpoint = !isReposted ? Statuses.reblog(id: statusId) : Statuses.unreblog(id: statusId)
|
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
||||||
|
let endpoint = !isReposted ? Statuses.reblog(id: statusId) : Statuses.unreblog(id: statusId)
|
||||||
isReposted = !isReposted
|
|
||||||
let newStatus: Status = try await client.post(endpoint: endpoint)
|
isReposted = !isReposted
|
||||||
if isReposted != newStatus.reblogged {
|
let newStatus: Status = try await client.post(endpoint: endpoint)
|
||||||
isReposted = newStatus.reblogged ?? !isReposted
|
if isReposted != newStatus.reblogged {
|
||||||
|
isReposted = newStatus.reblogged ?? !isReposted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusPost: some View {
|
@ViewBuilder
|
||||||
|
func statusPost(_ status: AnyStatus) -> some View {
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
// MARK: Profile picture
|
// MARK: Profile picture
|
||||||
// if status.repliesCount > 0 {
|
if status.repliesCount > 0 {
|
||||||
// VStack {
|
VStack {
|
||||||
// profilePicture
|
profilePicture
|
||||||
// .onTapGesture {
|
.onTapGesture {
|
||||||
// navigator.navigate(to: .account(acc: status.account))
|
navigator.navigate(to: .account(acc: status.account))
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// Rectangle()
|
Spacer()
|
||||||
// .fill(Color.gray.opacity(0.3))
|
|
||||||
// .frame(width: 2.5)
|
Rectangle()
|
||||||
// .clipShape(.capsule)
|
.fill(Color.gray.opacity(0.3))
|
||||||
// .padding([.vertical], 5)
|
.frame(width: 2.5)
|
||||||
//
|
.clipShape(.capsule)
|
||||||
// Image(systemName: "person.crop.circle")
|
.padding([.vertical], 5)
|
||||||
// .resizable()
|
|
||||||
// .frame(width: 15, height: 15)
|
Spacer()
|
||||||
// .symbolRenderingMode(.monochrome)
|
|
||||||
// .foregroundStyle(Color.gray.opacity(0.3))
|
Image(systemName: "person.crop.circle")
|
||||||
// .padding(.bottom, 2.5)
|
.resizable()
|
||||||
// }
|
.frame(width: 15, height: 15)
|
||||||
// } else {
|
.symbolRenderingMode(.monochrome)
|
||||||
|
.foregroundStyle(Color.gray.opacity(0.3))
|
||||||
|
.padding(.bottom, 2.5)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
profilePicture
|
profilePicture
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
navigator.navigate(to: .account(acc: status.account))
|
navigator.navigate(to: .account(acc: status.account))
|
||||||
}
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
// MARK: Status main content
|
// MARK: Status main content
|
||||||
|
@ -108,10 +113,11 @@ struct CompactPostView: View {
|
||||||
navigator.navigate(to: .account(acc: status.account))
|
navigator.navigate(to: .account(acc: status.account))
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(status.content.asRawText)
|
TextEmoji(status.content, emojis: status.emojis, language: status.language)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.frame(width: 300, alignment: .topLeading)
|
.frame(width: 300, alignment: .topLeading)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.font(.callout)
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: Action buttons
|
//MARK: Action buttons
|
||||||
|
@ -127,7 +133,7 @@ struct CompactPostView: View {
|
||||||
}
|
}
|
||||||
actionButton("bubble.right") {
|
actionButton("bubble.right") {
|
||||||
print("reply")
|
print("reply")
|
||||||
navigator.presentedSheet = .post
|
navigator.presentedSheet = .post()
|
||||||
}
|
}
|
||||||
asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") {
|
asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") {
|
||||||
do {
|
do {
|
||||||
|
@ -152,90 +158,6 @@ struct CompactPostView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusRepost: some View {
|
|
||||||
HStack(alignment: .top, spacing: 0) {
|
|
||||||
// MARK: Profile picture
|
|
||||||
// if status.reblog!.repliesCount > 0 {
|
|
||||||
// VStack {
|
|
||||||
// profilePicture
|
|
||||||
// .onTapGesture {
|
|
||||||
// navigator.navigate(to: .account(acc: status.reblog!.account))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Rectangle()
|
|
||||||
// .fill(Color.gray.opacity(0.3))
|
|
||||||
// .frame(width: 2.5)
|
|
||||||
// .clipShape(.capsule)
|
|
||||||
// .padding([.vertical], 5)
|
|
||||||
//
|
|
||||||
// Image(systemName: "person.crop.circle")
|
|
||||||
// .resizable()
|
|
||||||
// .frame(width: 15, height: 15)
|
|
||||||
// .symbolRenderingMode(.monochrome)
|
|
||||||
// .foregroundStyle(Color.gray.opacity(0.3))
|
|
||||||
// .padding(.bottom, 2.5)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
profilePicture
|
|
||||||
.onTapGesture {
|
|
||||||
navigator.navigate(to: .account(acc: status.reblog!.account))
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
// MARK: Status main content
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
Text(status.reblog!.account.username)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.bold()
|
|
||||||
.onTapGesture {
|
|
||||||
navigator.navigate(to: .account(acc: status.reblog!.account))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(status.reblog!.content.asRawText)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.frame(width: 300, alignment: .topLeading)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
//MARK: Action buttons
|
|
||||||
HStack(spacing: 13) {
|
|
||||||
asyncActionButton(isLiked ? "heart.fill" : "heart") {
|
|
||||||
do {
|
|
||||||
try await likePost()
|
|
||||||
HapticManager.playHaptics(haptics: Haptic.tap)
|
|
||||||
} catch {
|
|
||||||
HapticManager.playHaptics(haptics: Haptic.error)
|
|
||||||
print("Error: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
actionButton("bubble.right") {
|
|
||||||
print("reply")
|
|
||||||
navigator.presentedSheet = .post
|
|
||||||
}
|
|
||||||
asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") {
|
|
||||||
do {
|
|
||||||
try await repostPost()
|
|
||||||
HapticManager.playHaptics(haptics: Haptic.tap)
|
|
||||||
} catch {
|
|
||||||
HapticManager.playHaptics(haptics: Haptic.error)
|
|
||||||
print("Error: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ShareLink(item: URL(string: status.reblog!.url ?? "https://joinmastodon.org/")!) {
|
|
||||||
Image(systemName: "square.and.arrow.up")
|
|
||||||
.font(.title2)
|
|
||||||
}
|
|
||||||
.tint(Color(uiColor: UIColor.label))
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
|
|
||||||
// MARK: Status stats
|
|
||||||
stats.padding(.top, 5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pinnedNotice: some View {
|
var pinnedNotice: some View {
|
||||||
HStack (alignment:.center, spacing: 5) {
|
HStack (alignment:.center, spacing: 5) {
|
||||||
Image(systemName: "pin.fill")
|
Image(systemName: "pin.fill")
|
||||||
|
@ -264,12 +186,12 @@ struct CompactPostView: View {
|
||||||
|
|
||||||
var profilePicture: some View {
|
var profilePicture: some View {
|
||||||
if status.reblog != nil {
|
if status.reblog != nil {
|
||||||
OnlineImage(url: status.reblog!.account.avatar)
|
OnlineImage(url: status.reblog!.account.avatar, size: 50, useNuke: true)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.clipShape(.circle)
|
.clipShape(.circle)
|
||||||
} else {
|
} else {
|
||||||
OnlineImage(url: status.account.avatar)
|
OnlineImage(url: status.account.avatar, size: 50, useNuke: true)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.clipShape(.circle)
|
.clipShape(.circle)
|
||||||
|
@ -292,11 +214,12 @@ struct CompactPostView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.favouritesCount > 0 || isLiked {
|
if status.favouritesCount > 0 || isLiked {
|
||||||
let addedLike: Int = isLiked ? 1 : 0
|
let i: Int = status.favouritesCount
|
||||||
Text("status.favourites-\(initialLike ? (status.favouritesCount - addedLike) : (status.favouritesCount + addedLike))")
|
let favsCount: Int = i - (initialLike ? 1 : 0) + (isLiked ? 1 : 0)
|
||||||
|
Text("status.favourites-\(favsCount)")
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
.contentTransition(.numericText(value: Double(status.favouritesCount + addedLike)))
|
.contentTransition(.numericText(value: Double(favsCount)))
|
||||||
.transaction { t in
|
.transaction { t in
|
||||||
t.animation = .default
|
t.animation = .default
|
||||||
}
|
}
|
||||||
|
@ -316,11 +239,11 @@ struct CompactPostView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.reblog!.favouritesCount > 0 || isLiked {
|
if status.reblog!.favouritesCount > 0 || isLiked {
|
||||||
let addedLike: Int = isLiked ? 1 : 0
|
let favsCount: Int = (status.favouritesCount - (initialLike ? 1 : 0)) + (isLiked ? 1 : 0)
|
||||||
Text("status.favourites-\(initialLike ? (status.favouritesCount - addedLike) : (status.favouritesCount + addedLike))")
|
Text("status.favourites-\(favsCount)")
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
.contentTransition(.numericText(value: Double(status.reblog!.favouritesCount + addedLike)))
|
.contentTransition(.numericText(value: Double(favsCount)))
|
||||||
.transaction { t in
|
.transaction { t in
|
||||||
t.animation = .default
|
t.animation = .default
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,226 @@
|
||||||
|
//Made by Lumaa
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// A SwiftUI TextView implementation that supports both scrolling and auto-sizing layouts
|
||||||
|
public struct DynamicTextEditor: View {
|
||||||
|
@Environment(\.layoutDirection) private var layoutDirection
|
||||||
|
|
||||||
|
@Binding private var text: NSMutableAttributedString
|
||||||
|
@Binding private var isEmpty: Bool
|
||||||
|
|
||||||
|
@State private var calculatedHeight: CGFloat = 44
|
||||||
|
|
||||||
|
private var getTextView: ((UITextView) -> Void)?
|
||||||
|
|
||||||
|
var placeholderView: AnyView?
|
||||||
|
var placeholderText: String?
|
||||||
|
var keyboard: UIKeyboardType = .default
|
||||||
|
|
||||||
|
/// Makes a new TextView that supports `NSAttributedString`
|
||||||
|
/// - Parameters:
|
||||||
|
/// - text: A binding to the attributed text
|
||||||
|
public init(_ text: Binding<NSMutableAttributedString>,
|
||||||
|
getTextView: ((UITextView) -> Void)? = nil)
|
||||||
|
{
|
||||||
|
_text = text
|
||||||
|
_isEmpty = Binding(
|
||||||
|
get: { text.wrappedValue.string.isEmpty },
|
||||||
|
set: { _ in }
|
||||||
|
)
|
||||||
|
|
||||||
|
self.getTextView = getTextView
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Representable(
|
||||||
|
text: $text,
|
||||||
|
calculatedHeight: $calculatedHeight,
|
||||||
|
keyboard: keyboard,
|
||||||
|
getTextView: getTextView
|
||||||
|
)
|
||||||
|
.frame(
|
||||||
|
minHeight: calculatedHeight,
|
||||||
|
maxHeight: calculatedHeight
|
||||||
|
)
|
||||||
|
.accessibilityValue($text.wrappedValue.string.isEmpty ? (placeholderText ?? "") : $text.wrappedValue.string)
|
||||||
|
.background(
|
||||||
|
placeholderView?
|
||||||
|
.foregroundColor(Color(.placeholderText))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.font(.callout)
|
||||||
|
.padding(.horizontal, 0)
|
||||||
|
.padding(.vertical, 0)
|
||||||
|
.opacity(isEmpty ? 1 : 0)
|
||||||
|
.accessibilityHidden(true),
|
||||||
|
alignment: .topLeading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UIKitTextView: UITextView {
|
||||||
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
|
(super.keyCommands ?? []) + [
|
||||||
|
UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escape(_:))),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func escape(_: Any) {
|
||||||
|
resignFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension DynamicTextEditor {
|
||||||
|
/// Specify a placeholder text
|
||||||
|
/// - Parameter placeholder: The placeholder text
|
||||||
|
func placeholder(_ placeholder: String) -> DynamicTextEditor {
|
||||||
|
self.placeholder(placeholder) { $0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify a placeholder with the specified configuration
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// TextView($text)
|
||||||
|
/// .placeholder("placeholder") { view in
|
||||||
|
/// view.foregroundColor(.red)
|
||||||
|
/// }
|
||||||
|
func placeholder(_ placeholder: String, _ configure: (Text) -> some View) -> DynamicTextEditor {
|
||||||
|
var view = self
|
||||||
|
let text = Text(placeholder)
|
||||||
|
view.placeholderView = AnyView(configure(text))
|
||||||
|
view.placeholderText = placeholder
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify a custom placeholder view
|
||||||
|
func placeholder(_ placeholder: some View) -> DynamicTextEditor {
|
||||||
|
var view = self
|
||||||
|
view.placeholderView = AnyView(placeholder)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func setKeyboardType(_ keyboardType: UIKeyboardType) -> DynamicTextEditor {
|
||||||
|
var view = self
|
||||||
|
view.keyboard = keyboardType
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DynamicTextEditor {
|
||||||
|
struct Representable: UIViewRepresentable {
|
||||||
|
@Binding var text: NSMutableAttributedString
|
||||||
|
@Binding var calculatedHeight: CGFloat
|
||||||
|
@Environment(\.sizeCategory) var sizeCategory
|
||||||
|
|
||||||
|
let keyboard: UIKeyboardType
|
||||||
|
var getTextView: ((UITextView) -> Void)?
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIKitTextView {
|
||||||
|
context.coordinator.textView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_: UIKitTextView, context: Context) {
|
||||||
|
context.coordinator.update(representable: self)
|
||||||
|
if !context.coordinator.didBecomeFirstResponder {
|
||||||
|
context.coordinator.textView.becomeFirstResponder()
|
||||||
|
context.coordinator.didBecomeFirstResponder = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(
|
||||||
|
text: $text,
|
||||||
|
calculatedHeight: $calculatedHeight,
|
||||||
|
sizeCategory: sizeCategory,
|
||||||
|
getTextView: getTextView
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DynamicTextEditor.Representable {
|
||||||
|
final class Coordinator: NSObject, UITextViewDelegate {
|
||||||
|
let textView: UIKitTextView
|
||||||
|
|
||||||
|
private var originalText: NSMutableAttributedString = .init()
|
||||||
|
private var text: Binding<NSMutableAttributedString>
|
||||||
|
private var sizeCategory: ContentSizeCategory
|
||||||
|
private var calculatedHeight: Binding<CGFloat>
|
||||||
|
|
||||||
|
var didBecomeFirstResponder = false
|
||||||
|
|
||||||
|
var getTextView: ((UITextView) -> Void)?
|
||||||
|
|
||||||
|
init(text: Binding<NSMutableAttributedString>,
|
||||||
|
calculatedHeight: Binding<CGFloat>,
|
||||||
|
sizeCategory: ContentSizeCategory,
|
||||||
|
getTextView: ((UITextView) -> Void)?)
|
||||||
|
{
|
||||||
|
textView = UIKitTextView()
|
||||||
|
textView.backgroundColor = .clear
|
||||||
|
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
textView.isScrollEnabled = false
|
||||||
|
textView.textContainer.lineFragmentPadding = 0
|
||||||
|
textView.textContainerInset = .zero
|
||||||
|
|
||||||
|
self.text = text
|
||||||
|
self.calculatedHeight = calculatedHeight
|
||||||
|
self.sizeCategory = sizeCategory
|
||||||
|
self.getTextView = getTextView
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
textView.delegate = self
|
||||||
|
|
||||||
|
textView.font = UIFont.preferredFont(forTextStyle: .callout)
|
||||||
|
textView.adjustsFontForContentSizeCategory = true
|
||||||
|
textView.autocapitalizationType = .sentences
|
||||||
|
textView.autocorrectionType = .yes
|
||||||
|
textView.isEditable = true
|
||||||
|
textView.isSelectable = true
|
||||||
|
textView.dataDetectorTypes = []
|
||||||
|
textView.allowsEditingTextAttributes = false
|
||||||
|
textView.returnKeyType = .default
|
||||||
|
textView.allowsEditingTextAttributes = true
|
||||||
|
|
||||||
|
self.getTextView?(textView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidBeginEditing(_: UITextView) {
|
||||||
|
originalText = text.wrappedValue
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.recalculateHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.text.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
|
||||||
|
self.recalculateHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textView(_: UITextView, shouldChangeTextIn _: NSRange, replacementText _: String) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DynamicTextEditor.Representable.Coordinator {
|
||||||
|
func update(representable: DynamicTextEditor.Representable) {
|
||||||
|
textView.keyboardType = representable.keyboard
|
||||||
|
recalculateHeight()
|
||||||
|
textView.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recalculateHeight() {
|
||||||
|
let newSize = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude))
|
||||||
|
guard calculatedHeight.wrappedValue != newSize.height else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.async { // call in next render cycle.
|
||||||
|
self.calculatedHeight.wrappedValue = newSize.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
//Made by Lumaa
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MetaPicker<Content : View>: View {
|
||||||
|
@Namespace private var metaPicker
|
||||||
|
@Namespace private var selectBar
|
||||||
|
|
||||||
|
var items: [String]
|
||||||
|
@Binding var selectedItem: String
|
||||||
|
|
||||||
|
@ViewBuilder let content: (_ item: String) -> Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
ForEach(items, id: \.self) { item in
|
||||||
|
GeometryReader { geo in
|
||||||
|
let size = geo.size
|
||||||
|
VStack {
|
||||||
|
content(item)
|
||||||
|
.tag(item)
|
||||||
|
.foregroundStyle(item == selectedItem ? Color.white : Color.gray.opacity(0.3))
|
||||||
|
|
||||||
|
if item == selectedItem {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.white)
|
||||||
|
.frame(width: size.width, height: 2)
|
||||||
|
.matchedGeometryEffect(id: selectBar, in: metaPicker)
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(width: size.width, height: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
withAnimation(.spring) {
|
||||||
|
selectedItem = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if items.last != item {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,72 @@
|
||||||
//Made by Lumaa
|
//Made by Lumaa
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Nuke
|
||||||
|
import NukeUI
|
||||||
|
|
||||||
struct OnlineImage: View {
|
struct OnlineImage: View {
|
||||||
var url: URL
|
var url: URL?
|
||||||
|
var size: CGFloat = 500
|
||||||
|
var priority: ImageRequest.Priority = .normal
|
||||||
|
var useNuke: Bool = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
AsyncImage(url: url) { element in
|
if useNuke {
|
||||||
element
|
LazyImage(url: url) { state in
|
||||||
.resizable()
|
if let image = state.image {
|
||||||
.scaledToFit()
|
image
|
||||||
.aspectRatio(1.0, contentMode: .fit)
|
.resizable()
|
||||||
} placeholder: {
|
.scaledToFit()
|
||||||
Rectangle()
|
.aspectRatio(1.0, contentMode: .fit)
|
||||||
.fill(Color.gray)
|
} else if state.error != nil {
|
||||||
.overlay {
|
ContentUnavailableView("error.loading-image", systemImage: "rectangle.slash")
|
||||||
ProgressView()
|
} else {
|
||||||
.progressViewStyle(.circular)
|
Rectangle()
|
||||||
|
.fill(Color.gray)
|
||||||
|
.overlay {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.priority(priority)
|
||||||
|
.processors([.resize(width: size)])
|
||||||
|
} else {
|
||||||
|
AsyncImage(url: url) { element in
|
||||||
|
element
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.aspectRatio(1.0, contentMode: .fit)
|
||||||
|
} placeholder: {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray)
|
||||||
|
.overlay {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new OnlineImage using Nuke or not, default priority is .normal
|
||||||
|
init(url: URL? = nil, size: CGFloat, useNuke: Bool) {
|
||||||
|
self.url = url
|
||||||
|
self.size = size
|
||||||
|
self.priority = .normal
|
||||||
|
self.useNuke = useNuke
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new OnlineImage using Nuke, using the selected priority
|
||||||
|
init(url: URL? = nil, size: CGFloat, priority: ImageRequest.Priority) {
|
||||||
|
self.url = url
|
||||||
|
self.size = size
|
||||||
|
self.priority = priority
|
||||||
|
self.useNuke = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the priority of the Nuke OnlineImage
|
||||||
|
mutating func setPriority(_ priority: ImageRequest.Priority) {
|
||||||
|
guard self.useNuke == true else { return }
|
||||||
|
self.priority = priority
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ struct TabsView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
navigator.presentedSheet = .post
|
navigator.presentedSheet = .post()
|
||||||
} label: {
|
} label: {
|
||||||
Tabs.post.image
|
Tabs.post.image
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,6 @@ struct TabsView: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(NoTapAnimationStyle())
|
.buttonStyle(NoTapAnimationStyle())
|
||||||
}
|
}
|
||||||
.withSheets(sheetDestination: $navigator.presentedSheet)
|
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
}
|
}
|
||||||
|
@ -127,7 +126,7 @@ enum Tabs {
|
||||||
extension Image {
|
extension Image {
|
||||||
func tabBarify(_ neutral: Bool = true) -> some View {
|
func tabBarify(_ neutral: Bool = true) -> some View {
|
||||||
self
|
self
|
||||||
.font(.title2)
|
.font(.title)
|
||||||
.opacity(neutral ? 0.3 : 1)
|
.opacity(neutral ? 0.3 : 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
//Made by Lumaa
|
||||||
|
|
||||||
|
import EmojiText
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct TextEmoji: View {
|
||||||
|
private let markdown: HTMLString
|
||||||
|
private let emojis: [any CustomEmoji]
|
||||||
|
private let language: String?
|
||||||
|
private let append: (() -> Text)?
|
||||||
|
// private let lineLimit: Int?
|
||||||
|
|
||||||
|
public init(_ markdown: HTMLString, emojis: [Emoji], language: String? = nil, append: (() -> Text)? = nil) {
|
||||||
|
self.markdown = markdown
|
||||||
|
self.emojis = emojis.map { RemoteEmoji(shortcode: $0.shortcode, url: $0.url) }
|
||||||
|
self.language = language
|
||||||
|
// self.lineLimit = lineLimit
|
||||||
|
self.append = append
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
if let append {
|
||||||
|
EmojiText(markdown: markdown.asMarkdown, emojis: emojis)
|
||||||
|
.append {
|
||||||
|
append()
|
||||||
|
}
|
||||||
|
// .lineLimit(lineLimit)
|
||||||
|
} else if emojis.isEmpty {
|
||||||
|
Text(markdown.asSafeMarkdownAttributedString)
|
||||||
|
// .lineLimit(lineLimit)
|
||||||
|
.environment(\.layoutDirection, isRTL() ? .rightToLeft : .leftToRight)
|
||||||
|
} else {
|
||||||
|
EmojiText(markdown: markdown.asMarkdown, emojis: emojis)
|
||||||
|
// .lineLimit(lineLimit)
|
||||||
|
.environment(\.layoutDirection, isRTL() ? .rightToLeft : .leftToRight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isRTL() -> Bool {
|
||||||
|
// Arabic, Hebrew, Persian, Urdu, Kurdish, Azeri, Dhivehi
|
||||||
|
["ar", "he", "fa", "ur", "ku", "az", "dv"].contains(language)
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,3 +73,56 @@ class DateFormatterCache: @unchecked Sendable {
|
||||||
self.createdAtDateFormatter = createdAtDateFormatter
|
self.createdAtDateFormatter = createdAtDateFormatter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Relationship: Codable {
|
||||||
|
public let id: String
|
||||||
|
public let following: Bool
|
||||||
|
public let showingReblogs: Bool
|
||||||
|
public let followedBy: Bool
|
||||||
|
public let blocking: Bool
|
||||||
|
public let blockedBy: Bool
|
||||||
|
public let muting: Bool
|
||||||
|
public let mutingNotifications: Bool
|
||||||
|
public let requested: Bool
|
||||||
|
public let domainBlocking: Bool
|
||||||
|
public let endorsed: Bool
|
||||||
|
public let note: String
|
||||||
|
public let notifying: Bool
|
||||||
|
|
||||||
|
public static func placeholder() -> Relationship {
|
||||||
|
.init(id: UUID().uuidString,
|
||||||
|
following: false,
|
||||||
|
showingReblogs: false,
|
||||||
|
followedBy: false,
|
||||||
|
blocking: false,
|
||||||
|
blockedBy: false,
|
||||||
|
muting: false,
|
||||||
|
mutingNotifications: false,
|
||||||
|
requested: false,
|
||||||
|
domainBlocking: false,
|
||||||
|
endorsed: false,
|
||||||
|
note: "",
|
||||||
|
notifying: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Relationship {
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
|
||||||
|
following = try values.decodeIfPresent(Bool.self, forKey: .following) ?? false
|
||||||
|
showingReblogs = try values.decodeIfPresent(Bool.self, forKey: .showingReblogs) ?? false
|
||||||
|
followedBy = try values.decodeIfPresent(Bool.self, forKey: .followedBy) ?? false
|
||||||
|
blocking = try values.decodeIfPresent(Bool.self, forKey: .blocking) ?? false
|
||||||
|
blockedBy = try values.decodeIfPresent(Bool.self, forKey: .blockedBy) ?? false
|
||||||
|
muting = try values.decodeIfPresent(Bool.self, forKey: .muting) ?? false
|
||||||
|
mutingNotifications = try values.decodeIfPresent(Bool.self, forKey: .mutingNotifications) ?? false
|
||||||
|
requested = try values.decodeIfPresent(Bool.self, forKey: .requested) ?? false
|
||||||
|
domainBlocking = try values.decodeIfPresent(Bool.self, forKey: .domainBlocking) ?? false
|
||||||
|
endorsed = try values.decodeIfPresent(Bool.self, forKey: .endorsed) ?? false
|
||||||
|
note = try values.decodeIfPresent(String.self, forKey: .note) ?? ""
|
||||||
|
notifying = try values.decodeIfPresent(Bool.self, forKey: .notifying) ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Relationship: Sendable {}
|
||||||
|
|
|
@ -2,11 +2,61 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
public class AccountManager {
|
||||||
|
private var client: Client?
|
||||||
|
private var account: Account?
|
||||||
|
|
||||||
|
init(client: Client? = nil, account: Account? = nil) {
|
||||||
|
self.client = client
|
||||||
|
self.account = account
|
||||||
|
}
|
||||||
|
|
||||||
|
public func clear() {
|
||||||
|
self.client = nil
|
||||||
|
self.account = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setClient(_ client: Client) {
|
||||||
|
self.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getClient() -> Client? {
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getAccount() -> Account? {
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
public func forceClient() -> Client {
|
||||||
|
if client != nil {
|
||||||
|
return client!
|
||||||
|
} else {
|
||||||
|
fatalError("Client is not existant in that context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func forceAccount() -> Account {
|
||||||
|
if account != nil {
|
||||||
|
return account!
|
||||||
|
} else {
|
||||||
|
fatalError("Account is not existant in that context and couldn't be fetched from Client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func fetchAccount() async -> Account? {
|
||||||
|
guard client != nil else { fatalError("Client is not existant in that context") }
|
||||||
|
account = try? await client!.get(endpoint: Accounts.verifyCredentials)
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct AppAccount: Codable, Identifiable, Hashable {
|
public struct AppAccount: Codable, Identifiable, Hashable {
|
||||||
public let server: String
|
public let server: String
|
||||||
public var accountName: String?
|
public var accountName: String?
|
||||||
public let oauthToken: OauthToken?
|
public let oauthToken: OauthToken?
|
||||||
public static let saveKey: String = "threaded-appaccount.current"
|
private static let saveKey: String = "threaded-appaccount.current"
|
||||||
|
|
||||||
public var key: String {
|
public var key: String {
|
||||||
if let oauthToken {
|
if let oauthToken {
|
||||||
|
@ -43,6 +93,10 @@ public struct AppAccount: Codable, Identifiable, Hashable {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func clear() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: AppAccount.saveKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppAccount: Sendable {}
|
extension AppAccount: Sendable {}
|
||||||
|
|
|
@ -30,6 +30,10 @@ struct FetchTimeline {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func setTimelineFilter(_ filter: TimelineFilter) {
|
||||||
|
self.timeline = filter
|
||||||
|
}
|
||||||
|
|
||||||
func getStatuses() -> [Status] {
|
func getStatuses() -> [Status] {
|
||||||
return datasource
|
return datasource
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,14 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
public class Navigator {
|
public class Navigator: ObservableObject {
|
||||||
public var path: [RouterDestination] = []
|
public var path: [RouterDestination] = []
|
||||||
public var presentedSheet: SheetDestination?
|
public var presentedSheet: SheetDestination?
|
||||||
public var selectedTab: TabDestination = .timeline
|
public var selectedTab: TabDestination = .timeline
|
||||||
|
|
||||||
public func navigate(to: RouterDestination) {
|
public func navigate(to: RouterDestination) {
|
||||||
path.append(to)
|
path.append(to)
|
||||||
|
print("appended view")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ public enum TabDestination: Identifiable {
|
||||||
public enum SheetDestination: Identifiable {
|
public enum SheetDestination: Identifiable {
|
||||||
case welcome
|
case welcome
|
||||||
case mastodonLogin(logged: Binding<Bool>)
|
case mastodonLogin(logged: Binding<Bool>)
|
||||||
case post
|
case post(content: String = "")
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -72,15 +73,15 @@ public enum RouterDestination: Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func withAppRouter() -> some View {
|
func withAppRouter(_ navigator: Navigator) -> some View {
|
||||||
navigationDestination(for: RouterDestination.self) { destination in
|
navigationDestination(for: RouterDestination.self) { destination in
|
||||||
switch destination {
|
switch destination {
|
||||||
case .settings:
|
case .settings:
|
||||||
SettingsView()
|
SettingsView(navigator: navigator)
|
||||||
case .privacy:
|
case .privacy:
|
||||||
PrivacyView()
|
PrivacyView()
|
||||||
case .account(let acc):
|
case .account(let acc):
|
||||||
AccountView(account: acc)
|
AccountView(account: acc, navigator: navigator)
|
||||||
case .about:
|
case .about:
|
||||||
AboutView()
|
AboutView()
|
||||||
}
|
}
|
||||||
|
@ -110,8 +111,11 @@ extension View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch destination {
|
switch destination {
|
||||||
case .post:
|
case .post(let content):
|
||||||
Text("Posting view")
|
NavigationStack {
|
||||||
|
PostingView(initialString: content)
|
||||||
|
.tint(Color(uiColor: UIColor.label))
|
||||||
|
}
|
||||||
case let .mastodonLogin(logged):
|
case let .mastodonLogin(logged):
|
||||||
AddInstanceView(logged: logged)
|
AddInstanceView(logged: logged)
|
||||||
.tint(Color.accentColor)
|
.tint(Color.accentColor)
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Threaded uses third-party libraries and code:\n- [IceCubesApp](https://github.com/dimillian/IceCubesApp)\n- [SwiftSoup](https://github.com/scinfu/SwiftSoup)"
|
"value" : "Threaded uses third-party open-source libraries and code:\n- [IceCubesApp](https://github.com/dimillian/IceCubesApp)\n- [SwiftSoup](https://github.com/scinfu/SwiftSoup)\n- [Nuke](https://github.com/kean/Nuke)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,78 @@
|
||||||
"accessibility.media.supported-type.video.label" : {
|
"accessibility.media.supported-type.video.label" : {
|
||||||
"comment" : "A localized description of SupportedType.video"
|
"comment" : "A localized description of SupportedType.video"
|
||||||
},
|
},
|
||||||
|
"account.follow" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Follow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"account.follow-back" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Follow back"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"account.followers-%lld" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"variations" : {
|
||||||
|
"plural" : {
|
||||||
|
"one" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "%lld follower"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"other" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "%lld followers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"account.mention" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mention"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"account.unfollow" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Unfollow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error.loading-image" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Image Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Hello world" : {
|
"Hello world" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -174,9 +246,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Posting view" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"setting.privacy" : {
|
"setting.privacy" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
@ -230,6 +299,96 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"status.posting" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "New post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.cancel" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.placeholder" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "What's new?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.post" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.visibility" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Visibility"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.visibility.direct" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Direct Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.visibility.private" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Private"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.visibility.public" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status.posting.visibility.unlisted" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Unlisted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"status.replies-%lld" : {
|
"status.replies-%lld" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|
|
@ -3,38 +3,91 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AccountView: View {
|
struct AccountView: View {
|
||||||
@Environment(Client.self) private var client: Client
|
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||||
|
|
||||||
@Namespace var accountAnims
|
@Namespace var accountAnims
|
||||||
@Namespace var animPicture
|
@Namespace var animPicture
|
||||||
|
|
||||||
@State private var navigator: Navigator = Navigator()
|
|
||||||
@State private var biggerPicture: Bool = false
|
@State private var biggerPicture: Bool = false
|
||||||
@State private var location: CGPoint = .zero
|
|
||||||
|
|
||||||
|
@State var isCurrent: Bool = false
|
||||||
@State var account: Account
|
@State var account: Account
|
||||||
@State var statuses: [Status]?
|
@State var navigator: Navigator = Navigator()
|
||||||
@State var statusesPinned: [Status]?
|
|
||||||
|
@State private var canFollow: Bool? = nil
|
||||||
|
@State private var initialFollowing: Bool = false
|
||||||
|
@State private var isFollowing: Bool = false
|
||||||
|
@State private var accountFollows: Bool = false
|
||||||
|
|
||||||
|
@State private var statuses: [Status]?
|
||||||
|
@State private var statusesPinned: [Status]?
|
||||||
|
|
||||||
private let animPicCurve = Animation.smooth(duration: 0.25, extraBounce: 0.0)
|
private let animPicCurve = Animation.smooth(duration: 0.25, extraBounce: 0.0)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
if isCurrent {
|
||||||
|
NavigationStack(path: $navigator.path) {
|
||||||
|
accountView
|
||||||
|
.onAppear {
|
||||||
|
account = accountManager.forceAccount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
accountView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountView: some View {
|
||||||
ZStack (alignment: .center) {
|
ZStack (alignment: .center) {
|
||||||
if account != Account.placeholder() {
|
if account != Account.placeholder() {
|
||||||
if biggerPicture {
|
if biggerPicture {
|
||||||
big
|
big
|
||||||
|
.navigationBarBackButtonHidden()
|
||||||
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
} else {
|
} else {
|
||||||
wholeSmall
|
wholeSmall
|
||||||
|
.offset(y: isCurrent ? 50 : 0)
|
||||||
|
.overlay(alignment: .top) {
|
||||||
|
if isCurrent {
|
||||||
|
HStack {
|
||||||
|
Button {
|
||||||
|
navigator.navigate(to: .privacy)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "globe")
|
||||||
|
.font(.title2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer() // middle seperation
|
||||||
|
|
||||||
|
Button {
|
||||||
|
navigator.navigate(to: .settings)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "text.alignright")
|
||||||
|
.font(.title2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tint(Color(uiColor: UIColor.label))
|
||||||
|
.safeAreaPadding()
|
||||||
|
.background(Color.appBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loading
|
loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
await updateRelationship()
|
||||||
|
initialFollowing = isFollowing
|
||||||
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
if let ref: Account = try? await client.get(endpoint: Accounts.accounts(id: account.id)) {
|
if let client = accountManager.getClient() {
|
||||||
account = ref
|
if let ref: Account = try? await client.get(endpoint: Accounts.accounts(id: account.id)) {
|
||||||
|
account = ref
|
||||||
statuses = try? await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil))
|
|
||||||
statusesPinned = try? await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: true))
|
await updateRelationship()
|
||||||
|
statuses = try? await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil))
|
||||||
|
statusesPinned = try? await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
|
@ -48,27 +101,72 @@ struct AccountView: View {
|
||||||
var wholeSmall: some View {
|
var wholeSmall: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack {
|
VStack {
|
||||||
unbig
|
VStack (alignment: .leading) {
|
||||||
|
unbig
|
||||||
HStack {
|
|
||||||
Text(account.note.asRawText)
|
|
||||||
.font(.body)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
|
|
||||||
Spacer()
|
Text(account.note.asRawText)
|
||||||
|
.font(.callout)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
|
||||||
|
let followCount = (account.followersCount ?? 0 - (initialFollowing ? 1 : 0)) + (isFollowing ? 1 : 0)
|
||||||
|
Text("account.followers-\(followCount)")
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.font(.callout)
|
||||||
|
|
||||||
|
if canFollow != nil && (canFollow ?? true) == true {
|
||||||
|
HStack (spacing: 5) {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await followAccount()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(isFollowing ? "account.unfollow" : accountFollows ? "account.follow-back" : "account.follow")
|
||||||
|
.font(.callout)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(LargeButton(filled: true, height: 10))
|
||||||
|
|
||||||
|
Button {
|
||||||
|
if let server = account.acct.split(separator: "@").last {
|
||||||
|
navigator.presentedSheet = .post(content: "@\(account.username)@\(server)")
|
||||||
|
} else {
|
||||||
|
let client = accountManager.getClient()
|
||||||
|
navigator.presentedSheet = .post(content: "@\(account.username)@\(client?.server ?? "???")")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("account.mention")
|
||||||
|
.font(.callout)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(LargeButton(filled: false, height: 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
Rectangle()
|
VStack {
|
||||||
.fill(Color.gray.opacity(0.2))
|
Rectangle()
|
||||||
.frame(width: .infinity, height: 1)
|
.fill(Color.gray.opacity(0.2))
|
||||||
.padding(.bottom, 3)
|
.frame(width: .infinity, height: 1)
|
||||||
|
.padding(.bottom, 3)
|
||||||
statusesList
|
|
||||||
|
statusesList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.safeAreaPadding(.vertical)
|
.safeAreaPadding(.vertical)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
.withAppRouter()
|
.withAppRouter(navigator)
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusesList: some View {
|
var statusesList: some View {
|
||||||
|
@ -91,14 +189,42 @@ struct AccountView: View {
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if statuses == nil {
|
if statuses == nil {
|
||||||
Task {
|
if let client = accountManager.getClient() {
|
||||||
statuses = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil))
|
Task {
|
||||||
statusesPinned = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: true))
|
statuses = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil))
|
||||||
|
statusesPinned = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func followAccount() async {
|
||||||
|
if let client = accountManager.getClient() {
|
||||||
|
Task {
|
||||||
|
let endpoint: Endpoint = isFollowing ? Accounts.unfollow(id: account.id) : Accounts.follow(id: account.id, notify: false, reblogs: true)
|
||||||
|
HapticManager.playHaptics(haptics: Haptic.tap)
|
||||||
|
try await client.post(endpoint: endpoint) // Notify off until APNs? | Reblogs on by default (later changeable)
|
||||||
|
isFollowing = !isFollowing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateRelationship() async {
|
||||||
|
if let client = accountManager.getClient() {
|
||||||
|
if let currentAccount: Account = try? await client.get(endpoint: Accounts.verifyCredentials) {
|
||||||
|
canFollow = currentAccount.id != account.id
|
||||||
|
guard canFollow == true else { return }
|
||||||
|
if let relationship: [Relationship] = try? await client.get(endpoint: Accounts.relationships(ids: [account.id])) {
|
||||||
|
isFollowing = relationship.first!.following
|
||||||
|
accountFollows = relationship.first!.followedBy
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canFollow = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var loading: some View {
|
var loading: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack {
|
VStack {
|
||||||
|
@ -117,7 +243,6 @@ struct AccountView: View {
|
||||||
.safeAreaPadding(.vertical)
|
.safeAreaPadding(.vertical)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
.withAppRouter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var unbig: some View {
|
var unbig: some View {
|
||||||
|
@ -130,6 +255,7 @@ struct AccountView: View {
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
let server = account.acct.split(separator: "@").last
|
let server = account.acct.split(separator: "@").last
|
||||||
|
let client = accountManager.getClient()
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
if server != nil {
|
if server != nil {
|
||||||
|
@ -148,7 +274,7 @@ struct AccountView: View {
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
Text("\(client.server)")
|
Text("\(client?.server ?? "???")")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(Color.gray)
|
.foregroundStyle(Color.gray)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
@ -159,7 +285,7 @@ struct AccountView: View {
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
Text("\(client.server)")
|
Text("\(client?.server ?? "???")")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(Color.gray)
|
.foregroundStyle(Color.gray)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
@ -197,7 +323,7 @@ struct AccountView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var profilePicture: some View {
|
var profilePicture: some View {
|
||||||
OnlineImage(url: account.avatar)
|
OnlineImage(url: account.avatar, size: biggerPicture ? 300 : 50, useNuke: true)
|
||||||
.clipShape(.circle)
|
.clipShape(.circle)
|
||||||
.matchedGeometryEffect(id: animPicture, in: accountAnims)
|
.matchedGeometryEffect(id: animPicture, in: accountAnims)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
|
|
@ -10,6 +10,12 @@ struct ConnectView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
Image("HeroIcon")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 30)
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
Text("login.title")
|
Text("login.title")
|
||||||
.font(.title.bold())
|
.font(.title.bold())
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
@ -30,6 +36,7 @@ struct ConnectView: View {
|
||||||
noAccount
|
noAccount
|
||||||
}
|
}
|
||||||
.buttonStyle(LargeButton())
|
.buttonStyle(LargeButton())
|
||||||
|
.disabled(true)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 100)
|
.padding(.vertical, 100)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,13 @@ import SwiftUI
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@State private var navigator = Navigator()
|
@State private var navigator = Navigator()
|
||||||
@State private var sheet: SheetDestination?
|
@State private var sheet: SheetDestination?
|
||||||
@State private var client: Client?
|
@State private var accountManager: AccountManager = AccountManager()
|
||||||
@State private var currentAccount: Account?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $navigator.selectedTab, content: {
|
TabView(selection: $navigator.selectedTab, content: {
|
||||||
ZStack {
|
ZStack {
|
||||||
if client != nil {
|
if accountManager.getClient() != nil {
|
||||||
TimelineView(timelineModel: FetchTimeline(client: self.client!))
|
TimelineView(navigator: navigator, timelineModel: FetchTimeline(client: accountManager.forceClient()))
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
.safeAreaPadding()
|
.safeAreaPadding()
|
||||||
} else {
|
} else {
|
||||||
|
@ -33,9 +32,10 @@ struct ContentView: View {
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
.tag(TabDestination.activity)
|
.tag(TabDestination.activity)
|
||||||
|
|
||||||
ProfileView(account: currentAccount ?? .placeholder())
|
AccountView(isCurrent: true, account: accountManager.getAccount() ?? .placeholder())
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
.tag(TabDestination.profile)
|
.tag(TabDestination.profile)
|
||||||
|
|
||||||
})
|
})
|
||||||
.overlay(alignment: .bottom) {
|
.overlay(alignment: .bottom) {
|
||||||
TabsView(navigator: navigator)
|
TabsView(navigator: navigator)
|
||||||
|
@ -43,17 +43,22 @@ struct ContentView: View {
|
||||||
.zIndex(10)
|
.zIndex(10)
|
||||||
}
|
}
|
||||||
.withCovers(sheetDestination: $sheet)
|
.withCovers(sheetDestination: $sheet)
|
||||||
|
.withSheets(sheetDestination: $navigator.presentedSheet)
|
||||||
|
.environment(accountManager)
|
||||||
.environment(navigator)
|
.environment(navigator)
|
||||||
.environment(client)
|
.task {
|
||||||
.onAppear {
|
await recognizeAccount()
|
||||||
let acc = try? AppAccount.loadAsCurrent()
|
}
|
||||||
if acc == nil {
|
}
|
||||||
sheet = .welcome
|
|
||||||
} else {
|
func recognizeAccount() async {
|
||||||
Task {
|
let acc = try? AppAccount.loadAsCurrent()
|
||||||
client = .init(server: acc!.server, oauthToken: acc!.oauthToken)
|
if acc == nil {
|
||||||
currentAccount = try? await client!.get(endpoint: Accounts.verifyCredentials)
|
sheet = .welcome
|
||||||
}
|
} else {
|
||||||
|
Task {
|
||||||
|
accountManager.setClient(.init(server: acc!.server, oauthToken: acc!.oauthToken))
|
||||||
|
await accountManager.fetchAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +73,7 @@ struct ContentView: View {
|
||||||
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)]
|
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)]
|
||||||
|
|
||||||
UITabBar.appearance().standardAppearance = appearance
|
UITabBar.appearance().standardAppearance = appearance
|
||||||
|
UINavigationBar.appearance().tintColor = UIColor.label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
//Made by Lumaa
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
|
struct PostingView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||||
|
|
||||||
|
var initialString: String = ""
|
||||||
|
@State private var postText: NSMutableAttributedString = .init(string: "")
|
||||||
|
@State private var visibility: Visibility = .pub
|
||||||
|
@State private var selectedPhotos: PhotosPickerItem?
|
||||||
|
|
||||||
|
@State private var postingStatus: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if accountManager.getAccount() != nil {
|
||||||
|
posting
|
||||||
|
} else {
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var posting: some View {
|
||||||
|
VStack {
|
||||||
|
HStack(alignment: .top, spacing: 0) {
|
||||||
|
// MARK: Profile picture
|
||||||
|
profilePicture
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
// MARK: Status main content
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text(accountManager.forceAccount().username)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.bold()
|
||||||
|
|
||||||
|
DynamicTextEditor($postText)
|
||||||
|
.placeholder(String(localized: "status.posting.placeholder"))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(Color(uiColor: UIColor.label))
|
||||||
|
|
||||||
|
editorButtons
|
||||||
|
.padding(.vertical)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Picker("status.posting.visibility", selection: $visibility) {
|
||||||
|
ForEach(Visibility.allCases, id: \.self) { item in
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
switch (item) {
|
||||||
|
case .pub:
|
||||||
|
Text("status.posting.visibility.public")
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
case .unlisted:
|
||||||
|
Text("status.posting.visibility.unlisted")
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
case .direct:
|
||||||
|
Text("status.posting.visibility.direct")
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
case .priv:
|
||||||
|
Text("status.posting.visibility.private")
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
.frame(width: 200, alignment: .leading)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
if let client = accountManager.getClient() {
|
||||||
|
postingStatus = true
|
||||||
|
try await client.post(endpoint: Statuses.postStatus(json: .init(status: postText.string, visibility: visibility)))
|
||||||
|
postingStatus = false
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if postingStatus {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.foregroundStyle(Color.appBackground)
|
||||||
|
} else {
|
||||||
|
Text("status.posting.post")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(postingStatus)
|
||||||
|
.buttonStyle(LargeButton(filled: true, height: 7.5))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.navigationBarBackButtonHidden()
|
||||||
|
.navigationTitle(Text("status.posting"))
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button {
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Text("status.posting.cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
postText.append(NSAttributedString(string: initialString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loading: some View {
|
||||||
|
ProgressView()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
}
|
||||||
|
|
||||||
|
var editorButtons: some View {
|
||||||
|
HStack(spacing: 18) {
|
||||||
|
PhotosPicker(selection: $selectedPhotos, matching: .any(of: [.images, .videos]), label: {
|
||||||
|
Image(systemName: "photo.badge.plus")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
})
|
||||||
|
.tint(Color.blue)
|
||||||
|
|
||||||
|
actionButton("number") {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
postText.append(NSAttributedString(string: "#"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func actionButton(_ image: String, action: @escaping () -> Void) -> some View {
|
||||||
|
Button {
|
||||||
|
action()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: image)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
.tint(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func asyncActionButton(_ image: String, action: @escaping () async -> Void) -> some View {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await action()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: image)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
.tint(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
var profilePicture: some View {
|
||||||
|
OnlineImage(url: accountManager.forceAccount().avatar, size: 50, useNuke: true)
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.clipShape(.circle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PostingView()
|
||||||
|
}
|
|
@ -15,6 +15,8 @@ struct AboutView: View {
|
||||||
.listRowBackground(Color.appBackground)
|
.listRowBackground(Color.appBackground)
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color.appBackground)
|
||||||
.navigationTitle("about")
|
.navigationTitle("about")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +31,8 @@ struct AboutView: View {
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color.appBackground)
|
||||||
.navigationTitle("about.app")
|
.navigationTitle("about.app")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,41 +3,51 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Environment(Navigator.self) private var navigator: Navigator
|
@State var navigator: Navigator
|
||||||
@State private var sheet: SheetDestination?
|
@State private var sheet: SheetDestination?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
NavigationStack(path: $navigator.path) {
|
||||||
Button {
|
List {
|
||||||
navigator.navigate(to: .about)
|
Button {
|
||||||
} label: {
|
navigator.navigate(to: .about)
|
||||||
Label("about", systemImage: "info.circle")
|
} label: {
|
||||||
|
Label("about", systemImage: "info.circle")
|
||||||
|
}
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
navigator.navigate(to: .privacy)
|
||||||
|
} label: {
|
||||||
|
Label("setting.privacy", systemImage: "lock")
|
||||||
|
}
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
AppAccount.clear()
|
||||||
|
sheet = .welcome
|
||||||
|
} label: {
|
||||||
|
Text("logout")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
.tint(Color.red)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
}
|
}
|
||||||
.listRowSeparator(.hidden)
|
.withAppRouter(navigator)
|
||||||
|
.withCovers(sheetDestination: $sheet)
|
||||||
Button {
|
.scrollContentBackground(.hidden)
|
||||||
navigator.navigate(to: .privacy)
|
.tint(Color.white)
|
||||||
} label: {
|
.background(Color.appBackground)
|
||||||
Label("setting.privacy", systemImage: "lock")
|
.listStyle(.inset)
|
||||||
}
|
.navigationTitle("settings")
|
||||||
.listRowSeparator(.hidden)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|
||||||
Button {
|
|
||||||
UserDefaults.standard.removeObject(forKey: AppAccount.saveKey)
|
|
||||||
sheet = .welcome
|
|
||||||
} label: {
|
|
||||||
Text("logout")
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
}
|
|
||||||
.tint(Color.red)
|
|
||||||
.listRowSeparator(.visible, edges: .bottom)
|
|
||||||
}
|
}
|
||||||
.withCovers(sheetDestination: $sheet)
|
|
||||||
.listStyle(.inset)
|
|
||||||
.navigationTitle("settings")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SettingsView()
|
SettingsView(navigator: .init())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,55 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TimelineView: View {
|
struct TimelineView: View {
|
||||||
@Environment(Client.self) private var client: Client
|
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||||
|
@State var navigator: Navigator
|
||||||
|
|
||||||
|
@State private var showPicker: Bool = false
|
||||||
|
@State private var stringTimeline: String = "home"
|
||||||
|
@State private var timeline: TimelineFilter = .home
|
||||||
|
@State private var timelines: [TimelineFilter] = [.trending, .home]
|
||||||
|
|
||||||
@State private var navigator: Navigator = Navigator()
|
|
||||||
@State private var statuses: [Status]?
|
@State private var statuses: [Status]?
|
||||||
|
|
||||||
@State var timelineModel: FetchTimeline
|
@State var timelineModel: FetchTimeline // home timeline by default
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $navigator.path) {
|
NavigationStack(path: $navigator.path) {
|
||||||
if statuses != nil {
|
if statuses != nil {
|
||||||
if !statuses!.isEmpty {
|
if !statuses!.isEmpty {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
Image("HeroIcon")
|
Button {
|
||||||
.resizable()
|
withAnimation(.easeInOut) {
|
||||||
.aspectRatio(contentMode: .fit)
|
showPicker.toggle()
|
||||||
.frame(width: 30)
|
}
|
||||||
.padding(.bottom)
|
} label: {
|
||||||
|
Image("HeroIcon")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 30)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if showPicker {
|
||||||
|
// //TODO: Fix this
|
||||||
|
//
|
||||||
|
// MetaPicker(items: timelines.map { $0.rawValue }, selectedItem: $stringTimeline) { item in
|
||||||
|
// let title: String = timelines.filter{ $0.rawValue == item }.first?.localizedTitle() ?? "Unknown"
|
||||||
|
// Text("\(title)")
|
||||||
|
// }
|
||||||
|
// .padding(.bottom)
|
||||||
|
// .onChange(of: stringTimeline) { _, newTimeline in
|
||||||
|
// let loc = timelines.filter{ $0.rawValue == newTimeline }.first?.localizedTitle()
|
||||||
|
// switch (loc) {
|
||||||
|
// case "home":
|
||||||
|
// timeline = .home
|
||||||
|
// case "trending":
|
||||||
|
// timeline = .trending
|
||||||
|
// default:
|
||||||
|
// timeline = .home
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
ForEach(statuses!, id: \.id) { status in
|
ForEach(statuses!, id: \.id) { status in
|
||||||
VStack(spacing: 2) {
|
VStack(spacing: 2) {
|
||||||
|
@ -27,9 +59,16 @@ struct TimelineView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.refreshable {
|
||||||
|
if let client = accountManager.getClient() {
|
||||||
|
Task {
|
||||||
|
statuses = try? await client.get(endpoint: Timelines.home(sinceId: nil, maxId: nil, minId: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
.withAppRouter()
|
.withAppRouter(navigator)
|
||||||
} else {
|
} else {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.appBackground
|
Color.appBackground
|
||||||
|
@ -60,8 +99,10 @@ struct TimelineView: View {
|
||||||
Color.appBackground
|
Color.appBackground
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onAppear {
|
.onAppear {
|
||||||
Task {
|
if let client = accountManager.getClient() {
|
||||||
statuses = try? await client.get(endpoint: Timelines.home(sinceId: nil, maxId: nil, minId: nil))
|
Task {
|
||||||
|
statuses = try? await client.get(endpoint: Timelines.home(sinceId: nil, maxId: nil, minId: nil))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue