New post view, follow unfollow, Nuke, better CompactPostView, fixes and improvements
This commit is contained in:
parent
17463147c7
commit
a2ee1a092b
|
@ -11,3 +11,4 @@ Threaded is a 100% free, made in SwiftUI, [#OpenSource](https://github.com/luma
|
|||
- [Lumaa](https://lumaa.fr/)
|
||||
- [IceCubesApp](https://github.com/dimillian/IceCubesApp) by [@dimillian](https://github.com/dimillian)
|
||||
- [SwiftSoup](https://github.com/scinfu/SwiftSoup)
|
||||
- [Nuke](https://github.com/kean/Nuke)
|
||||
|
|
|
@ -7,6 +7,14 @@
|
|||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE252B3DE5A10044756D /* AccountView.swift */; };
|
||||
B97BCE282B3ED2A80044756D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = B97BCE272B3ED2A80044756D /* .gitignore */; };
|
||||
|
@ -17,6 +25,7 @@
|
|||
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */; };
|
||||
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C152B2F363600D9F3C1 /* TimelineFilter.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 */; };
|
||||
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945A2B2DEECE00D81C07 /* ThreadedApp.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 */; };
|
||||
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94852B2E211200D81C07 /* Account+Elms.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 */; };
|
||||
B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB948D2B2E28E800D81C07 /* ShareableImage.swift */; };
|
||||
B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B9FB948F2B2E2B0E00D81C07 /* Localizable.xcstrings */; };
|
||||
|
@ -75,6 +83,9 @@
|
|||
/* End PBXCopyFilesBuildPhase 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>"; };
|
||||
B97BCE252B3DE5A10044756D /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
|
@ -101,7 +113,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -126,7 +137,12 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B93B67762B42E8AB000892E9 /* EmojiText in Frameworks */,
|
||||
B9FB94842B2E20AF00D81C07 /* SwiftSoup in Frameworks */,
|
||||
B93B676D2B42C94F000892E9 /* Nuke in Frameworks */,
|
||||
B93B67732B42C94F000892E9 /* NukeVideo in Frameworks */,
|
||||
B93B676F2B42C94F000892E9 /* NukeExtensions in Frameworks */,
|
||||
B93B67712B42C94F000892E9 /* NukeUI in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -212,11 +228,11 @@
|
|||
children = (
|
||||
B9FB94952B2EDAB600D81C07 /* Settings */,
|
||||
B9FB94712B2DF49700D81C07 /* ConnectView.swift */,
|
||||
B9FB94892B2E227000D81C07 /* ProfileView.swift */,
|
||||
B9FB94982B2EEB9400D81C07 /* AddInstanceView.swift */,
|
||||
B9FB945C2B2DEECE00D81C07 /* ContentView.swift */,
|
||||
B9842C112B2F2A5800D9F3C1 /* TimelineView.swift */,
|
||||
B97BCE252B3DE5A10044756D /* AccountView.swift */,
|
||||
B93B677B2B433A6E000892E9 /* PostingView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -226,8 +242,11 @@
|
|||
children = (
|
||||
B9FB94752B2E023D00D81C07 /* TabsView.swift */,
|
||||
B9FB94732B2DF6A100D81C07 /* ButtonStyles.swift */,
|
||||
B93B67792B42EC51000892E9 /* MetaPicker.swift */,
|
||||
B9FB948B2B2E232300D81C07 /* OnlineImage.swift */,
|
||||
B9842C0D2B2F21B700D9F3C1 /* CompactPostView.swift */,
|
||||
B93B67772B42E8F0000892E9 /* TextEmoji.swift */,
|
||||
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
|
@ -291,6 +310,11 @@
|
|||
name = Threaded;
|
||||
packageProductDependencies = (
|
||||
B9FB94832B2E20AF00D81C07 /* SwiftSoup */,
|
||||
B93B676C2B42C94F000892E9 /* Nuke */,
|
||||
B93B676E2B42C94F000892E9 /* NukeExtensions */,
|
||||
B93B67702B42C94F000892E9 /* NukeUI */,
|
||||
B93B67722B42C94F000892E9 /* NukeVideo */,
|
||||
B93B67752B42E8AB000892E9 /* EmojiText */,
|
||||
);
|
||||
productName = Threaded;
|
||||
productReference = B9FB94572B2DEECE00D81C07 /* Threaded.app */;
|
||||
|
@ -342,6 +366,8 @@
|
|||
mainGroup = B9FB944E2B2DEECE00D81C07;
|
||||
packageReferences = (
|
||||
B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||
B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */,
|
||||
);
|
||||
productRefGroup = B9FB94582B2DEECE00D81C07 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -384,18 +410,19 @@
|
|||
B9FB94922B2E35D000D81C07 /* SettingsView.swift in Sources */,
|
||||
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */,
|
||||
B9FB94882B2E223E00D81C07 /* Emoji.swift in Sources */,
|
||||
B93B67782B42E8F0000892E9 /* TextEmoji.swift in Sources */,
|
||||
B9FB94762B2E023D00D81C07 /* TabsView.swift in Sources */,
|
||||
B9FB947D2B2E19E300D81C07 /* AccountManager.swift in Sources */,
|
||||
B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */,
|
||||
B9842C0E2B2F21B700D9F3C1 /* CompactPostView.swift in Sources */,
|
||||
B9FB94992B2EEB9400D81C07 /* AddInstanceView.swift in Sources */,
|
||||
B9FB94972B2EDABF00D81C07 /* PrivacyView.swift in Sources */,
|
||||
B9FB948A2B2E227000D81C07 /* ProfileView.swift in Sources */,
|
||||
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */,
|
||||
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */,
|
||||
B9FB949B2B2EF09A00D81C07 /* Client.swift in Sources */,
|
||||
B9FB949D2B2EF0D600D81C07 /* Instance.swift in Sources */,
|
||||
B9842C102B2F228C00D9F3C1 /* Status.swift in Sources */,
|
||||
B93B677A2B42EC51000892E9 /* MetaPicker.swift in Sources */,
|
||||
B9FB94722B2DF49700D81C07 /* ConnectView.swift in Sources */,
|
||||
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */,
|
||||
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */,
|
||||
|
@ -406,8 +433,10 @@
|
|||
B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */,
|
||||
B9FB94742B2DF6A100D81C07 /* ButtonStyles.swift in Sources */,
|
||||
B9FB94702B2DF3CD00D81C07 /* Navigator.swift in Sources */,
|
||||
B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */,
|
||||
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */,
|
||||
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */,
|
||||
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */,
|
||||
B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */,
|
||||
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */,
|
||||
B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */,
|
||||
|
@ -721,6 +750,22 @@
|
|||
/* End XCConfigurationList 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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/scinfu/SwiftSoup";
|
||||
|
@ -732,6 +777,31 @@
|
|||
/* End XCRemoteSwiftPackageReference 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 */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
{
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
|
@ -4,10 +4,12 @@ import SwiftUI
|
|||
|
||||
struct LargeButton: ButtonStyle {
|
||||
var filled: Bool = false
|
||||
var height: CGFloat? = nil
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, height)
|
||||
.background {
|
||||
if filled {
|
||||
Color(uiColor: UIColor.label)
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
import SwiftUI
|
||||
|
||||
struct CompactPostView: View {
|
||||
@Environment(Client.self) private var client: Client
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
var status: Status
|
||||
var navigator: Navigator
|
||||
@ObservedObject var navigator: Navigator
|
||||
var pinned: Bool = false
|
||||
|
||||
@State private var initialLike: Bool = false
|
||||
|
@ -14,22 +14,18 @@ struct CompactPostView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if status.reblog != nil {
|
||||
VStack(alignment: .leading) {
|
||||
repostNotice
|
||||
.padding(.leading, 30)
|
||||
|
||||
statusRepost
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
if pinned {
|
||||
pinnedNotice
|
||||
.padding(.leading, 15)
|
||||
.padding(.leading, 35)
|
||||
}
|
||||
|
||||
statusPost
|
||||
if status.reblog != nil {
|
||||
repostNotice
|
||||
.padding(.leading, 30)
|
||||
}
|
||||
|
||||
statusPost(status.reblog ?? status)
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
|
@ -45,6 +41,7 @@ struct CompactPostView: View {
|
|||
}
|
||||
|
||||
func likePost() async throws {
|
||||
if let client = accountManager.getClient() {
|
||||
guard client.isAuth else { fatalError("Client is not authenticated") }
|
||||
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
||||
let endpoint = !isLiked ? Statuses.favorite(id: statusId) : Statuses.unfavorite(id: statusId)
|
||||
|
@ -55,8 +52,10 @@ struct CompactPostView: View {
|
|||
isLiked = newStatus.favourited ?? !isLiked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func repostPost() async throws {
|
||||
if let client = accountManager.getClient() {
|
||||
guard client.isAuth else { fatalError("Client is not authenticated") }
|
||||
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
||||
let endpoint = !isReposted ? Statuses.reblog(id: statusId) : Statuses.unreblog(id: statusId)
|
||||
|
@ -67,36 +66,42 @@ struct CompactPostView: View {
|
|||
isReposted = newStatus.reblogged ?? !isReposted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var statusPost: some View {
|
||||
@ViewBuilder
|
||||
func statusPost(_ status: AnyStatus) -> some View {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// MARK: Profile picture
|
||||
// if status.repliesCount > 0 {
|
||||
// VStack {
|
||||
// profilePicture
|
||||
// .onTapGesture {
|
||||
// navigator.navigate(to: .account(acc: status.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 {
|
||||
if status.repliesCount > 0 {
|
||||
VStack {
|
||||
profilePicture
|
||||
.onTapGesture {
|
||||
navigator.navigate(to: .account(acc: status.account))
|
||||
}
|
||||
// }
|
||||
|
||||
Spacer()
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 2.5)
|
||||
.clipShape(.capsule)
|
||||
.padding([.vertical], 5)
|
||||
|
||||
Spacer()
|
||||
|
||||
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.account))
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
// MARK: Status main content
|
||||
|
@ -108,10 +113,11 @@ struct CompactPostView: View {
|
|||
navigator.navigate(to: .account(acc: status.account))
|
||||
}
|
||||
|
||||
Text(status.content.asRawText)
|
||||
TextEmoji(status.content, emojis: status.emojis, language: status.language)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(width: 300, alignment: .topLeading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
//MARK: Action buttons
|
||||
|
@ -127,7 +133,7 @@ struct CompactPostView: View {
|
|||
}
|
||||
actionButton("bubble.right") {
|
||||
print("reply")
|
||||
navigator.presentedSheet = .post
|
||||
navigator.presentedSheet = .post()
|
||||
}
|
||||
asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") {
|
||||
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 {
|
||||
HStack (alignment:.center, spacing: 5) {
|
||||
Image(systemName: "pin.fill")
|
||||
|
@ -264,12 +186,12 @@ struct CompactPostView: View {
|
|||
|
||||
var profilePicture: some View {
|
||||
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)
|
||||
.padding(.horizontal)
|
||||
.clipShape(.circle)
|
||||
} else {
|
||||
OnlineImage(url: status.account.avatar)
|
||||
OnlineImage(url: status.account.avatar, size: 50, useNuke: true)
|
||||
.frame(width: 40, height: 40)
|
||||
.padding(.horizontal)
|
||||
.clipShape(.circle)
|
||||
|
@ -292,11 +214,12 @@ struct CompactPostView: View {
|
|||
}
|
||||
|
||||
if status.favouritesCount > 0 || isLiked {
|
||||
let addedLike: Int = isLiked ? 1 : 0
|
||||
Text("status.favourites-\(initialLike ? (status.favouritesCount - addedLike) : (status.favouritesCount + addedLike))")
|
||||
let i: Int = status.favouritesCount
|
||||
let favsCount: Int = i - (initialLike ? 1 : 0) + (isLiked ? 1 : 0)
|
||||
Text("status.favourites-\(favsCount)")
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.gray)
|
||||
.contentTransition(.numericText(value: Double(status.favouritesCount + addedLike)))
|
||||
.contentTransition(.numericText(value: Double(favsCount)))
|
||||
.transaction { t in
|
||||
t.animation = .default
|
||||
}
|
||||
|
@ -316,11 +239,11 @@ struct CompactPostView: View {
|
|||
}
|
||||
|
||||
if status.reblog!.favouritesCount > 0 || isLiked {
|
||||
let addedLike: Int = isLiked ? 1 : 0
|
||||
Text("status.favourites-\(initialLike ? (status.favouritesCount - addedLike) : (status.favouritesCount + addedLike))")
|
||||
let favsCount: Int = (status.favouritesCount - (initialLike ? 1 : 0)) + (isLiked ? 1 : 0)
|
||||
Text("status.favourites-\(favsCount)")
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.gray)
|
||||
.contentTransition(.numericText(value: Double(status.reblog!.favouritesCount + addedLike)))
|
||||
.contentTransition(.numericText(value: Double(favsCount)))
|
||||
.transaction { t in
|
||||
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,11 +1,37 @@
|
|||
//Made by Lumaa
|
||||
|
||||
import SwiftUI
|
||||
import Nuke
|
||||
import NukeUI
|
||||
|
||||
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 {
|
||||
if useNuke {
|
||||
LazyImage(url: url) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.aspectRatio(1.0, contentMode: .fit)
|
||||
} else if state.error != nil {
|
||||
ContentUnavailableView("error.loading-image", systemImage: "rectangle.slash")
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray)
|
||||
.overlay {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
}
|
||||
.priority(priority)
|
||||
.processors([.resize(width: size)])
|
||||
} else {
|
||||
AsyncImage(url: url) { element in
|
||||
element
|
||||
.resizable()
|
||||
|
@ -21,3 +47,26 @@ struct OnlineImage: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
||||
Button {
|
||||
navigator.presentedSheet = .post
|
||||
navigator.presentedSheet = .post()
|
||||
} label: {
|
||||
Tabs.post.image
|
||||
}
|
||||
|
@ -66,7 +66,6 @@ struct TabsView: View {
|
|||
}
|
||||
.buttonStyle(NoTapAnimationStyle())
|
||||
}
|
||||
.withSheets(sheetDestination: $navigator.presentedSheet)
|
||||
.padding(.horizontal, 30)
|
||||
.background(Color.appBackground)
|
||||
}
|
||||
|
@ -127,7 +126,7 @@ enum Tabs {
|
|||
extension Image {
|
||||
func tabBarify(_ neutral: Bool = true) -> some View {
|
||||
self
|
||||
.font(.title2)
|
||||
.font(.title)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@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 let server: String
|
||||
public var accountName: String?
|
||||
public let oauthToken: OauthToken?
|
||||
public static let saveKey: String = "threaded-appaccount.current"
|
||||
private static let saveKey: String = "threaded-appaccount.current"
|
||||
|
||||
public var key: String {
|
||||
if let oauthToken {
|
||||
|
@ -43,6 +93,10 @@ public struct AppAccount: Codable, Identifiable, Hashable {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func clear() {
|
||||
UserDefaults.standard.removeObject(forKey: AppAccount.saveKey)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppAccount: Sendable {}
|
||||
|
|
|
@ -30,6 +30,10 @@ struct FetchTimeline {
|
|||
return []
|
||||
}
|
||||
|
||||
mutating func setTimelineFilter(_ filter: TimelineFilter) {
|
||||
self.timeline = filter
|
||||
}
|
||||
|
||||
func getStatuses() -> [Status] {
|
||||
return datasource
|
||||
}
|
||||
|
|
|
@ -4,13 +4,14 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
@Observable
|
||||
public class Navigator {
|
||||
public class Navigator: ObservableObject {
|
||||
public var path: [RouterDestination] = []
|
||||
public var presentedSheet: SheetDestination?
|
||||
public var selectedTab: TabDestination = .timeline
|
||||
|
||||
public func navigate(to: RouterDestination) {
|
||||
path.append(to)
|
||||
print("appended view")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,7 +38,7 @@ public enum TabDestination: Identifiable {
|
|||
public enum SheetDestination: Identifiable {
|
||||
case welcome
|
||||
case mastodonLogin(logged: Binding<Bool>)
|
||||
case post
|
||||
case post(content: String = "")
|
||||
|
||||
public var id: String {
|
||||
switch self {
|
||||
|
@ -72,15 +73,15 @@ public enum RouterDestination: Hashable {
|
|||
}
|
||||
|
||||
extension View {
|
||||
func withAppRouter() -> some View {
|
||||
func withAppRouter(_ navigator: Navigator) -> some View {
|
||||
navigationDestination(for: RouterDestination.self) { destination in
|
||||
switch destination {
|
||||
case .settings:
|
||||
SettingsView()
|
||||
SettingsView(navigator: navigator)
|
||||
case .privacy:
|
||||
PrivacyView()
|
||||
case .account(let acc):
|
||||
AccountView(account: acc)
|
||||
AccountView(account: acc, navigator: navigator)
|
||||
case .about:
|
||||
AboutView()
|
||||
}
|
||||
|
@ -110,8 +111,11 @@ extension View {
|
|||
}
|
||||
} else {
|
||||
switch destination {
|
||||
case .post:
|
||||
Text("Posting view")
|
||||
case .post(let content):
|
||||
NavigationStack {
|
||||
PostingView(initialString: content)
|
||||
.tint(Color(uiColor: UIColor.label))
|
||||
}
|
||||
case let .mastodonLogin(logged):
|
||||
AddInstanceView(logged: logged)
|
||||
.tint(Color.accentColor)
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"en" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
"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" : {
|
||||
|
||||
},
|
||||
|
@ -174,9 +246,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Posting view" : {
|
||||
|
||||
},
|
||||
"setting.privacy" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
|
|
@ -3,40 +3,93 @@
|
|||
import SwiftUI
|
||||
|
||||
struct AccountView: View {
|
||||
@Environment(Client.self) private var client: Client
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
|
||||
@Namespace var accountAnims
|
||||
@Namespace var animPicture
|
||||
|
||||
@State private var navigator: Navigator = Navigator()
|
||||
@State private var biggerPicture: Bool = false
|
||||
@State private var location: CGPoint = .zero
|
||||
|
||||
@State var isCurrent: Bool = false
|
||||
@State var account: Account
|
||||
@State var statuses: [Status]?
|
||||
@State var statusesPinned: [Status]?
|
||||
@State var navigator: Navigator = Navigator()
|
||||
|
||||
@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)
|
||||
|
||||
var body: some View {
|
||||
if isCurrent {
|
||||
NavigationStack(path: $navigator.path) {
|
||||
accountView
|
||||
.onAppear {
|
||||
account = accountManager.forceAccount()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
accountView
|
||||
}
|
||||
}
|
||||
|
||||
var accountView: some View {
|
||||
ZStack (alignment: .center) {
|
||||
if account != Account.placeholder() {
|
||||
if biggerPicture {
|
||||
big
|
||||
.navigationBarBackButtonHidden()
|
||||
.toolbar(.hidden, for: .navigationBar)
|
||||
} else {
|
||||
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 {
|
||||
loading
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await updateRelationship()
|
||||
initialFollowing = isFollowing
|
||||
}
|
||||
.refreshable {
|
||||
if let client = accountManager.getClient() {
|
||||
if let ref: Account = try? await client.get(endpoint: Accounts.accounts(id: account.id)) {
|
||||
account = ref
|
||||
|
||||
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)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(Color.appBackground, for: .navigationBar)
|
||||
|
@ -48,16 +101,60 @@ struct AccountView: View {
|
|||
var wholeSmall: some View {
|
||||
ScrollView {
|
||||
VStack {
|
||||
VStack (alignment: .leading) {
|
||||
unbig
|
||||
|
||||
HStack {
|
||||
Text(account.note.asRawText)
|
||||
.font(.body)
|
||||
.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)
|
||||
|
||||
VStack {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(width: .infinity, height: 1)
|
||||
|
@ -65,10 +162,11 @@ struct AccountView: View {
|
|||
|
||||
statusesList
|
||||
}
|
||||
}
|
||||
.safeAreaPadding(.vertical)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.withAppRouter()
|
||||
.withAppRouter(navigator)
|
||||
}
|
||||
|
||||
var statusesList: some View {
|
||||
|
@ -91,6 +189,7 @@ struct AccountView: View {
|
|||
}
|
||||
.onAppear {
|
||||
if statuses == nil {
|
||||
if let client = accountManager.getClient() {
|
||||
Task {
|
||||
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))
|
||||
|
@ -98,6 +197,33 @@ struct AccountView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
ScrollView {
|
||||
|
@ -117,7 +243,6 @@ struct AccountView: View {
|
|||
.safeAreaPadding(.vertical)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.withAppRouter()
|
||||
}
|
||||
|
||||
var unbig: some View {
|
||||
|
@ -130,6 +255,7 @@ struct AccountView: View {
|
|||
.lineLimit(1)
|
||||
|
||||
let server = account.acct.split(separator: "@").last
|
||||
let client = accountManager.getClient()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
if server != nil {
|
||||
|
@ -148,7 +274,7 @@ struct AccountView: View {
|
|||
.font(.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Text("\(client.server)")
|
||||
Text("\(client?.server ?? "???")")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.gray)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
@ -159,7 +285,7 @@ struct AccountView: View {
|
|||
.font(.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Text("\(client.server)")
|
||||
Text("\(client?.server ?? "???")")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.gray)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
@ -197,7 +323,7 @@ struct AccountView: View {
|
|||
}
|
||||
|
||||
var profilePicture: some View {
|
||||
OnlineImage(url: account.avatar)
|
||||
OnlineImage(url: account.avatar, size: biggerPicture ? 300 : 50, useNuke: true)
|
||||
.clipShape(.circle)
|
||||
.matchedGeometryEffect(id: animPicture, in: accountAnims)
|
||||
.onTapGesture {
|
||||
|
|
|
@ -10,6 +10,12 @@ struct ConnectView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image("HeroIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("login.title")
|
||||
.font(.title.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
|
@ -30,6 +36,7 @@ struct ConnectView: View {
|
|||
noAccount
|
||||
}
|
||||
.buttonStyle(LargeButton())
|
||||
.disabled(true)
|
||||
}
|
||||
.padding(.vertical, 100)
|
||||
}
|
||||
|
|
|
@ -5,14 +5,13 @@ import SwiftUI
|
|||
struct ContentView: View {
|
||||
@State private var navigator = Navigator()
|
||||
@State private var sheet: SheetDestination?
|
||||
@State private var client: Client?
|
||||
@State private var currentAccount: Account?
|
||||
@State private var accountManager: AccountManager = AccountManager()
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $navigator.selectedTab, content: {
|
||||
ZStack {
|
||||
if client != nil {
|
||||
TimelineView(timelineModel: FetchTimeline(client: self.client!))
|
||||
if accountManager.getClient() != nil {
|
||||
TimelineView(navigator: navigator, timelineModel: FetchTimeline(client: accountManager.forceClient()))
|
||||
.background(Color.appBackground)
|
||||
.safeAreaPadding()
|
||||
} else {
|
||||
|
@ -33,9 +32,10 @@ struct ContentView: View {
|
|||
.background(Color.appBackground)
|
||||
.tag(TabDestination.activity)
|
||||
|
||||
ProfileView(account: currentAccount ?? .placeholder())
|
||||
AccountView(isCurrent: true, account: accountManager.getAccount() ?? .placeholder())
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.profile)
|
||||
|
||||
})
|
||||
.overlay(alignment: .bottom) {
|
||||
TabsView(navigator: navigator)
|
||||
|
@ -43,17 +43,22 @@ struct ContentView: View {
|
|||
.zIndex(10)
|
||||
}
|
||||
.withCovers(sheetDestination: $sheet)
|
||||
.withSheets(sheetDestination: $navigator.presentedSheet)
|
||||
.environment(accountManager)
|
||||
.environment(navigator)
|
||||
.environment(client)
|
||||
.onAppear {
|
||||
.task {
|
||||
await recognizeAccount()
|
||||
}
|
||||
}
|
||||
|
||||
func recognizeAccount() async {
|
||||
let acc = try? AppAccount.loadAsCurrent()
|
||||
if acc == nil {
|
||||
sheet = .welcome
|
||||
} else {
|
||||
Task {
|
||||
client = .init(server: acc!.server, oauthToken: acc!.oauthToken)
|
||||
currentAccount = try? await client!.get(endpoint: Accounts.verifyCredentials)
|
||||
}
|
||||
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)]
|
||||
|
||||
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)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackground)
|
||||
.navigationTitle("about")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
@ -29,6 +31,8 @@ struct AboutView: View {
|
|||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackground)
|
||||
.navigationTitle("about.app")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(Navigator.self) private var navigator: Navigator
|
||||
@State var navigator: Navigator
|
||||
@State private var sheet: SheetDestination?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigator.path) {
|
||||
List {
|
||||
Button {
|
||||
navigator.navigate(to: .about)
|
||||
|
@ -14,6 +15,7 @@ struct SettingsView: View {
|
|||
Label("about", systemImage: "info.circle")
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
|
||||
Button {
|
||||
navigator.navigate(to: .privacy)
|
||||
|
@ -21,23 +23,31 @@ struct SettingsView: View {
|
|||
Label("setting.privacy", systemImage: "lock")
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
|
||||
Button {
|
||||
UserDefaults.standard.removeObject(forKey: AppAccount.saveKey)
|
||||
AppAccount.clear()
|
||||
sheet = .welcome
|
||||
} label: {
|
||||
Text("logout")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.tint(Color.red)
|
||||
.listRowSeparator(.visible, edges: .bottom)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
}
|
||||
.withAppRouter(navigator)
|
||||
.withCovers(sheetDestination: $sheet)
|
||||
.scrollContentBackground(.hidden)
|
||||
.tint(Color.white)
|
||||
.background(Color.appBackground)
|
||||
.listStyle(.inset)
|
||||
.navigationTitle("settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
SettingsView(navigator: .init())
|
||||
}
|
||||
|
|
|
@ -3,23 +3,55 @@
|
|||
import SwiftUI
|
||||
|
||||
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 var timelineModel: FetchTimeline
|
||||
@State var timelineModel: FetchTimeline // home timeline by default
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigator.path) {
|
||||
if statuses != nil {
|
||||
if !statuses!.isEmpty {
|
||||
ScrollView(showsIndicators: false) {
|
||||
Button {
|
||||
withAnimation(.easeInOut) {
|
||||
showPicker.toggle()
|
||||
}
|
||||
} 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
|
||||
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)
|
||||
.background(Color.appBackground)
|
||||
.withAppRouter()
|
||||
.withAppRouter(navigator)
|
||||
} else {
|
||||
ZStack {
|
||||
Color.appBackground
|
||||
|
@ -60,10 +99,12 @@ struct TimelineView: View {
|
|||
Color.appBackground
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
if let client = accountManager.getClient() {
|
||||
Task {
|
||||
statuses = try? await client.get(endpoint: Timelines.home(sinceId: nil, maxId: nil, minId: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
|
|
Loading…
Reference in New Issue