From bf77e2c4c1584718c66e1fb980ecaae31ba91195 Mon Sep 17 00:00:00 2001 From: Lumaa Date: Fri, 29 Dec 2023 11:17:37 +0100 Subject: [PATCH] first commit --- .gitignore | 91 +++ .../AuthenticationViewController.swift | 36 + .../AuthenticationViewController.xib | 22 + AuthService/Info.plist | 15 + LICENSE | 201 +++++ Threaded.xcodeproj/project.pbxproj | 739 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 14 + .../AccentColor.colorset/Contents.json | 11 + .../AppBackground.colorset/Contents.json | 38 + .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 39856 bytes .../AppIcon.appiconset/Contents.json | 14 + Threaded/Assets.xcassets/Contents.json | 6 + .../HeroIcon.imageset/Contents.json | 22 + .../HeroIcon.imageset/HeroIcon_black.png | Bin 0 -> 25815 bytes .../HeroIcon.imageset/HeroIcon_white.png | Bin 0 -> 49999 bytes .../Assets.xcassets/Mastodon/Contents.json | 6 + .../MastodonMark.imageset/Contents.json | 12 + .../Logo violet 1mastodon.png | Bin 0 -> 119330 bytes Threaded/Components/ButtonStyles.swift | 43 + Threaded/Components/CompactPostView.swift | 345 ++++++++ Threaded/Components/OnlineImage.swift | 23 + Threaded/Components/TabsNavs/TabsView.swift | 138 ++++ Threaded/Data/Accounts/Account+Elms.swift | 75 ++ Threaded/Data/Accounts/Account.swift | 127 +++ Threaded/Data/Accounts/AccountManager.swift | 101 +++ Threaded/Data/Accounts/TimelineFilter.swift | 340 ++++++++ Threaded/Data/AccountsList.swift | 20 + Threaded/Data/AppInfo.swift | 11 + Threaded/Data/Client.swift | 278 +++++++ Threaded/Data/Emoji.swift | 19 + Threaded/Data/FetchTimeline.swift | 42 + Threaded/Data/HTMLString.swift | 293 +++++++ Threaded/Data/HapticManager.swift | 67 ++ Threaded/Data/Instance.swift | 61 ++ Threaded/Data/MastodonRequest.swift | 521 ++++++++++++ Threaded/Data/Navigator.swift | 121 +++ Threaded/Data/ShareableImage.swift | 22 + Threaded/Data/Status.swift | 421 ++++++++++ Threaded/Data/Tag.swift | 69 ++ Threaded/Info.plist | 19 + Threaded/Localizable.xcstrings | 232 ++++++ Threaded/Packages/Models/.gitignore | 9 + .../xcshareddata/xcschemes/Models.xcscheme | 66 ++ .../xcschemes/ModelsTests.xcscheme | 53 ++ Threaded/Packages/Models/Package.swift | 36 + Threaded/Packages/Models/README.md | 3 + .../Models/Sources/Models/Account.swift | 125 +++ .../Models/Alias/DateFormatterCache.swift | 26 + .../Sources/Models/Alias/HTMLString.swift | 291 +++++++ .../Sources/Models/Alias/ServerDate.swift | 41 + .../Models/Sources/Models/App/App.swift | 12 + .../Models/Sources/Models/AppAccount.swift | 31 + .../Models/Sources/Models/Application.swift | 19 + .../Packages/Models/Sources/Models/Card.swift | 15 + .../Models/ConsolidatedNotification.swift | 45 ++ .../Models/Sources/Models/Conversation.swift | 26 + .../Models/Sources/Models/Emoji.swift | 17 + .../Models/Sources/Models/Filter.swift | 27 + .../Models/Sources/Models/Instance.swift | 47 ++ .../Models/Sources/Models/InstanceApp.swift | 13 + .../Sources/Models/InstanceSocial.swift | 16 + .../Models/Sources/Models/Language.swift | 23 + .../Packages/Models/Sources/Models/List.swift | 18 + .../Models/MastodonPushNotification.swift | 25 + .../Sources/Models/MediaAttachement.swift | 61 ++ .../Models/Sources/Models/Mention.swift | 10 + .../Models/Sources/Models/Notification.swift | 28 + .../Models/Sources/Models/OauthToken.swift | 8 + .../Packages/Models/Sources/Models/Poll.swift | 54 ++ .../Sources/Models/PushSubscription.swift | 20 + .../Models/Sources/Models/Relationship.swift | 54 ++ .../Models/Sources/Models/SearchResults.swift | 18 + .../Models/Sources/Models/ServerError.swift | 8 + .../Models/Sources/Models/ServerFilter.swift | 80 ++ .../Sources/Models/ServerPreferences.swift | 38 + .../Models/Sources/Models/Status.swift | 260 ++++++ .../Models/Sources/Models/StatusContext.swift | 12 + .../Models/Sources/Models/StatusHistory.swift | 13 + .../Sources/Models/Stream/StreamEvent.swift | 57 ++ .../Sources/Models/Stream/StreamMessage.swift | 11 + .../Sources/Models/SwiftData/Draft.swift | 13 + .../Models/SwiftData/LocalTimeline.swift | 13 + .../Sources/Models/SwiftData/TagGroup.swift | 17 + .../Packages/Models/Sources/Models/Tag.swift | 67 ++ .../Models/Sources/Models/Translation.swift | 15 + .../Tests/ModelsTests/HTMLStringTests.swift | 91 +++ .../Preview Assets.xcassets/Contents.json | 6 + Threaded/ThreadedApp.swift | 17 + Threaded/Views/AccountView.swift | 179 +++++ Threaded/Views/AddInstanceView.swift | 160 ++++ Threaded/Views/ConnectView.swift | 82 ++ Threaded/Views/ContentView.swift | 76 ++ Threaded/Views/ProfileView.swift | 240 ++++++ Threaded/Views/Settings/PrivacyView.swift | 13 + Threaded/Views/Settings/SettingsView.swift | 36 + Threaded/Views/TimelineView.swift | 52 ++ 98 files changed, 7423 insertions(+) create mode 100644 .gitignore create mode 100644 AuthService/AuthenticationViewController.swift create mode 100644 AuthService/Base.lproj/AuthenticationViewController.xib create mode 100644 AuthService/Info.plist create mode 100644 LICENSE create mode 100644 Threaded.xcodeproj/project.pbxproj create mode 100644 Threaded.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Threaded.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Threaded.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Threaded/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Threaded/Assets.xcassets/AppBackground.colorset/Contents.json create mode 100644 Threaded/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100644 Threaded/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Threaded/Assets.xcassets/Contents.json create mode 100644 Threaded/Assets.xcassets/HeroIcon.imageset/Contents.json create mode 100644 Threaded/Assets.xcassets/HeroIcon.imageset/HeroIcon_black.png create mode 100644 Threaded/Assets.xcassets/HeroIcon.imageset/HeroIcon_white.png create mode 100644 Threaded/Assets.xcassets/Mastodon/Contents.json create mode 100644 Threaded/Assets.xcassets/Mastodon/MastodonMark.imageset/Contents.json create mode 100644 Threaded/Assets.xcassets/Mastodon/MastodonMark.imageset/Logo violet 1mastodon.png create mode 100644 Threaded/Components/ButtonStyles.swift create mode 100644 Threaded/Components/CompactPostView.swift create mode 100644 Threaded/Components/OnlineImage.swift create mode 100644 Threaded/Components/TabsNavs/TabsView.swift create mode 100644 Threaded/Data/Accounts/Account+Elms.swift create mode 100644 Threaded/Data/Accounts/Account.swift create mode 100644 Threaded/Data/Accounts/AccountManager.swift create mode 100644 Threaded/Data/Accounts/TimelineFilter.swift create mode 100644 Threaded/Data/AccountsList.swift create mode 100644 Threaded/Data/AppInfo.swift create mode 100644 Threaded/Data/Client.swift create mode 100644 Threaded/Data/Emoji.swift create mode 100644 Threaded/Data/FetchTimeline.swift create mode 100644 Threaded/Data/HTMLString.swift create mode 100644 Threaded/Data/HapticManager.swift create mode 100644 Threaded/Data/Instance.swift create mode 100644 Threaded/Data/MastodonRequest.swift create mode 100644 Threaded/Data/Navigator.swift create mode 100644 Threaded/Data/ShareableImage.swift create mode 100644 Threaded/Data/Status.swift create mode 100644 Threaded/Data/Tag.swift create mode 100644 Threaded/Info.plist create mode 100644 Threaded/Localizable.xcstrings create mode 100644 Threaded/Packages/Models/.gitignore create mode 100644 Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme create mode 100644 Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/ModelsTests.xcscheme create mode 100644 Threaded/Packages/Models/Package.swift create mode 100644 Threaded/Packages/Models/README.md create mode 100644 Threaded/Packages/Models/Sources/Models/Account.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Alias/DateFormatterCache.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Alias/HTMLString.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Alias/ServerDate.swift create mode 100644 Threaded/Packages/Models/Sources/Models/App/App.swift create mode 100644 Threaded/Packages/Models/Sources/Models/AppAccount.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Application.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Card.swift create mode 100644 Threaded/Packages/Models/Sources/Models/ConsolidatedNotification.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Conversation.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Emoji.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Filter.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Instance.swift create mode 100644 Threaded/Packages/Models/Sources/Models/InstanceApp.swift create mode 100644 Threaded/Packages/Models/Sources/Models/InstanceSocial.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Language.swift create mode 100644 Threaded/Packages/Models/Sources/Models/List.swift create mode 100644 Threaded/Packages/Models/Sources/Models/MastodonPushNotification.swift create mode 100644 Threaded/Packages/Models/Sources/Models/MediaAttachement.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Mention.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Notification.swift create mode 100644 Threaded/Packages/Models/Sources/Models/OauthToken.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Poll.swift create mode 100644 Threaded/Packages/Models/Sources/Models/PushSubscription.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Relationship.swift create mode 100644 Threaded/Packages/Models/Sources/Models/SearchResults.swift create mode 100644 Threaded/Packages/Models/Sources/Models/ServerError.swift create mode 100644 Threaded/Packages/Models/Sources/Models/ServerFilter.swift create mode 100644 Threaded/Packages/Models/Sources/Models/ServerPreferences.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Status.swift create mode 100644 Threaded/Packages/Models/Sources/Models/StatusContext.swift create mode 100644 Threaded/Packages/Models/Sources/Models/StatusHistory.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Stream/StreamEvent.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Stream/StreamMessage.swift create mode 100644 Threaded/Packages/Models/Sources/Models/SwiftData/Draft.swift create mode 100644 Threaded/Packages/Models/Sources/Models/SwiftData/LocalTimeline.swift create mode 100644 Threaded/Packages/Models/Sources/Models/SwiftData/TagGroup.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Tag.swift create mode 100644 Threaded/Packages/Models/Sources/Models/Translation.swift create mode 100644 Threaded/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift create mode 100644 Threaded/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Threaded/ThreadedApp.swift create mode 100644 Threaded/Views/AccountView.swift create mode 100644 Threaded/Views/AddInstanceView.swift create mode 100644 Threaded/Views/ConnectView.swift create mode 100644 Threaded/Views/ContentView.swift create mode 100644 Threaded/Views/ProfileView.swift create mode 100644 Threaded/Views/Settings/PrivacyView.swift create mode 100644 Threaded/Views/Settings/SettingsView.swift create mode 100644 Threaded/Views/TimelineView.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b2c072 --- /dev/null +++ b/.gitignore @@ -0,0 +1,91 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ +.DS_Store \ No newline at end of file diff --git a/AuthService/AuthenticationViewController.swift b/AuthService/AuthenticationViewController.swift new file mode 100644 index 0000000..86376e9 --- /dev/null +++ b/AuthService/AuthenticationViewController.swift @@ -0,0 +1,36 @@ +//Made by Lumaa + +import UIKit +import AuthenticationServices + +class AuthenticationViewController: UIViewController { + + var authorizationRequest: ASAuthorizationProviderExtensionAuthorizationRequest? + + override func loadView() { + super.loadView() + // Do any additional setup after loading the view. + } + + override var nibName: String? { + return "AuthenticationViewController" + } +} + +extension AuthenticationViewController: ASAuthorizationProviderExtensionAuthorizationRequestHandler { + + public func beginAuthorization(with request: ASAuthorizationProviderExtensionAuthorizationRequest) { + self.authorizationRequest = request + + // Call this to indicate immediate authorization succeeded. + let authorizationHeaders = [String: String]() // TODO: Fill in appropriate authorization headers. + request.complete(httpAuthorizationHeaders: authorizationHeaders) + + // Or present authorization view and call self.authorizationRequest.complete() later after handling interactive authorization. + // request.presentAuthorizationViewController(completion: { (success, error) in + // if error != nil { + // request.complete(error: error!) + // } + // }) + } +} diff --git a/AuthService/Base.lproj/AuthenticationViewController.xib b/AuthService/Base.lproj/AuthenticationViewController.xib new file mode 100644 index 0000000..dd4c042 --- /dev/null +++ b/AuthService/Base.lproj/AuthenticationViewController.xib @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/AuthService/Info.plist b/AuthService/Info.plist new file mode 100644 index 0000000..23103cb --- /dev/null +++ b/AuthService/Info.plist @@ -0,0 +1,15 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionPointIdentifier + com.apple.AppSSO.idp-extension + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).AuthenticationViewController + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b584c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Lumaa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Threaded.xcodeproj/project.pbxproj b/Threaded.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c6808c5 --- /dev/null +++ b/Threaded.xcodeproj/project.pbxproj @@ -0,0 +1,739 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 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 */; }; + B97BCE2A2B3ED2C80044756D /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = B97BCE292B3ED2C80044756D /* LICENSE */; }; + B9842C0E2B2F21B700D9F3C1 /* CompactPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C0D2B2F21B700D9F3C1 /* CompactPostView.swift */; }; + B9842C102B2F228C00D9F3C1 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C0F2B2F228C00D9F3C1 /* Status.swift */; }; + B9842C122B2F2A5800D9F3C1 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C112B2F2A5800D9F3C1 /* TimelineView.swift */; }; + 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 */; }; + B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */; }; + B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945C2B2DEECE00D81C07 /* ContentView.swift */; }; + B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9FB94602B2DEECF00D81C07 /* Assets.xcassets */; }; + B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9FB94632B2DEECF00D81C07 /* Preview Assets.xcassets */; }; + B9FB94702B2DF3CD00D81C07 /* Navigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB946F2B2DF3CD00D81C07 /* Navigator.swift */; }; + B9FB94722B2DF49700D81C07 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94712B2DF49700D81C07 /* ConnectView.swift */; }; + B9FB94742B2DF6A100D81C07 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94732B2DF6A100D81C07 /* ButtonStyles.swift */; }; + B9FB94762B2E023D00D81C07 /* TabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94752B2E023D00D81C07 /* TabsView.swift */; }; + B9FB947D2B2E19E300D81C07 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB947C2B2E19E300D81C07 /* AccountManager.swift */; }; + B9FB947F2B2E1D5F00D81C07 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB947E2B2E1D5F00D81C07 /* Account.swift */; }; + B9FB94812B2E1FEF00D81C07 /* HTMLString.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94802B2E1FEF00D81C07 /* HTMLString.swift */; }; + 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 */; }; + B9FB94922B2E35D000D81C07 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94912B2E35D000D81C07 /* SettingsView.swift */; }; + B9FB94972B2EDABF00D81C07 /* PrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94962B2EDABF00D81C07 /* PrivacyView.swift */; }; + B9FB94992B2EEB9400D81C07 /* AddInstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94982B2EEB9400D81C07 /* AddInstanceView.swift */; }; + B9FB949B2B2EF09A00D81C07 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB949A2B2EF09A00D81C07 /* Client.swift */; }; + B9FB949D2B2EF0D600D81C07 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB949C2B2EF0D600D81C07 /* Instance.swift */; }; + B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB949E2B2EF0F200D81C07 /* MastodonRequest.swift */; }; + B9FB94A22B2EF24A00D81C07 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94A12B2EF24A00D81C07 /* AppInfo.swift */; }; + B9FB94AA2B2F009F00D81C07 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9FB94A92B2F009F00D81C07 /* AuthenticationServices.framework */; }; + B9FB94AD2B2F009F00D81C07 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94AC2B2F009F00D81C07 /* AuthenticationViewController.swift */; }; + B9FB94B02B2F009F00D81C07 /* AuthenticationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B9FB94AE2B2F009F00D81C07 /* AuthenticationViewController.xib */; }; + B9FB94B42B2F009F00D81C07 /* ThreadedAuthService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B9FB94A72B2F009F00D81C07 /* ThreadedAuthService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + B9FB94BC2B2F035500D81C07 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94BB2B2F035500D81C07 /* Tag.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B9FB94B22B2F009F00D81C07 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B9FB944F2B2DEECE00D81C07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B9FB94A62B2F009F00D81C07; + remoteInfo = AuthService; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + B9FB94B82B2F009F00D81C07 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + B9FB94B42B2F009F00D81C07 /* ThreadedAuthService.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + B97BCE232B3DD8400044756D /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; + B97BCE252B3DE5A10044756D /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; + B97BCE272B3ED2A80044756D /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + B97BCE292B3ED2C80044756D /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + B9842C0D2B2F21B700D9F3C1 /* CompactPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactPostView.swift; sourceTree = ""; }; + B9842C0F2B2F228C00D9F3C1 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + B9842C112B2F2A5800D9F3C1 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; + B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTimeline.swift; sourceTree = ""; }; + B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilter.swift; sourceTree = ""; }; + B9842C172B2F36F500D9F3C1 /* AccountsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsList.swift; sourceTree = ""; }; + B9FB94572B2DEECE00D81C07 /* Threaded.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Threaded.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedApp.swift; sourceTree = ""; }; + B9FB945C2B2DEECE00D81C07 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B9FB94602B2DEECF00D81C07 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B9FB94632B2DEECF00D81C07 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + B9FB946F2B2DF3CD00D81C07 /* Navigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigator.swift; sourceTree = ""; }; + B9FB94712B2DF49700D81C07 /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = ""; }; + B9FB94732B2DF6A100D81C07 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; + B9FB94752B2E023D00D81C07 /* TabsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsView.swift; sourceTree = ""; }; + B9FB947C2B2E19E300D81C07 /* AccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; + B9FB947E2B2E1D5F00D81C07 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; + B9FB94802B2E1FEF00D81C07 /* HTMLString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLString.swift; sourceTree = ""; }; + B9FB94852B2E211200D81C07 /* Account+Elms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Elms.swift"; sourceTree = ""; }; + B9FB94872B2E223E00D81C07 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; + B9FB94892B2E227000D81C07 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + B9FB948B2B2E232300D81C07 /* OnlineImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineImage.swift; sourceTree = ""; }; + B9FB948D2B2E28E800D81C07 /* ShareableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareableImage.swift; sourceTree = ""; }; + B9FB948F2B2E2B0E00D81C07 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + B9FB94912B2E35D000D81C07 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + B9FB94962B2EDABF00D81C07 /* PrivacyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyView.swift; sourceTree = ""; }; + B9FB94982B2EEB9400D81C07 /* AddInstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddInstanceView.swift; sourceTree = ""; }; + B9FB949A2B2EF09A00D81C07 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; + B9FB949C2B2EF0D600D81C07 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; + B9FB949E2B2EF0F200D81C07 /* MastodonRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRequest.swift; sourceTree = ""; }; + B9FB94A02B2EF23100D81C07 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + B9FB94A12B2EF24A00D81C07 /* AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfo.swift; sourceTree = ""; }; + B9FB94A72B2F009F00D81C07 /* ThreadedAuthService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ThreadedAuthService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + B9FB94A92B2F009F00D81C07 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; + B9FB94AC2B2F009F00D81C07 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; + B9FB94AF2B2F009F00D81C07 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AuthenticationViewController.xib; sourceTree = ""; }; + B9FB94B12B2F009F00D81C07 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B9FB94BB2B2F035500D81C07 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B9FB94542B2DEECE00D81C07 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B9FB94842B2E20AF00D81C07 /* SwiftSoup in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B9FB94A42B2F009F00D81C07 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B9FB94AA2B2F009F00D81C07 /* AuthenticationServices.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B9FB944E2B2DEECE00D81C07 = { + isa = PBXGroup; + children = ( + B97BCE292B3ED2C80044756D /* LICENSE */, + B97BCE272B3ED2A80044756D /* .gitignore */, + B9FB94592B2DEECE00D81C07 /* Threaded */, + B9FB94AB2B2F009F00D81C07 /* AuthService */, + B9FB94A82B2F009F00D81C07 /* Frameworks */, + B9FB94582B2DEECE00D81C07 /* Products */, + ); + sourceTree = ""; + }; + B9FB94582B2DEECE00D81C07 /* Products */ = { + isa = PBXGroup; + children = ( + B9FB94572B2DEECE00D81C07 /* Threaded.app */, + B9FB94A72B2F009F00D81C07 /* ThreadedAuthService.appex */, + ); + name = Products; + sourceTree = ""; + }; + B9FB94592B2DEECE00D81C07 /* Threaded */ = { + isa = PBXGroup; + children = ( + B9FB94A02B2EF23100D81C07 /* Info.plist */, + B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */, + B9FB946E2B2DF3BB00D81C07 /* Components */, + B9FB946D2B2DF3B800D81C07 /* Views */, + B9FB946C2B2DF3A600D81C07 /* Data */, + B9FB94602B2DEECF00D81C07 /* Assets.xcassets */, + B9FB94622B2DEECF00D81C07 /* Preview Content */, + B9FB948F2B2E2B0E00D81C07 /* Localizable.xcstrings */, + ); + path = Threaded; + sourceTree = ""; + }; + B9FB94622B2DEECF00D81C07 /* Preview Content */ = { + isa = PBXGroup; + children = ( + B9FB94632B2DEECF00D81C07 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + B9FB946C2B2DF3A600D81C07 /* Data */ = { + isa = PBXGroup; + children = ( + B9FB94BD2B2F038D00D81C07 /* Accounts */, + B9FB946F2B2DF3CD00D81C07 /* Navigator.swift */, + B9FB94BB2B2F035500D81C07 /* Tag.swift */, + B9FB94802B2E1FEF00D81C07 /* HTMLString.swift */, + B9FB94872B2E223E00D81C07 /* Emoji.swift */, + B9FB948D2B2E28E800D81C07 /* ShareableImage.swift */, + B9FB949A2B2EF09A00D81C07 /* Client.swift */, + B9FB949C2B2EF0D600D81C07 /* Instance.swift */, + B9FB949E2B2EF0F200D81C07 /* MastodonRequest.swift */, + B9FB94A12B2EF24A00D81C07 /* AppInfo.swift */, + B9842C0F2B2F228C00D9F3C1 /* Status.swift */, + B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */, + B9842C172B2F36F500D9F3C1 /* AccountsList.swift */, + B97BCE232B3DD8400044756D /* HapticManager.swift */, + ); + path = Data; + sourceTree = ""; + }; + B9FB946D2B2DF3B800D81C07 /* Views */ = { + isa = PBXGroup; + children = ( + B9FB94952B2EDAB600D81C07 /* Settings */, + B9FB94712B2DF49700D81C07 /* ConnectView.swift */, + B9FB94892B2E227000D81C07 /* ProfileView.swift */, + B9FB94982B2EEB9400D81C07 /* AddInstanceView.swift */, + B9FB945C2B2DEECE00D81C07 /* ContentView.swift */, + B9842C112B2F2A5800D9F3C1 /* TimelineView.swift */, + B97BCE252B3DE5A10044756D /* AccountView.swift */, + ); + path = Views; + sourceTree = ""; + }; + B9FB946E2B2DF3BB00D81C07 /* Components */ = { + isa = PBXGroup; + children = ( + B9FB94792B2E137100D81C07 /* TabsNavs */, + B9FB94732B2DF6A100D81C07 /* ButtonStyles.swift */, + B9FB948B2B2E232300D81C07 /* OnlineImage.swift */, + B9842C0D2B2F21B700D9F3C1 /* CompactPostView.swift */, + ); + path = Components; + sourceTree = ""; + }; + B9FB94792B2E137100D81C07 /* TabsNavs */ = { + isa = PBXGroup; + children = ( + B9FB94752B2E023D00D81C07 /* TabsView.swift */, + ); + path = TabsNavs; + sourceTree = ""; + }; + B9FB94952B2EDAB600D81C07 /* Settings */ = { + isa = PBXGroup; + children = ( + B9FB94912B2E35D000D81C07 /* SettingsView.swift */, + B9FB94962B2EDABF00D81C07 /* PrivacyView.swift */, + ); + path = Settings; + sourceTree = ""; + }; + B9FB94A82B2F009F00D81C07 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B9FB94A92B2F009F00D81C07 /* AuthenticationServices.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B9FB94AB2B2F009F00D81C07 /* AuthService */ = { + isa = PBXGroup; + children = ( + B9FB94AC2B2F009F00D81C07 /* AuthenticationViewController.swift */, + B9FB94AE2B2F009F00D81C07 /* AuthenticationViewController.xib */, + B9FB94B12B2F009F00D81C07 /* Info.plist */, + ); + path = AuthService; + sourceTree = ""; + }; + B9FB94BD2B2F038D00D81C07 /* Accounts */ = { + isa = PBXGroup; + children = ( + B9FB947C2B2E19E300D81C07 /* AccountManager.swift */, + B9FB947E2B2E1D5F00D81C07 /* Account.swift */, + B9FB94852B2E211200D81C07 /* Account+Elms.swift */, + B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */, + ); + path = Accounts; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B9FB94562B2DEECE00D81C07 /* Threaded */ = { + isa = PBXNativeTarget; + buildConfigurationList = B9FB94672B2DEECF00D81C07 /* Build configuration list for PBXNativeTarget "Threaded" */; + buildPhases = ( + B9FB94532B2DEECE00D81C07 /* Sources */, + B9FB94542B2DEECE00D81C07 /* Frameworks */, + B9FB94552B2DEECE00D81C07 /* Resources */, + B9FB94B82B2F009F00D81C07 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + B9FB94B32B2F009F00D81C07 /* PBXTargetDependency */, + ); + name = Threaded; + packageProductDependencies = ( + B9FB94832B2E20AF00D81C07 /* SwiftSoup */, + ); + productName = Threaded; + productReference = B9FB94572B2DEECE00D81C07 /* Threaded.app */; + productType = "com.apple.product-type.application"; + }; + B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */ = { + isa = PBXNativeTarget; + buildConfigurationList = B9FB94B52B2F009F00D81C07 /* Build configuration list for PBXNativeTarget "ThreadedAuthService" */; + buildPhases = ( + B9FB94A32B2F009F00D81C07 /* Sources */, + B9FB94A42B2F009F00D81C07 /* Frameworks */, + B9FB94A52B2F009F00D81C07 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ThreadedAuthService; + productName = AuthService; + productReference = B9FB94A72B2F009F00D81C07 /* ThreadedAuthService.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B9FB944F2B2DEECE00D81C07 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + B9FB94562B2DEECE00D81C07 = { + CreatedOnToolsVersion = 15.1; + }; + B9FB94A62B2F009F00D81C07 = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = B9FB94522B2DEECE00D81C07 /* Build configuration list for PBXProject "Threaded" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B9FB944E2B2DEECE00D81C07; + packageReferences = ( + B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */, + ); + productRefGroup = B9FB94582B2DEECE00D81C07 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B9FB94562B2DEECE00D81C07 /* Threaded */, + B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B9FB94552B2DEECE00D81C07 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B97BCE282B3ED2A80044756D /* .gitignore in Resources */, + B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */, + B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */, + B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */, + B97BCE2A2B3ED2C80044756D /* LICENSE in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B9FB94A52B2F009F00D81C07 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9FB94B02B2F009F00D81C07 /* AuthenticationViewController.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B9FB94532B2DEECE00D81C07 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9FB94922B2E35D000D81C07 /* SettingsView.swift in Sources */, + B9FB94882B2E223E00D81C07 /* Emoji.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 */, + B9FB94722B2DF49700D81C07 /* ConnectView.swift in Sources */, + B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */, + B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */, + B9FB94BC2B2F035500D81C07 /* Tag.swift in Sources */, + B9FB94812B2E1FEF00D81C07 /* HTMLString.swift in Sources */, + B9FB947F2B2E1D5F00D81C07 /* Account.swift in Sources */, + B9842C122B2F2A5800D9F3C1 /* TimelineView.swift in Sources */, + B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */, + B9FB94742B2DF6A100D81C07 /* ButtonStyles.swift in Sources */, + B9FB94702B2DF3CD00D81C07 /* Navigator.swift in Sources */, + B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */, + B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */, + B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */, + B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */, + B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */, + B9FB94A22B2EF24A00D81C07 /* AppInfo.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B9FB94A32B2F009F00D81C07 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9FB94AD2B2F009F00D81C07 /* AuthenticationViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B9FB94B32B2F009F00D81C07 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */; + targetProxy = B9FB94B22B2F009F00D81C07 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + B9FB94AE2B2F009F00D81C07 /* AuthenticationViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + B9FB94AF2B2F009F00D81C07 /* Base */, + ); + name = AuthenticationViewController.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B9FB94652B2DEECF00D81C07 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B9FB94662B2DEECF00D81C07 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B9FB94682B2DEECF00D81C07 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\""; + DEVELOPMENT_TEAM = HB5P3BML86; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Threaded/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B9FB94692B2DEECF00D81C07 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\""; + DEVELOPMENT_TEAM = HB5P3BML86; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Threaded/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + B9FB94B62B2F009F00D81C07 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HB5P3BML86; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AuthService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AuthService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.AuthService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B9FB94B72B2F009F00D81C07 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HB5P3BML86; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AuthService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AuthService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.AuthService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B9FB94522B2DEECE00D81C07 /* Build configuration list for PBXProject "Threaded" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9FB94652B2DEECF00D81C07 /* Debug */, + B9FB94662B2DEECF00D81C07 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B9FB94672B2DEECF00D81C07 /* Build configuration list for PBXNativeTarget "Threaded" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9FB94682B2DEECF00D81C07 /* Debug */, + B9FB94692B2DEECF00D81C07 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B9FB94B52B2F009F00D81C07 /* Build configuration list for PBXNativeTarget "ThreadedAuthService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9FB94B62B2F009F00D81C07 /* Debug */, + B9FB94B72B2F009F00D81C07 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scinfu/SwiftSoup"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.6.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + B9FB94832B2E20AF00D81C07 /* SwiftSoup */ = { + isa = XCSwiftPackageProductDependency; + package = B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = B9FB944F2B2DEECE00D81C07 /* Project object */; +} diff --git a/Threaded.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Threaded.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Threaded.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Threaded.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Threaded.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Threaded.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Threaded.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Threaded.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..e55f384 --- /dev/null +++ b/Threaded.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup", + "state" : { + "revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6", + "version" : "2.6.1" + } + } + ], + "version" : 2 +} diff --git a/Threaded/Assets.xcassets/AccentColor.colorset/Contents.json b/Threaded/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Threaded/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Threaded/Assets.xcassets/AppBackground.colorset/Contents.json b/Threaded/Assets.xcassets/AppBackground.colorset/Contents.json new file mode 100644 index 0000000..fe808bf --- /dev/null +++ b/Threaded/Assets.xcassets/AppBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x10", + "green" : "0x10", + "red" : "0x10" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Threaded/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/Threaded/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..221a240d0118da14bf9b34daff4e5db03117d0c8 GIT binary patch literal 39856 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 _FB98M(b8-^Qqptgt`ZE-vT*FAks(_?I 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 cZ8(phK26R=W>W})b~i$p|ETJ18ROfjQ{`u literal 0 HcmV?d00001 diff --git a/Threaded/Assets.xcassets/HeroIcon.imageset/HeroIcon_white.png b/Threaded/Assets.xcassets/HeroIcon.imageset/HeroIcon_white.png new file mode 100644 index 0000000000000000000000000000000000000000..82731ae5fcde5954489b5844697f288a7ff099b0 GIT binary patch literal 49999 zcmYIwc|4Tg`~EZbL`-B!n8p;!R%A)G8X3u&v1fVHAZykvF;Nm_8I>k$C>d*H%Q90c zWD6m?GDY@%XMSh&`M!R=yzq~n^PF?v*L~gBeVut|WT3;zev}=8AWmHj+601_z^{zy zY%JhMTJL}A5QKnq(ORarQ)lY>eUF(x`?Hxg{`SA)Y~DOd4}XzRXp_TvrY3o(w7Sj>~e2xMg+lT^@HG+wFMnBF2a@zY?qa~yI##rRg#>Q2w z%(J!vCWPTxapWoFf4DAnTPzG(tWZW0euxZ;Xd8bw>Ulvj)6O{@^^+Zc1a1i{F_Gds z9XL0Xm#!Kpa~OVT+AHu_<1O%&x{B%GcWJz(sE+av3sYy!%p_?O?!S8V%H*v@YyQo* zw#5_LIowQIMV&6x2m~7%t@{VVLOL3a;np@bKt}{v^J)iS+h7vWm=CS&v*(*H;4k3u zISk)KBEm99B?%{yaD0N(8o?kj`X=s8MCvLATF%c${z8f+(=bX=$_^<4toP0$E98a`l)Vz+q2~9ya9Rzx{ zb50_Ay@$2#eOjx7N9d<;{@I&^@h}Mi!pTM}{oN=*x^7 zFE}^^8&ej#H{DmvpPZcB^7+5FwWXzJ2^|wHI5V@J!_7W}0i(A3i6(kY$(U!*6WF6@ z%mMwEqNC4PjSxis#DLct@i^`?tO%FcWv9B|hrhMYt8gT0NGy&~;!{#QVT6~_3eY_$ z%S&5+(k#-`%h|?)f`XQL zJvX0Td1ay+vgWRjo)NS;ydMq6oOyo2pvEAWi*tuH8`Rp`dI8=yK0cmBvetGwjZcFr z7@wn5Zs3xTXAjt)ZZ3pQVhrAWm3CpLUEHp{b$3LvH|bNzK!w6!?+Y0izG$%P-KvEb z^d_c>5G}=Ef!Z{}iV{L4kn7r1bv!FH7t^GMtvfiTmECE<^3B4^D%hvgccQ(Vk%0*c z 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
  • ++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 literal 0 HcmV?d00001 diff --git a/Threaded/Assets.xcassets/Mastodon/Contents.json b/Threaded/Assets.xcassets/Mastodon/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Threaded/Assets.xcassets/Mastodon/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Threaded/Assets.xcassets/Mastodon/MastodonMark.imageset/Contents.json b/Threaded/Assets.xcassets/Mastodon/MastodonMark.imageset/Contents.json new file mode 100644 index 0000000..068d4ba --- /dev/null +++ b/Threaded/Assets.xcassets/Mastodon/MastodonMark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Logo violet 1mastodon.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Threaded/Assets.xcassets/Mastodon/MastodonMark.imageset/Logo violet 1mastodon.png b/Threaded/Assets.xcassets/Mastodon/MastodonMark.imageset/Logo violet 1mastodon.png new file mode 100644 index 0000000000000000000000000000000000000000..99f3caf2db6437be26831b676dc57c5d12535515 GIT binary patch literal 119330 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 z zan|fs&2nX>Th29eyu|7jBAtc3rShl`A}V#$>v3tRLh@Dkxc{m~r_ulOh~*&4{PHfJw&49;9~iXU_7LS)ChrZH&3G*+|R}! z(t|TSq{?gb)5ju+#DXyz@OIki0Pt;JaUsLd45=rhoU@`|rbi7;p(!<@u>XdiFmIvOA~M_UMP87+u*|te!O<$F zGTmrucFn|(Squsb<#1?nOjH>!;1Ymdw#oNm4}yka2DO|ffm8MRuf4{-Q*H~=S@`&R zs6+dsk)}CTV@ds1b|9|=yU0Z8m(SRFXXMgkcFv1Zai%*jrHGtwR(>mUiq>c zp0d=UTmoqbSXT*Nl8?5>-Boa+Ar&+f-Qtk3ZtxoXX9b*p@r2t4BZh=}^_}`3twMAZ zhifqf77UJP(MgsKaw(zuylX|_#J6bSs-CzhS+KGc@@7m>WJRoac-|*uX-kYnDHCrk{fBhDDz9|Qu0Qb8&G-%P5T>jf}HU-MuGX+XX z;jde@zk+#!#`YiFd2{v_I8FvWqdXcMbEty{uaL})1?&7yBy25C&m0u!PckTkgueF# z{=CL^F4PJl3#`lQMFf&MA}B10i|?boKUC4_+~uQ6KXDb&0)Ji$B+}ro5m;+@ zh=uUnBIh(F>^pH5{|mzow6Z^_aKH_2`v((LNK}yH!ys=-7P(=N8oIZpmJ<{B!^K*3WA3}cOowRmZ99?;W^_&~oDBBtR5?y|UB-}GmrhD6WL zizOOZ(aMFRM;EeToX!PdDv~eg-YZ`caZ|fn*6wp8t$foUr9{i;FxQci_+xEmpKF`Z z7Ses~Y)k^?fI&}d`PJK1&dMx7&8xpuw{<)P;mMufz=IDL)?h@;4GzGQk5Dy5pPH0F zfA7&TF%^(0IO+$C_)W@#W<CyKiv#+Q9y&c~l3&cu%$ll?1 zx^lkrSjT7*&wtky2!4DTBQYt_s}%yiNCai4JQN5`f?Qif4ajeOtp$w2XC1Hlhg4kt z6{d67kpve?Vd=rMtED9PmR~|zz0~y%l$>A~(460+>0b+<}tRv{BJy3Qk?TK zQ;1=oUO#*Smnm0=)oJX4muJ_)mo`=|SA{%T5eXKRu}(2kU4?E${}d6aBWOWN8;Lp*q_)VuZ_E4W}_vMM}Vamjh&JP%+`H}fp7IjEISL;071sN$AcuwKR7vePCo($_^? zPVgxo+qk7n8+1FzPp2&BBfal{w}AY6a5s3d7|Xcx zdOY>#Vem(@h>i!lr8KJe-xrJHza))|`gKS_7ZuvArko;KSXg~H$sFAMvtUf~O= z@Auay{aOEb8RX2d6^dIpH?&Q1z5S zF0pqyP|s(9^S18HidU_Pc}-h=0Ua~f}foG-Te32x{U=XyfJ&#kW3(R?dn6vQ^4DwHi&vHCxiwdo3YkX zE_zLEezypaovy|x!MWm>J!}jbxO@5CasBrh#6j}szWkGmvwje(DKh);eEa86B$*^9 zvhE;h*`-8d{8Kac{MF7YnD9g4HQ)8aWQo#lZlXXd* zo7$HJkUN9ZcgFBd#V^9XaQSTOY1^7J>gO(Uw;inh0KUT^udj0%OA>lTs=RT)Tyh=HJYto9~)j%oW($Lj{d_XRtkqev}uIjARq`r9`oN>gT(xw z4fDbO#m0M!p**S+&En%nFg;qrU#EUk!Bf*ia8z7uOp6gu-p@|_J&|I*aBe%nO>-tW z^{$L^0T68O*C=?j^Zo{c8Wc}-ed|-%Hr+?X`f_()n%;2Q_p5F0CiUCQ18Kds87ozu zeF=@B*WtKnjv`Dw5FtV>vnp&%-DDl?F+U9av6&ze48R~>KV#;LF^mX8@2W2uC5>+{ zWAoOD;CV$OkI%j}DPs&UoY+#?b{ygbI}<;4;zfz zpgZY2JQth=WfA}EZ>e$XiMe-e?thq0NKSt$dx)jbWIxkgfDBNOZ}z89d#!NlpKj%B znRj<@(OhB8)a>cGqQ9iraTL1CIDzui<@-nzFfEl0sn&R*cTVSJyh}QKBp87?`mjw3 zIr_yvywFMB*o8&Qnizin@V)ikvRx%7ugHI?TR+Wd`-uB_*-^Z0-^K2c#ViDYCBaV- z&Xr7FsY1{+<%re*Tu+Dx1NapL-n{I96pJH800t`l{l`C3c@ofN0OLe??$K?g`t7ia ztOFwaRLJa-X8>atNmQo;0_Hd@ou?^(kv6UBC%d0cTo&YA9^*L`TsRfHIh!1u30}2p zH?n=jr`ozU7AD^oQx?b0ZX0bwx+S(R=OW?mwp-Pjs+1Bn+U$;#H@6*w-_){jA{Ie> z_im#;V^5Ab)dwAjJSD%=k2D@wYk$9!K=lV!@KPqh=&{ZOu&O|1WbhmopK1Pm>AjE4 zoHOJIoP32ohitG62EbvjOd1Oqz>&a{Srv3$?Lk9O*W(~~GaX~+=vu6K5%B5nHD9d3 z8$j?3)B@ub45iAGNO1VxK6gCS89Lf~+n84#L5^x5fFlx5J<|34+)7#-o({=@Qwt%H zu#YbDK0HP_6pl`(d$FRB+~u2&ZK&TCf3K3gFi}pN$I=B{o>Bgp(B3Dd9C^~6Zpj&MIQs-+%qT ztlR~Yf0(`=J-}=T;Q?M{r*8r8{TZv1Jn-FSZ!Q65YDK=tIqx(d6yoL{)-5v6$+0QN zBjuA#7!Rulmf^v+u+i-c|8U=+mfobV_bII zVZUfL_rw(*;w0#Q3X+}Wo?^oItyTFbWYfZoEo6=OD^~+Ahm$Ulv5L>Pez%_HoLZ1@ zT?b1jb=i2@no}j6`I?2Ry*Sd>14~c3TAB_-P1Il54+^)vLEzgfw#IQU{hV>3jcY>Q z>5bHlIPmNUcyqk>8T?@__G-iiko0lJ=;NEW6BMrt?8n>EYJyk!w`Bl={;lKcuGTR$ zb6Z;72;cuj{nuz(SD2Qc^k8Eo;Lq$?=rer1Z3intA3u*WU3)H0?Vt0N$JRV=Ify#5 z!IO2f#si2x$vCZGn~D15;7^7s;4Rmo;>zg(^viE6v}*mN6{;#_2$p9>6DsGqlfUHH zLjFd)dYwa65mfbj_mqmozKG;9aVINlPYbH5MYu3~$Li+J%>zzAEkgb*d@#Ce8g#I& z+EN#s9~X}AfhS0;*I)EwXGQh{`~b80tO+n)M}U`cQ+3pWZd=?dn%+83{fUxtIC2ha zqapeEb$77Uf<_)SG;w!$YWF)|w!8ctzHJ5Xc`L)q!#GYKtt7cw^Kj02Gqt1H*eU;j zI+Efa?uA|rsgeD|u@ylf`@}1P6$4|>0}k<)50G=$CM=aD)*Mr@c@R#sr&xoOOl14qHnl5({82xro=oGb?eRPRT-X%TW!OjfY)REb3w0Mw`Lmm z(pr4O5)euGe~7BT377Y7Al${7R1RM-XE_GK*8)LJ*WjW%*L0F z&tE<(#yV}%Iu53FDAXqFZo1Ua-MSHJ)E=8hw|1;$q_Qw^DB z0--b9$!|((qo69e)od|pVC3FWs|#Tr^g!sRFnuIunh#$~&(lY?lw*U_;P=!ORf{4c z6rvDX!ZqP^9DxT0mdCF#?%^LCrB1~Nhj=4A#J{h>mRAYcN=HRd*MH@Khz4blD;!{< zJ+X7$fNPjZ@a)6#djF9*9QYjk7PP}B+YKGd_$m^$Wj0n|h1reNcHT+szduk-)pis> z)?VYxku&*vn>9@_#wg_av$lzq5^L=PEN8O#^U?2k=Sz^(tq5b$J~R0f^B%Hkl7>qm zOhgjdlDvqr62RXS5#lBe)Or@HpX>i z&tVPVA#Xnq^fZM(cQK*l?jU<1Z|Bq1~>k`4gisBLNX{K2<+S&m;au41fD0m%!_)m zDz_ph*sYt5Nxn>1>kGeS|4a(y!zd27+(S_-+fv)RSk%FyB!qq1!Z503P02!n5YK

    + asMarkdown += "\n\n" + } + } else if node.nodeName() == "br" { + if asMarkdown.count > 0 { // ignore first opening
    + asMarkdown += "\n" + } + } else if node.nodeName() == "a" { + let href = try node.attr("href") + if href != "" { + if let url = URL(string: href), + let _ = Int(url.lastPathComponent) + { + statusesURLs.append(url) + } + } + asMarkdown += "[" + let start = asMarkdown.endIndex + // descend into this node now so we can wrap the + // inner part of the link in the right markup + for nn in node.getChildNodes() { + handleNode(node: nn) + } + let finish = asMarkdown.endIndex + + var linkRef = href + + // Try creating a URL from the string. If it fails, try URL encoding + // the string first. + var url = URL(string: href) + if url == nil { + url = URL(string: href, encodePath: true) + } + if let linkUrl = url { + linkRef = linkUrl.absoluteString + let displayString = asMarkdown[start ..< finish] + links.append(Link(linkUrl, displayString: String(displayString))) + } + + asMarkdown += "](" + asMarkdown += linkRef + asMarkdown += ")" + + return + } else if node.nodeName() == "#text" { + var txt = node.description + + if let underscore_regex, let main_regex { + // This is the markdown escaper + txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1") + txt = underscore_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1") + } + // Strip newlines and line separators - they should be being sent as
    s + asMarkdown += txt.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\u{2028}", with: "") + } else if node.nodeName() == "ul" { + // Unordered (bulleted) list + // SwiftUI's Text won't display these in an AttributedString, but we can at least improve the appearance + asMarkdown += "\n\n" + for nn in node.getChildNodes() { + asMarkdown += "- " + handleNode(node: nn) + asMarkdown += "\n" + } + return + } else if node.nodeName() == "ol" { + // Ordered (numbered) list + // Same thing, won't display in a Text, but this is just an attempt to improve the appearance + asMarkdown += "\n\n" + var curNumber = 1 + for nn in node.getChildNodes() { + asMarkdown += "\(curNumber). " + handleNode(node: nn) + asMarkdown += "\n" + curNumber += 1 + } + return + } + + for n in node.getChildNodes() { + handleNode(node: n) + } + } catch {} + } + + public struct Link: Codable, Hashable, Identifiable { + public var id: Int { hashValue } + public let url: URL + public let displayString: String + public let type: LinkType + public let title: String + + init(_ url: URL, displayString: String) { + self.url = url + self.displayString = displayString + + switch displayString.first { + case "@": + type = .mention + title = displayString + case "#": + type = .hashtag + title = String(displayString.dropFirst()) + default: + type = .url + var hostNameUrl = url.host ?? url.absoluteString + if hostNameUrl.hasPrefix("www.") { + hostNameUrl = String(hostNameUrl.dropFirst(4)) + } + title = hostNameUrl + } + } + + public enum LinkType: String, Codable { + case url + case mention + case hashtag + } + } +} + +public extension URL { + // It's common to use non-ASCII characters in URLs even though they're technically + // invalid characters. Every modern browser handles this by silently encoding + // the invalid characters on the user's behalf. However, trying to create a URL + // object with un-encoded characters will result in nil so we need to encode the + // invalid characters before creating the URL object. The unencoded version + // should still be shown in the displayed status. + init?(string: String, encodePath: Bool) { + var encodedUrlString = "" + if encodePath, + string.starts(with: "http://") || string.starts(with: "https://"), + var startIndex = string.firstIndex(of: "/") + { + startIndex = string.index(startIndex, offsetBy: 1) + + // We don't want to encode the host portion of the URL + if var startIndex = string[startIndex...].firstIndex(of: "/") { + encodedUrlString = String(string[...startIndex]) + while let endIndex = string[string.index(after: startIndex)...].firstIndex(of: "/") { + let componentStartIndex = string.index(after: startIndex) + encodedUrlString = encodedUrlString + (string[componentStartIndex ... endIndex].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") + startIndex = endIndex + } + + // The last part of the path may have a query string appended to it + let componentStartIndex = string.index(after: startIndex) + if let queryStartIndex = string[componentStartIndex...].firstIndex(of: "?") { + encodedUrlString = encodedUrlString + (string[componentStartIndex ..< queryStartIndex].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") + encodedUrlString = encodedUrlString + (string[queryStartIndex...].addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") + } else { + encodedUrlString = encodedUrlString + (string[componentStartIndex...].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") + } + } + } + if encodedUrlString.isEmpty { + encodedUrlString = string + } + self.init(string: encodedUrlString) + } +} diff --git a/Threaded/Data/HapticManager.swift b/Threaded/Data/HapticManager.swift new file mode 100644 index 0000000..e58a138 --- /dev/null +++ b/Threaded/Data/HapticManager.swift @@ -0,0 +1,67 @@ +//Made by Lumaa + +import Foundation +import CoreHaptics + +struct Haptic: Hashable { + var intensity: CGFloat + var sharpness: CGFloat + var interval: CGFloat + + static let tap: [Haptic] = [Haptic(intensity: 0.5, sharpness: 0.8, interval: 0.0)] + static let error: [Haptic] = [ + Haptic(intensity: 1.0, sharpness: 0.7, interval: 0.0), + Haptic(intensity: 1.0, sharpness: 0.3, interval: 0.2) + ] +} + +class HapticManager { + private static var supportsHaptics: Bool = false + private static var engine: CHHapticEngine? + + static func playHaptics(haptics: [Haptic]) { + guard supportsHaptics else { return } + var events = [CHHapticEvent]() + let hapticIntensity: [CGFloat] = haptics.map { $0.intensity } + let hapticSharpness: [CGFloat] = haptics.map { $0.sharpness } + let intervals: [CGFloat] = haptics.map({ $0.interval }) + + for index in 0.. String + func queryItems() -> [URLQueryItem]? + var jsonValue: Encodable? { get } +} + +public extension Endpoint { + var jsonValue: Encodable? { + nil + } +} + +extension Endpoint { + func makePaginationParam(sinceId: String?, maxId: String?, mindId: String?) -> [URLQueryItem]? { + if let sinceId { + return [.init(name: "since_id", value: sinceId)] + } else if let maxId { + return [.init(name: "max_id", value: maxId)] + } else if let mindId { + return [.init(name: "min_id", value: mindId)] + } + return nil + } +} + +public struct LinkHandler { + public let rawLink: String + + public var maxId: String? { + do { + let regex = try Regex("max_id=[0-9]+") + if let match = rawLink.firstMatch(of: regex) { + return match.output.first?.substring?.replacingOccurrences(of: "max_id=", with: "") + } + } catch { + return nil + } + return nil + } +} + +extension LinkHandler: Sendable {} + +public struct ServerError: Decodable, Error { + public let error: String? + public var httpCode: Int? +} + +extension ServerError: Sendable {} + +public enum Apps: Endpoint { + case registerApp + + public func path() -> String { + switch self { + case .registerApp: + "apps" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case .registerApp: + return [ + .init(name: "client_name", value: AppInfo.clientName), + .init(name: "redirect_uris", value: AppInfo.scheme), + .init(name: "scopes", value: AppInfo.scopes), + .init(name: "website", value: AppInfo.website), + ] + } + } +} + +public enum Instances: Endpoint { + case instance + case peers + + public func path() -> String { + switch self { + case .instance: + "instance" + case .peers: + "instance/peers" + } + } + + public func queryItems() -> [URLQueryItem]? { + nil + } +} + +public enum Accounts: Endpoint { + case accounts(id: String) + case lookup(name: String) + case favorites(sinceId: String?) + case bookmarks(sinceId: String?) + case followedTags + case featuredTags(id: String) + case verifyCredentials + case updateCredentials(json: UpdateCredentialsData) + case statuses(id: String, + sinceId: String?, + tag: String?, + onlyMedia: Bool?, + excludeReplies: Bool?, + pinned: Bool?) + case relationships(ids: [String]) + case follow(id: String, notify: Bool, reblogs: Bool) + case unfollow(id: String) + case familiarFollowers(withAccount: String) + case suggestions + case followers(id: String, maxId: String?) + case following(id: String, maxId: String?) + case lists(id: String) + case preferences + case block(id: String) + case unblock(id: String) + case mute(id: String, json: MuteData) + case unmute(id: String) + case relationshipNote(id: String, json: RelationshipNoteData) + + public func path() -> String { + switch self { + case let .accounts(id): + "accounts/\(id)" + case .lookup: + "accounts/lookup" + case .favorites: + "favourites" + case .bookmarks: + "bookmarks" + case .followedTags: + "followed_tags" + case let .featuredTags(id): + "accounts/\(id)/featured_tags" + case .verifyCredentials: + "accounts/verify_credentials" + case .updateCredentials: + "accounts/update_credentials" + case let .statuses(id, _, _, _, _, _): + "accounts/\(id)/statuses" + case .relationships: + "accounts/relationships" + case let .follow(id, _, _): + "accounts/\(id)/follow" + case let .unfollow(id): + "accounts/\(id)/unfollow" + case .familiarFollowers: + "accounts/familiar_followers" + case .suggestions: + "suggestions" + case let .following(id, _): + "accounts/\(id)/following" + case let .followers(id, _): + "accounts/\(id)/followers" + case let .lists(id): + "accounts/\(id)/lists" + case .preferences: + "preferences" + case let .block(id): + "accounts/\(id)/block" + case let .unblock(id): + "accounts/\(id)/unblock" + case let .mute(id, _): + "accounts/\(id)/mute" + case let .unmute(id): + "accounts/\(id)/unmute" + case let .relationshipNote(id, _): + "accounts/\(id)/note" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case let .lookup(name): + return [ + .init(name: "acct", value: name), + ] + case let .statuses(_, sinceId, tag, onlyMedia, excludeReplies, pinned): + var params: [URLQueryItem] = [] + if let tag { + params.append(.init(name: "tagged", value: tag)) + } + if let sinceId { + params.append(.init(name: "max_id", value: sinceId)) + } + if let onlyMedia { + params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false")) + } + if let excludeReplies { + params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false")) + } + if let pinned { + params.append(.init(name: "pinned", value: pinned ? "true" : "false")) + } + return params + case let .relationships(ids): + return ids.map { + URLQueryItem(name: "id[]", value: $0) + } + case let .follow(_, notify, reblogs): + return [ + .init(name: "notify", value: notify ? "true" : "false"), + .init(name: "reblogs", value: reblogs ? "true" : "false"), + ] + case let .familiarFollowers(withAccount): + return [.init(name: "id[]", value: withAccount)] + case let .followers(_, maxId): + return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) + case let .following(_, maxId): + return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) + case let .favorites(sinceId): + guard let sinceId else { return nil } + return [.init(name: "max_id", value: sinceId)] + case let .bookmarks(sinceId): + guard let sinceId else { return nil } + return [.init(name: "max_id", value: sinceId)] + default: + return nil + } + } + + public var jsonValue: Encodable? { + switch self { + case let .mute(_, json): + json + case let .relationshipNote(_, json): + json + case let .updateCredentials(json): + json + default: + nil + } + } +} + +public struct MuteData: Encodable, Sendable { + public let duration: Int + + public init(duration: Int) { + self.duration = duration + } +} + +public struct RelationshipNoteData: Encodable, Sendable { + public let comment: String + + public init(note comment: String) { + self.comment = comment + } +} + +public struct UpdateCredentialsData: Encodable, Sendable { + public struct SourceData: Encodable, Sendable { + public let privacy: Visibility + public let sensitive: Bool + + public init(privacy: Visibility, sensitive: Bool) { + self.privacy = privacy + self.sensitive = sensitive + } + } + + public struct FieldData: Encodable, Sendable { + public let name: String + public let value: String + + public init(name: String, value: String) { + self.name = name + self.value = value + } + } + + public let displayName: String + public let note: String + public let source: SourceData + public let bot: Bool + public let locked: Bool + public let discoverable: Bool + public let fieldsAttributes: [String: FieldData] + + public init(displayName: String, + note: String, + source: UpdateCredentialsData.SourceData, + bot: Bool, + locked: Bool, + discoverable: Bool, + fieldsAttributes: [FieldData]) + { + self.displayName = displayName + self.note = note + self.source = source + self.bot = bot + self.locked = locked + self.discoverable = discoverable + + var fieldAttributes: [String: FieldData] = [:] + for (index, field) in fieldsAttributes.enumerated() { + fieldAttributes[String(index)] = field + } + self.fieldsAttributes = fieldAttributes + } +} + +public enum Timelines: Endpoint { + case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool) + case home(sinceId: String?, maxId: String?, minId: String?) + case list(listId: String, sinceId: String?, maxId: String?, minId: String?) + case hashtag(tag: String, additional: [String]?, maxId: String?) + + public func path() -> String { + switch self { + case .pub: + "timelines/public" + case .home: + "timelines/home" + case let .list(listId, _, _, _): + "timelines/list/\(listId)" + case let .hashtag(tag, _, _): + "timelines/tag/\(tag)" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case let .pub(sinceId, maxId, minId, local): + var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: minId) ?? [] + params.append(.init(name: "local", value: local ? "true" : "false")) + return params + case let .home(sinceId, maxId, mindId): + return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) + case let .list(_, sinceId, maxId, mindId): + return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) + case let .hashtag(_, additional, maxId): + var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) ?? [] + params.append(contentsOf: (additional ?? []) + .map { URLQueryItem(name: "any[]", value: $0) }) + return params + } + } +} + +public enum Statuses: Endpoint { + case postStatus(json: StatusData) + case editStatus(id: String, json: StatusData) + case status(id: String) + case context(id: String) + case favorite(id: String) + case unfavorite(id: String) + case reblog(id: String) + case unreblog(id: String) + case rebloggedBy(id: String, maxId: String?) + case favoritedBy(id: String, maxId: String?) + case pin(id: String) + case unpin(id: String) + case bookmark(id: String) + case unbookmark(id: String) + case history(id: String) + case translate(id: String, lang: String?) + case report(accountId: String, statusId: String, comment: String) + + public func path() -> String { + switch self { + case .postStatus: + "statuses" + case let .status(id): + "statuses/\(id)" + case let .editStatus(id, _): + "statuses/\(id)" + case let .context(id): + "statuses/\(id)/context" + case let .favorite(id): + "statuses/\(id)/favourite" + case let .unfavorite(id): + "statuses/\(id)/unfavourite" + case let .reblog(id): + "statuses/\(id)/reblog" + case let .unreblog(id): + "statuses/\(id)/unreblog" + case let .rebloggedBy(id, _): + "statuses/\(id)/reblogged_by" + case let .favoritedBy(id, _): + "statuses/\(id)/favourited_by" + case let .pin(id): + "statuses/\(id)/pin" + case let .unpin(id): + "statuses/\(id)/unpin" + case let .bookmark(id): + "statuses/\(id)/bookmark" + case let .unbookmark(id): + "statuses/\(id)/unbookmark" + case let .history(id): + "statuses/\(id)/history" + case let .translate(id, _): + "statuses/\(id)/translate" + case .report: + "reports" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case let .rebloggedBy(_, maxId): + return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) + case let .favoritedBy(_, maxId): + return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) + case let .translate(_, lang): + if let lang { + return [.init(name: "lang", value: lang)] + } + return nil + case let .report(accountId, statusId, comment): + return [.init(name: "account_id", value: accountId), + .init(name: "status_ids[]", value: statusId), + .init(name: "comment", value: comment)] + default: + return nil + } + } + + public var jsonValue: Encodable? { + switch self { + case let .postStatus(json): + json + case let .editStatus(_, json): + json + default: + nil + } + } +} + +public struct StatusData: Encodable, Sendable { + public let status: String + public let visibility: Visibility + public let inReplyToId: String? + public let spoilerText: String? + public let mediaIds: [String]? + public let poll: PollData? + public let language: String? + public let mediaAttributes: [MediaAttribute]? + + public struct PollData: Encodable, Sendable { + public let options: [String] + public let multiple: Bool + public let expires_in: Int + + public init(options: [String], multiple: Bool, expires_in: Int) { + self.options = options + self.multiple = multiple + self.expires_in = expires_in + } + } + + public struct MediaAttribute: Encodable, Sendable { + public let id: String + public let description: String? + public let thumbnail: String? + public let focus: String? + + public init(id: String, description: String?, thumbnail: String?, focus: String?) { + self.id = id + self.description = description + self.thumbnail = thumbnail + self.focus = focus + } + } + + public init(status: String, + visibility: Visibility, + inReplyToId: String? = nil, + spoilerText: String? = nil, + mediaIds: [String]? = nil, + poll: PollData? = nil, + language: String? = nil, + mediaAttributes: [MediaAttribute]? = nil) + { + self.status = status + self.visibility = visibility + self.inReplyToId = inReplyToId + self.spoilerText = spoilerText + self.mediaIds = mediaIds + self.poll = poll + self.language = language + self.mediaAttributes = mediaAttributes + } +} + +public enum Trends: Endpoint { + case tags + case statuses(offset: Int?) + case links + + public func path() -> String { + switch self { + case .tags: + "trends/tags" + case .statuses: + "trends/statuses" + case .links: + "trends/links" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case let .statuses(offset): + if let offset { + return [.init(name: "offset", value: String(offset))] + } + return nil + default: + return nil + } + } +} diff --git a/Threaded/Data/Navigator.swift b/Threaded/Data/Navigator.swift new file mode 100644 index 0000000..1557a7a --- /dev/null +++ b/Threaded/Data/Navigator.swift @@ -0,0 +1,121 @@ +//Made by Lumaa + +import Foundation +import SwiftUI + +@Observable +public class Navigator { + public var path: [RouterDestination] = [] + public var presentedSheet: SheetDestination? + public var selectedTab: TabDestination = .timeline + + public func navigate(to: RouterDestination) { + path.append(to) + } +} + +public enum TabDestination: Identifiable { + case timeline + case search + case activity + case profile + + public var id: String { + switch self { + case .timeline: + return "timeline" + case .search: + return "search" + case .activity: + return "activity" + case .profile: + return "profile" + } + } +} + +public enum SheetDestination: Identifiable { + case welcome + case mastodonLogin(logged: Binding) + case post + + public var id: String { + switch self { + case .welcome: + return "welcome" + case .mastodonLogin: + return "login" + case .post: + return "post" + } + } + + public var isCover: Bool { + switch self { + case .welcome: + return true + + case .mastodonLogin: + return false + + case .post: + return false + } + } +} + +public enum RouterDestination: Hashable { + case settings + case privacy + case account(acc: Account) +} + +extension View { + func withAppRouter() -> some View { + navigationDestination(for: RouterDestination.self) { destination in + switch destination { + case .settings: + SettingsView() + case .privacy: + PrivacyView() + case .account(let acc): + AccountView(account: acc) + } + } + } + + func withSheets(sheetDestination: Binding) -> some View { + sheet(item: sheetDestination) { destination in + viewRepresentation(destination: destination, isCover: false) + } + } + + func withCovers(sheetDestination: Binding) -> some View { + fullScreenCover(item: sheetDestination) { destination in + viewRepresentation(destination: destination, isCover: true) + } + } + + private func viewRepresentation(destination: SheetDestination, isCover: Bool) -> some View { + Group { + if destination.isCover { + switch destination { + case .welcome: + ConnectView() + default: + EmptyView() + } + } else { + switch destination { + case .post: + Text("Posting view") + case let .mastodonLogin(logged): + AddInstanceView(logged: logged) + .tint(Color.accentColor) + default: + EmptyView() + } + } + } + } +} diff --git a/Threaded/Data/ShareableImage.swift b/Threaded/Data/ShareableImage.swift new file mode 100644 index 0000000..1dbc4b6 --- /dev/null +++ b/Threaded/Data/ShareableImage.swift @@ -0,0 +1,22 @@ +//Made by Lumaa + +import Foundation +import SwiftUI + +struct AsyncImageTransferable: Codable, Transferable { + let url: URL + + func fetchAsImage() async -> Image { + let data = try? await URLSession.shared.data(from: url).0 + guard let data, let uiimage = UIImage(data: data) else { + return Image(systemName: "photo") + } + return Image(uiImage: uiimage) + } + + static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { media in + await media.fetchAsImage() + } + } +} diff --git a/Threaded/Data/Status.swift b/Threaded/Data/Status.swift new file mode 100644 index 0000000..1a2d306 --- /dev/null +++ b/Threaded/Data/Status.swift @@ -0,0 +1,421 @@ +//Made by Lumaa + +import Foundation + +public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable { + public static func == (lhs: Status, rhs: Status) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let id: String + public let content: HTMLString + public let account: Account + public let createdAt: ServerDate + public let editedAt: ServerDate? + public let reblog: ReblogStatus? + public let mediaAttachments: [MediaAttachment] + public let mentions: [Mention] + public let repliesCount: Int + public let reblogsCount: Int + public let favouritesCount: Int + public let card: Card? + public let favourited: Bool? + public let reblogged: Bool? + public let pinned: Bool? + public let bookmarked: Bool? + public let emojis: [Emoji] + public let url: String? + public let application: Application? + public let inReplyToId: String? + public let inReplyToAccountId: String? + public let visibility: Visibility + public let poll: Poll? + public let spoilerText: HTMLString + public let filtered: [Filtered]? + public let sensitive: Bool + public let language: String? + + public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { + self.id = id + self.content = content + self.account = account + self.createdAt = createdAt + self.editedAt = editedAt + self.reblog = reblog + self.mediaAttachments = mediaAttachments + self.mentions = mentions + self.repliesCount = repliesCount + self.reblogsCount = reblogsCount + self.favouritesCount = favouritesCount + self.card = card + self.favourited = favourited + self.reblogged = reblogged + self.pinned = pinned + self.bookmarked = bookmarked + self.emojis = emojis + self.url = url + self.application = application + self.inReplyToId = inReplyToId + self.inReplyToAccountId = inReplyToAccountId + self.visibility = visibility + self.poll = poll + self.spoilerText = spoilerText + self.filtered = filtered + self.sensitive = sensitive + self.language = language + } + + public static func placeholder(forSettings: Bool = false, language: String? = nil) -> Status { + .init(id: UUID().uuidString, + content: .init(stringValue: "Here's to the [#crazy](#) ones", + parseMarkdown: forSettings), + + account: .placeholder(), + createdAt: ServerDate(), + editedAt: nil, + reblog: nil, + mediaAttachments: [], + mentions: [], + repliesCount: 2, + reblogsCount: 1, + favouritesCount: 3, + card: nil, + favourited: false, + reblogged: false, + pinned: false, + bookmarked: false, + emojis: [], + url: "https://example.com", + application: nil, + inReplyToId: nil, + inReplyToAccountId: nil, + visibility: .pub, + poll: nil, + spoilerText: .init(stringValue: ""), + filtered: [], + sensitive: false, + language: language) + } + + public static func placeholders() -> [Status] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } + + public var reblogAsAsStatus: Status? { + if let reblog { + return .init(id: reblog.id, + content: reblog.content, + account: reblog.account, + createdAt: reblog.createdAt, + editedAt: reblog.editedAt, + reblog: nil, + mediaAttachments: reblog.mediaAttachments, + mentions: reblog.mentions, + repliesCount: reblog.repliesCount, + reblogsCount: reblog.reblogsCount, + favouritesCount: reblog.favouritesCount, + card: reblog.card, + favourited: reblog.favourited, + reblogged: reblog.reblogged, + pinned: reblog.pinned, + bookmarked: reblog.bookmarked, + emojis: reblog.emojis, + url: reblog.url, + application: reblog.application, + inReplyToId: reblog.inReplyToId, + inReplyToAccountId: reblog.inReplyToAccountId, + visibility: reblog.visibility, + poll: reblog.poll, + spoilerText: reblog.spoilerText, + filtered: reblog.filtered, + sensitive: reblog.sensitive, + language: reblog.language) + } + return nil + } +} + +public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Hashable { + public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let id: String + public let content: HTMLString + public let account: Account + public let createdAt: ServerDate + public let editedAt: ServerDate? + public let mediaAttachments: [MediaAttachment] + public let mentions: [Mention] + public let repliesCount: Int + public let reblogsCount: Int + public let favouritesCount: Int + public let card: Card? + public let favourited: Bool? + public let reblogged: Bool? + public let pinned: Bool? + public let bookmarked: Bool? + public let emojis: [Emoji] + public let url: String? + public let application: Application? + public let inReplyToId: String? + public let inReplyToAccountId: String? + public let visibility: Visibility + public let poll: Poll? + public let spoilerText: HTMLString + public let filtered: [Filtered]? + public let sensitive: Bool + public let language: String? + + public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application? = nil, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { + self.id = id + self.content = content + self.account = account + self.createdAt = createdAt + self.editedAt = editedAt + self.mediaAttachments = mediaAttachments + self.mentions = mentions + self.repliesCount = repliesCount + self.reblogsCount = reblogsCount + self.favouritesCount = favouritesCount + self.card = card + self.favourited = favourited + self.reblogged = reblogged + self.pinned = pinned + self.bookmarked = bookmarked + self.emojis = emojis + self.url = url + self.application = application + self.inReplyToId = inReplyToId + self.inReplyToAccountId = inReplyToAccountId + self.visibility = visibility + self.poll = poll + self.spoilerText = spoilerText + self.filtered = filtered + self.sensitive = sensitive + self.language = language + } +} + +// Every property in Status is immutable. +extension Status: Sendable {} + +// Every property in ReblogStatus is immutable. +extension ReblogStatus: Sendable {} + +public protocol AnyStatus { + var id: String { get } + var content: HTMLString { get } + var account: Account { get } + var createdAt: ServerDate { get } + var editedAt: ServerDate? { get } + var mediaAttachments: [MediaAttachment] { get } + var mentions: [Mention] { get } + var repliesCount: Int { get } + var reblogsCount: Int { get } + var favouritesCount: Int { get } + var card: Card? { get } + var favourited: Bool? { get } + var reblogged: Bool? { get } + var pinned: Bool? { get } + var bookmarked: Bool? { get } + var emojis: [Emoji] { get } + var url: String? { get } + var application: Application? { get } + var inReplyToId: String? { get } + var inReplyToAccountId: String? { get } + var visibility: Visibility { get } + var poll: Poll? { get } + var spoilerText: HTMLString { get } + var filtered: [Filtered]? { get } + var sensitive: Bool { get } + var language: String? { get } +} + +public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable { + public struct MetaContainer: Codable, Equatable { + public struct Meta: Codable, Equatable { + public let width: Int? + public let height: Int? + } + + public let original: Meta? + } + + public enum SupportedType: String { + case image, gifv, video, audio + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let id: String + public let type: String + public var supportedType: SupportedType? { + SupportedType(rawValue: type) + } + + public var localizedTypeDescription: String? { + if let supportedType { + switch supportedType { + case .image: + return NSLocalizedString("accessibility.media.supported-type.image.label", bundle: .main, comment: "A localized description of SupportedType.image") + case .gifv: + return NSLocalizedString("accessibility.media.supported-type.gifv.label", bundle: .main, comment: "A localized description of SupportedType.gifv") + case .video: + return NSLocalizedString("accessibility.media.supported-type.video.label", bundle: .main, comment: "A localized description of SupportedType.video") + case .audio: + return NSLocalizedString("accessibility.media.supported-type.audio.label", bundle: .main, comment: "A localized description of SupportedType.audio") + } + } + return nil + } + + public let url: URL? + public let previewUrl: URL? + public let description: String? + public let meta: MetaContainer? + + public static func imageWith(url: URL) -> MediaAttachment { + .init(id: UUID().uuidString, + type: "image", + url: url, + previewUrl: url, + description: "Alternative text", + meta: nil) + } +} + +extension MediaAttachment: Sendable {} +extension MediaAttachment.MetaContainer: Sendable {} +extension MediaAttachment.MetaContainer.Meta: Sendable {} +extension MediaAttachment.SupportedType: Sendable {} + +public struct Mention: Codable, Equatable, Hashable { + public let id: String + public let username: String + public let url: URL + public let acct: String +} + +extension Mention: Sendable {} + +public struct Card: Codable, Identifiable, Equatable, Hashable { + public var id: String { + url + } + + public let url: String + public let title: String? + public let description: String? + public let type: String + public let image: URL? +} + +extension Card: Sendable {} + +public struct Application: Codable, Identifiable, Hashable, Equatable, Sendable { + public var id: String { + name + } + + public let name: String + public let website: URL? +} + +public extension Application { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + name = try values.decodeIfPresent(String.self, forKey: .name) ?? "" + website = try? values.decodeIfPresent(URL.self, forKey: .website) + } +} + +public struct Filtered: Codable, Equatable, Hashable { + public let filter: Filter + public let keywordMatches: [String]? +} + +public struct Filter: Codable, Identifiable, Equatable, Hashable { + public enum Action: String, Codable, Equatable { + case warn, hide + } + + public enum Context: String, Codable { + case home, notifications, account, thread + case pub = "public" + } + + public let id: String + public let title: String + public let context: [String] + public let filterAction: Action +} + +extension Filtered: Sendable {} +extension Filter: Sendable {} +extension Filter.Action: Sendable {} +extension Filter.Context: Sendable {} + +public struct Poll: Codable, Equatable, Hashable { + public static func == (lhs: Poll, rhs: Poll) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public struct Option: Identifiable, Codable { + enum CodingKeys: String, CodingKey { + case title, votesCount + } + + public var id = UUID().uuidString + public let title: String + public let votesCount: Int? + } + + public let id: String + public let expiresAt: NullableString + public let expired: Bool + public let multiple: Bool + public let votesCount: Int + public let votersCount: Int? + public let voted: Bool? + public let ownVotes: [Int]? + public let options: [Option] + + // the votersCount can be null according to the docs when multiple is false. + // Didn't find that to be true, but we make sure + public var safeVotersCount: Int { + votersCount ?? votesCount + } +} + +public struct NullableString: Codable, Equatable, Hashable { + public let value: ServerDate? + + public init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + value = try container.decode(ServerDate.self) + } catch { + value = nil + } + } +} + +extension Poll: Sendable {} +extension Poll.Option: Sendable {} +extension NullableString: Sendable {} diff --git a/Threaded/Data/Tag.swift b/Threaded/Data/Tag.swift new file mode 100644 index 0000000..d4f6fef --- /dev/null +++ b/Threaded/Data/Tag.swift @@ -0,0 +1,69 @@ +//Made by Lumaa + +import Foundation + +public struct Tag: Codable, Identifiable, Equatable, Hashable { + public struct History: Codable, Identifiable { + public var id: String { + day + } + public let day: String + public let accounts: String + public let uses: String + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + } + + public static func == (lhs: Tag, rhs: Tag) -> Bool { + lhs.name == rhs.name + } + + public var id: String { + name + } + + public let name: String + public let url: String + public let following: Bool + public let history: [History] + + public var totalUses: Int { + history.compactMap { Int($0.uses) }.reduce(0, +) + } + + public var totalAccounts: Int { + history.compactMap { Int($0.accounts) }.reduce(0, +) + } +} + +public struct FeaturedTag: Codable, Identifiable { + public let id: String + public let name: String + public let url: URL + public let statusesCount: String + public var statusesCountInt: Int { + Int(statusesCount) ?? 0 + } + + private enum CodingKeys: String, CodingKey { + case id, name, url, statusesCount + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + url = try container.decode(URL.self, forKey: .url) + do { + statusesCount = try container.decode(String.self, forKey: .statusesCount) + } catch DecodingError.typeMismatch { + statusesCount = try String(container.decode(Int.self, forKey: .statusesCount)) + } + } +} + +extension Tag: Sendable {} +extension Tag.History: Sendable {} +extension FeaturedTag: Sendable {} diff --git a/Threaded/Info.plist b/Threaded/Info.plist new file mode 100644 index 0000000..89bdd1b --- /dev/null +++ b/Threaded/Info.plist @@ -0,0 +1,19 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + threaded + CFBundleURLSchemes + + threaded:// + + + + + diff --git a/Threaded/Localizable.xcstrings b/Threaded/Localizable.xcstrings new file mode 100644 index 0000000..88d7557 --- /dev/null +++ b/Threaded/Localizable.xcstrings @@ -0,0 +1,232 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "#%@" : { + + }, + "%@" : { + + }, + "•" : { + + }, + "accessibility.media.supported-type.audio.label" : { + "comment" : "A localized description of SupportedType.audio" + }, + "accessibility.media.supported-type.gifv.label" : { + "comment" : "A localized description of SupportedType.gifv" + }, + "accessibility.media.supported-type.image.label" : { + "comment" : "A localized description of SupportedType.image" + }, + "accessibility.media.supported-type.video.label" : { + "comment" : "A localized description of SupportedType.video" + }, + "Hello world" : { + + }, + "instance.rules" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rules" + } + } + } + }, + "login.mastodon" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log in using Mastodon" + } + } + } + }, + "login.mastodon.footer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log in your Mastodon account using its instance URL" + } + } + } + }, + "login.mastodon.instance" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the instance's URL" + } + } + } + }, + "login.mastodon.login" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log in" + } + } + } + }, + "login.mastodon.verify" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verify" + } + } + } + }, + "login.mastodon.verify-error" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This might not be a Mastodon instance." + } + } + } + }, + "login.no-account" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stay anonymous" + } + } + } + }, + "login.no-account.footer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Without an account, you cannot interact with posts, users and instances. You can only read posts and users' public data." + } + } + } + }, + "login.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Welcome to Threaded!" + } + } + } + }, + "logout" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log out" + } + } + } + }, + "Posting view" : { + + }, + "setting.privacy" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy" + } + } + } + }, + "settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + } + } + }, + "status.favourites-%lld" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld like" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld likes" + } + } + } + } + } + } + }, + "status.replies-%lld" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld reply" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld replies" + } + } + } + } + } + } + }, + "status.reposted-by.%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ reposted" + } + } + } + }, + "timeline.federated" : { + + }, + "timeline.home" : { + + }, + "timeline.latest" : { + + }, + "timeline.local" : { + + }, + "timeline.trending" : { + + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Threaded/Packages/Models/.gitignore b/Threaded/Packages/Models/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Threaded/Packages/Models/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme b/Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme new file mode 100644 index 0000000..0b6ae59 --- /dev/null +++ b/Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/ModelsTests.xcscheme b/Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/ModelsTests.xcscheme new file mode 100644 index 0000000..a7819c7 --- /dev/null +++ b/Threaded/Packages/Models/.swiftpm/xcode/xcshareddata/xcschemes/ModelsTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Threaded/Packages/Models/Package.swift b/Threaded/Packages/Models/Package.swift new file mode 100644 index 0000000..3de3587 --- /dev/null +++ b/Threaded/Packages/Models/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Models", + defaultLocalization: "en", + platforms: [ + .iOS(.v17), + ], + products: [ + .library( + name: "Models", + targets: ["Models"] + ), + ], + dependencies: [ + .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.4.3"), + ], + targets: [ + .target( + name: "Models", + dependencies: [ + "SwiftSoup", + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + .testTarget( + name: "ModelsTests", + dependencies: ["Models"] + ), + ] +) diff --git a/Threaded/Packages/Models/README.md b/Threaded/Packages/Models/README.md new file mode 100644 index 0000000..8924cea --- /dev/null +++ b/Threaded/Packages/Models/README.md @@ -0,0 +1,3 @@ +# Models + +A description of this package. diff --git a/Threaded/Packages/Models/Sources/Models/Account.swift b/Threaded/Packages/Models/Sources/Models/Account.swift new file mode 100644 index 0000000..8bfe66a --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Account.swift @@ -0,0 +1,125 @@ +import Foundation + +public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable { + public static func == (lhs: Account, rhs: Account) -> Bool { + lhs.id == rhs.id && + lhs.username == rhs.username && + lhs.note.asRawText == rhs.note.asRawText && + lhs.statusesCount == rhs.statusesCount && + lhs.followersCount == rhs.followersCount && + lhs.followingCount == rhs.followingCount && + lhs.acct == rhs.acct && + lhs.displayName == rhs.displayName && + lhs.fields == rhs.fields && + lhs.lastStatusAt == rhs.lastStatusAt && + lhs.discoverable == rhs.discoverable && + lhs.bot == rhs.bot && + lhs.locked == rhs.locked + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public struct Field: Codable, Equatable, Identifiable, Sendable { + public var id: String { + value.asRawText + name + } + + public let name: String + public let value: HTMLString + public let verifiedAt: String? + } + + public struct Source: Codable, Equatable, Sendable { + public let privacy: Visibility + public let sensitive: Bool + public let language: String? + public let note: String + public let fields: [Field] + } + + public let id: String + public let username: String + public let displayName: String? + public let avatar: URL + public let header: URL + public let acct: String + public let note: HTMLString + public let createdAt: ServerDate + public let followersCount: Int? + public let followingCount: Int? + public let statusesCount: Int? + public let lastStatusAt: String? + public let fields: [Field] + public let locked: Bool + public let emojis: [Emoji] + public let url: URL? + public let source: Source? + public let bot: Bool + public let discoverable: Bool? + + public var haveAvatar: Bool { + avatar.lastPathComponent != "missing.png" + } + + public var haveHeader: Bool { + header.lastPathComponent != "missing.png" + } + + public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil) { + self.id = id + self.username = username + self.displayName = displayName + self.avatar = avatar + self.header = header + self.acct = acct + self.note = note + self.createdAt = createdAt + self.followersCount = followersCount + self.followingCount = followingCount + self.statusesCount = statusesCount + self.lastStatusAt = lastStatusAt + self.fields = fields + self.locked = locked + self.emojis = emojis + self.url = url + self.source = source + self.bot = bot + self.discoverable = discoverable + } + + public static func placeholder() -> Account { + .init(id: UUID().uuidString, + username: "Username", + displayName: "John Mastodon", + avatar: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!, + header: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!, + acct: "johnm@example.com", + note: .init(stringValue: "Some content"), + createdAt: ServerDate(), + followersCount: 10, + followingCount: 10, + statusesCount: 10, + lastStatusAt: nil, + fields: [], + locked: false, + emojis: [], + url: nil, + source: nil, + bot: false, + discoverable: true) + } + + public static func placeholders() -> [Account] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } +} + +public struct FamiliarAccounts: Decodable { + public let id: String + public let accounts: [Account] +} + +extension FamiliarAccounts: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Alias/DateFormatterCache.swift b/Threaded/Packages/Models/Sources/Models/Alias/DateFormatterCache.swift new file mode 100644 index 0000000..bbc617b --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Alias/DateFormatterCache.swift @@ -0,0 +1,26 @@ +import Foundation + +class DateFormatterCache: @unchecked Sendable { + static let shared = DateFormatterCache() + + let createdAtRelativeFormatter: RelativeDateTimeFormatter + let createdAtShortDateFormatted: DateFormatter + let createdAtDateFormatter: DateFormatter + + init() { + let createdAtRelativeFormatter = RelativeDateTimeFormatter() + createdAtRelativeFormatter.unitsStyle = .short + self.createdAtRelativeFormatter = createdAtRelativeFormatter + + let createdAtShortDateFormatted = DateFormatter() + createdAtShortDateFormatted.dateStyle = .short + createdAtShortDateFormatted.timeStyle = .none + self.createdAtShortDateFormatted = createdAtShortDateFormatted + + let createdAtDateFormatter = DateFormatter() + createdAtDateFormatter.calendar = .init(identifier: .iso8601) + createdAtDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" + createdAtDateFormatter.timeZone = .init(abbreviation: "UTC") + self.createdAtDateFormatter = createdAtDateFormatter + } +} diff --git a/Threaded/Packages/Models/Sources/Models/Alias/HTMLString.swift b/Threaded/Packages/Models/Sources/Models/Alias/HTMLString.swift new file mode 100644 index 0000000..686e030 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Alias/HTMLString.swift @@ -0,0 +1,291 @@ +import Foundation +import SwiftSoup +import SwiftUI + +private enum CodingKeys: CodingKey { + case htmlValue, asMarkdown, asRawText, statusesURLs, links +} + +public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { + public var htmlValue: String = "" + public var asMarkdown: String = "" + public var asRawText: String = "" + public var statusesURLs = [URL]() + public private(set) var links = [Link]() + + public var asSafeMarkdownAttributedString: AttributedString = .init() + private var main_regex: NSRegularExpression? + private var underscore_regex: NSRegularExpression? + public init(from decoder: Decoder) { + var alreadyDecoded = false + do { + let container = try decoder.singleValueContainer() + htmlValue = try container.decode(String.self) + } catch { + do { + alreadyDecoded = true + let container = try decoder.container(keyedBy: CodingKeys.self) + htmlValue = try container.decode(String.self, forKey: .htmlValue) + asMarkdown = try container.decode(String.self, forKey: .asMarkdown) + asRawText = try container.decode(String.self, forKey: .asRawText) + statusesURLs = try container.decode([URL].self, forKey: .statusesURLs) + links = try container.decode([Link].self, forKey: .links) + } catch { + htmlValue = "" + } + } + + if !alreadyDecoded { + // https://daringfireball.net/projects/markdown/syntax + // Pre-escape \ ` _ * ~ and [ as these are the only + // characters the markdown parser uses when it renders + // to attributed text. Note that ~ for strikethrough is + // not documented in the syntax docs but is used by + // AttributedString. + main_regex = try? NSRegularExpression(pattern: "([\\*\\`\\~\\[\\\\])", options: .caseInsensitive) + // don't escape underscores that are between colons, they are most likely custom emoji + underscore_regex = try? NSRegularExpression(pattern: "(?!\\B:[^:]*)(_)(?![^:]*:\\B)", options: .caseInsensitive) + + asMarkdown = "" + do { + let document: Document = try SwiftSoup.parse(htmlValue) + handleNode(node: document) + + document.outputSettings(OutputSettings().prettyPrint(pretty: false)) + try document.select("br").after("\n") + try document.select("p").after("\n\n") + let html = try document.html() + var text = try SwiftSoup.clean(html, "", Whitelist.none(), OutputSettings().prettyPrint(pretty: false)) ?? "" + // Remove the two last line break added after the last paragraph. + if text.hasSuffix("\n\n") { + _ = text.removeLast() + _ = text.removeLast() + } + asRawText = text + + if asMarkdown.hasPrefix("\n") { + _ = asMarkdown.removeFirst() + } + + } catch { + asRawText = htmlValue + } + } + + do { + let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, + interpretedSyntax: .inlineOnlyPreservingWhitespace) + asSafeMarkdownAttributedString = try AttributedString(markdown: asMarkdown, options: options) + } catch { + asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) + } + } + + public init(stringValue: String, parseMarkdown: Bool = false) { + htmlValue = stringValue + asMarkdown = stringValue + asRawText = stringValue + statusesURLs = [] + + if parseMarkdown { + do { + let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, + interpretedSyntax: .inlineOnlyPreservingWhitespace) + asSafeMarkdownAttributedString = try AttributedString(markdown: asMarkdown, options: options) + } catch { + asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) + } + } else { + asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(htmlValue, forKey: .htmlValue) + try container.encode(asMarkdown, forKey: .asMarkdown) + try container.encode(asRawText, forKey: .asRawText) + try container.encode(statusesURLs, forKey: .statusesURLs) + try container.encode(links, forKey: .links) + } + + private mutating func handleNode(node: SwiftSoup.Node) { + do { + if let className = try? node.attr("class") { + if className == "invisible" { + // don't display + return + } + + if className == "ellipsis" { + // descend into this one now and + // append the ellipsis + for nn in node.getChildNodes() { + handleNode(node: nn) + } + asMarkdown += "…" + return + } + } + + if node.nodeName() == "p" { + if asMarkdown.count > 0 { // ignore first opening

    + asMarkdown += "\n\n" + } + } else if node.nodeName() == "br" { + if asMarkdown.count > 0 { // ignore first opening
    + asMarkdown += "\n" + } + } else if node.nodeName() == "a" { + let href = try node.attr("href") + if href != "" { + if let url = URL(string: href), + let _ = Int(url.lastPathComponent) + { + statusesURLs.append(url) + } + } + asMarkdown += "[" + let start = asMarkdown.endIndex + // descend into this node now so we can wrap the + // inner part of the link in the right markup + for nn in node.getChildNodes() { + handleNode(node: nn) + } + let finish = asMarkdown.endIndex + + var linkRef = href + + // Try creating a URL from the string. If it fails, try URL encoding + // the string first. + var url = URL(string: href) + if url == nil { + url = URL(string: href, encodePath: true) + } + if let linkUrl = url { + linkRef = linkUrl.absoluteString + let displayString = asMarkdown[start ..< finish] + links.append(Link(linkUrl, displayString: String(displayString))) + } + + asMarkdown += "](" + asMarkdown += linkRef + asMarkdown += ")" + + return + } else if node.nodeName() == "#text" { + var txt = node.description + + if let underscore_regex, let main_regex { + // This is the markdown escaper + txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1") + txt = underscore_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1") + } + // Strip newlines and line separators - they should be being sent as
    s + asMarkdown += txt.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\u{2028}", with: "") + } else if node.nodeName() == "ul" { + // Unordered (bulleted) list + // SwiftUI's Text won't display these in an AttributedString, but we can at least improve the appearance + asMarkdown += "\n\n" + for nn in node.getChildNodes() { + asMarkdown += "- " + handleNode(node: nn) + asMarkdown += "\n" + } + return + } else if node.nodeName() == "ol" { + // Ordered (numbered) list + // Same thing, won't display in a Text, but this is just an attempt to improve the appearance + asMarkdown += "\n\n" + var curNumber = 1 + for nn in node.getChildNodes() { + asMarkdown += "\(curNumber). " + handleNode(node: nn) + asMarkdown += "\n" + curNumber += 1 + } + return + } + + for n in node.getChildNodes() { + handleNode(node: n) + } + } catch {} + } + + public struct Link: Codable, Hashable, Identifiable { + public var id: Int { hashValue } + public let url: URL + public let displayString: String + public let type: LinkType + public let title: String + + init(_ url: URL, displayString: String) { + self.url = url + self.displayString = displayString + + switch displayString.first { + case "@": + type = .mention + title = displayString + case "#": + type = .hashtag + title = String(displayString.dropFirst()) + default: + type = .url + var hostNameUrl = url.host ?? url.absoluteString + if hostNameUrl.hasPrefix("www.") { + hostNameUrl = String(hostNameUrl.dropFirst(4)) + } + title = hostNameUrl + } + } + + public enum LinkType: String, Codable { + case url + case mention + case hashtag + } + } +} + +public extension URL { + // It's common to use non-ASCII characters in URLs even though they're technically + // invalid characters. Every modern browser handles this by silently encoding + // the invalid characters on the user's behalf. However, trying to create a URL + // object with un-encoded characters will result in nil so we need to encode the + // invalid characters before creating the URL object. The unencoded version + // should still be shown in the displayed status. + init?(string: String, encodePath: Bool) { + var encodedUrlString = "" + if encodePath, + string.starts(with: "http://") || string.starts(with: "https://"), + var startIndex = string.firstIndex(of: "/") + { + startIndex = string.index(startIndex, offsetBy: 1) + + // We don't want to encode the host portion of the URL + if var startIndex = string[startIndex...].firstIndex(of: "/") { + encodedUrlString = String(string[...startIndex]) + while let endIndex = string[string.index(after: startIndex)...].firstIndex(of: "/") { + let componentStartIndex = string.index(after: startIndex) + encodedUrlString = encodedUrlString + (string[componentStartIndex ... endIndex].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") + startIndex = endIndex + } + + // The last part of the path may have a query string appended to it + let componentStartIndex = string.index(after: startIndex) + if let queryStartIndex = string[componentStartIndex...].firstIndex(of: "?") { + encodedUrlString = encodedUrlString + (string[componentStartIndex ..< queryStartIndex].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") + encodedUrlString = encodedUrlString + (string[queryStartIndex...].addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") + } else { + encodedUrlString = encodedUrlString + (string[componentStartIndex...].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") + } + } + } + if encodedUrlString.isEmpty { + encodedUrlString = string + } + self.init(string: encodedUrlString) + } +} diff --git a/Threaded/Packages/Models/Sources/Models/Alias/ServerDate.swift b/Threaded/Packages/Models/Sources/Models/Alias/ServerDate.swift new file mode 100644 index 0000000..e60d2a5 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Alias/ServerDate.swift @@ -0,0 +1,41 @@ +import Foundation + +private enum CodingKeys: CodingKey { + case asDate +} + +public struct ServerDate: Codable, Hashable, Equatable, Sendable { + public let asDate: Date + + public var relativeFormatted: String { + DateFormatterCache.shared.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date()) + } + + public var shortDateFormatted: String { + DateFormatterCache.shared.createdAtShortDateFormatted.string(from: asDate) + } + + private static let calendar = Calendar(identifier: .gregorian) + + public init() { + asDate = Date() - 100 + } + + public init(from decoder: Decoder) throws { + do { + // Decode from server + let container = try decoder.singleValueContainer() + let stringDate = try container.decode(String.self) + asDate = DateFormatterCache.shared.createdAtDateFormatter.date(from: stringDate) ?? Date() + } catch { + // Decode from cache + let container = try decoder.container(keyedBy: CodingKeys.self) + asDate = try container.decode(Date.self, forKey: .asDate) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(asDate, forKey: .asDate) + } +} diff --git a/Threaded/Packages/Models/Sources/Models/App/App.swift b/Threaded/Packages/Models/Sources/Models/App/App.swift new file mode 100644 index 0000000..dfde839 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/App/App.swift @@ -0,0 +1,12 @@ +import Foundation + +public enum AppInfo { + public static let appStoreAppId = "6444915884" + public static let clientName = "IceCubesApp" + public static let scheme = "icecubesapp://" + public static let scopes = "read write follow push" + public static let weblink = "https://github.com/Dimillian/IceCubesApp" + public static let revenueCatKey = "appl_JXmiRckOzXXTsHKitQiicXCvMQi" + public static let defaultServer = "mastodon.social" + public static let keychainGroup = "346J38YKE3.com.thomasricouard.IceCubesApp" +} diff --git a/Threaded/Packages/Models/Sources/Models/AppAccount.swift b/Threaded/Packages/Models/Sources/Models/AppAccount.swift new file mode 100644 index 0000000..a5c7291 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/AppAccount.swift @@ -0,0 +1,31 @@ +import Foundation +import SwiftUI + +public struct AppAccount: Codable, Identifiable, Hashable { + public let server: String + public var accountName: String? + public let oauthToken: OauthToken? + + public var key: String { + if let oauthToken { + "\(server):\(oauthToken.createdAt)" + } else { + "\(server):anonymous" + } + } + + public var id: String { + key + } + + public init(server: String, + accountName: String?, + oauthToken: OauthToken? = nil) + { + self.server = server + self.accountName = accountName + self.oauthToken = oauthToken + } +} + +extension AppAccount: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Application.swift b/Threaded/Packages/Models/Sources/Models/Application.swift new file mode 100644 index 0000000..3033e4c --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Application.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct Application: Codable, Identifiable, Hashable, Equatable, Sendable { + public var id: String { + name + } + + public let name: String + public let website: URL? +} + +public extension Application { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + name = try values.decodeIfPresent(String.self, forKey: .name) ?? "" + website = try? values.decodeIfPresent(URL.self, forKey: .website) + } +} diff --git a/Threaded/Packages/Models/Sources/Models/Card.swift b/Threaded/Packages/Models/Sources/Models/Card.swift new file mode 100644 index 0000000..132ead3 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Card.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct Card: Codable, Identifiable, Equatable, Hashable { + public var id: String { + url + } + + public let url: String + public let title: String? + public let description: String? + public let type: String + public let image: URL? +} + +extension Card: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/ConsolidatedNotification.swift b/Threaded/Packages/Models/Sources/Models/ConsolidatedNotification.swift new file mode 100644 index 0000000..5716192 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/ConsolidatedNotification.swift @@ -0,0 +1,45 @@ +// +// ConsolidatedNotification.swift +// +// +// Created by Jérôme Danthinne on 31/01/2023. +// + +import Foundation + +public struct ConsolidatedNotification: Identifiable { + public let notifications: [Notification] + public let type: Notification.NotificationType + public let createdAt: ServerDate + public let accounts: [Account] + public let status: Status? + + public var id: String? { notifications.first?.id } + + public init(notifications: [Notification], + type: Notification.NotificationType, + createdAt: ServerDate, + accounts: [Account], + status: Status?) + { + self.notifications = notifications + self.type = type + self.createdAt = createdAt + self.accounts = accounts + self.status = status ?? nil + } + + public static func placeholder() -> ConsolidatedNotification { + .init(notifications: [Notification.placeholder()], + type: .favourite, + createdAt: ServerDate(), + accounts: [.placeholder()], + status: .placeholder()) + } + + public static func placeholders() -> [ConsolidatedNotification] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } +} + +extension ConsolidatedNotification: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Conversation.swift b/Threaded/Packages/Models/Sources/Models/Conversation.swift new file mode 100644 index 0000000..7935cf8 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Conversation.swift @@ -0,0 +1,26 @@ +import Foundation + +public struct Conversation: Identifiable, Decodable, Hashable, Equatable { + public let id: String + public let unread: Bool + public let lastStatus: Status? + public let accounts: [Account] + + public init(id: String, unread: Bool, lastStatus: Status? = nil, accounts: [Account]) { + self.id = id + self.unread = unread + self.lastStatus = lastStatus + self.accounts = accounts + } + + public static func placeholder() -> Conversation { + .init(id: UUID().uuidString, unread: false, lastStatus: .placeholder(), accounts: [.placeholder()]) + } + + public static func placeholders() -> [Conversation] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } +} + +extension Conversation: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Emoji.swift b/Threaded/Packages/Models/Sources/Models/Emoji.swift new file mode 100644 index 0000000..bc12b82 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Emoji.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct Emoji: Codable, Hashable, Identifiable, Equatable, Sendable { + public func hash(into hasher: inout Hasher) { + hasher.combine(shortcode) + } + + public var id: String { + shortcode + } + + public let shortcode: String + public let url: URL + public let staticUrl: URL + public let visibleInPicker: Bool + public let category: String? +} diff --git a/Threaded/Packages/Models/Sources/Models/Filter.swift b/Threaded/Packages/Models/Sources/Models/Filter.swift new file mode 100644 index 0000000..cf49a13 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Filter.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct Filtered: Codable, Equatable, Hashable { + public let filter: Filter + public let keywordMatches: [String]? +} + +public struct Filter: Codable, Identifiable, Equatable, Hashable { + public enum Action: String, Codable, Equatable { + case warn, hide + } + + public enum Context: String, Codable { + case home, notifications, account, thread + case pub = "public" + } + + public let id: String + public let title: String + public let context: [String] + public let filterAction: Action +} + +extension Filtered: Sendable {} +extension Filter: Sendable {} +extension Filter.Action: Sendable {} +extension Filter.Context: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Instance.swift b/Threaded/Packages/Models/Sources/Models/Instance.swift new file mode 100644 index 0000000..6900c20 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Instance.swift @@ -0,0 +1,47 @@ +import Foundation + +public struct Instance: Codable, Sendable { + public struct Stats: Codable, Sendable { + public let userCount: Int + public let statusCount: Int + public let domainCount: Int + } + + public struct Configuration: Codable, Sendable { + public struct Statuses: Codable, Sendable { + public let maxCharacters: Int + public let maxMediaAttachments: Int + } + + public struct Polls: Codable, Sendable { + public let maxOptions: Int + public let maxCharactersPerOption: Int + public let minExpiration: Int + public let maxExpiration: Int + } + + public let statuses: Statuses + public let polls: Polls + } + + public struct Rule: Codable, Identifiable, Sendable { + public let id: String + public let text: String + } + + public struct URLs: Codable, Sendable { + public let streamingApi: URL? + } + + public let title: String + public let shortDescription: String + public let email: String + public let version: String + public let stats: Stats + public let languages: [String]? + public let registrations: Bool + public let thumbnail: URL? + public let configuration: Configuration? + public let rules: [Rule]? + public let urls: URLs? +} diff --git a/Threaded/Packages/Models/Sources/Models/InstanceApp.swift b/Threaded/Packages/Models/Sources/Models/InstanceApp.swift new file mode 100644 index 0000000..8f41ba9 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/InstanceApp.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct InstanceApp: Codable, Identifiable { + public let id: String + public let name: String + public let website: URL? + public let redirectUri: String + public let clientId: String + public let clientSecret: String + public let vapidKey: String? +} + +extension InstanceApp: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/InstanceSocial.swift b/Threaded/Packages/Models/Sources/Models/InstanceSocial.swift new file mode 100644 index 0000000..6d2dc36 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/InstanceSocial.swift @@ -0,0 +1,16 @@ +import Foundation + +public struct InstanceSocial: Decodable, Identifiable, Sendable { + public struct Info: Decodable, Sendable { + public let shortDescription: String? + } + + public let id: String + public let name: String + public let dead: Bool + public let users: String + public let activeUsers: Int? + public let statuses: String + public let thumbnail: URL? + public let info: Info? +} diff --git a/Threaded/Packages/Models/Sources/Models/Language.swift b/Threaded/Packages/Models/Sources/Models/Language.swift new file mode 100644 index 0000000..667b266 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Language.swift @@ -0,0 +1,23 @@ +import Foundation + +@MainActor +public struct Language: Identifiable, Equatable, Hashable { + public nonisolated var id: String { isoCode } + + public let isoCode: String + public let nativeName: String? + public let localizedName: String? + + public static var allAvailableLanguages: [Language] = Locale.LanguageCode.isoLanguageCodes + .filter { $0.identifier.count <= 3 } + .map { lang in + let nativeLocale = Locale(languageComponents: Locale.Language.Components(languageCode: lang)) + return Language( + isoCode: lang.identifier, + nativeName: nativeLocale.localizedString(forLanguageCode: lang.identifier)?.capitalized, + localizedName: Locale.current.localizedString(forLanguageCode: lang.identifier)?.localizedCapitalized + ) + } +} + +extension Language: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/List.swift b/Threaded/Packages/Models/Sources/Models/List.swift new file mode 100644 index 0000000..019fcf9 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/List.swift @@ -0,0 +1,18 @@ +import Foundation + +public struct List: Codable, Identifiable, Equatable, Hashable { + public let id: String + public let title: String + public let repliesPolicy: RepliesPolicy? + public let exclusive: Bool? + + public enum RepliesPolicy: String, Sendable, Codable, CaseIterable, Identifiable { + public var id: String { + rawValue + } + + case followed, list, `none` + } +} + +extension List: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/MastodonPushNotification.swift b/Threaded/Packages/Models/Sources/Models/MastodonPushNotification.swift new file mode 100644 index 0000000..2d86ea0 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/MastodonPushNotification.swift @@ -0,0 +1,25 @@ +import Foundation + +public struct MastodonPushNotification: Codable { + public let accessToken: String + + public let notificationID: Int + public let notificationType: String + + public let preferredLocale: String? + public let icon: String? + public let title: String + public let body: String + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case notificationID = "notification_id" + case notificationType = "notification_type" + case preferredLocale = "preferred_locale" + case icon + case title + case body + } +} + +extension MastodonPushNotification: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/MediaAttachement.swift b/Threaded/Packages/Models/Sources/Models/MediaAttachement.swift new file mode 100644 index 0000000..1c52776 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/MediaAttachement.swift @@ -0,0 +1,61 @@ +import Foundation + +public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable { + public struct MetaContainer: Codable, Equatable { + public struct Meta: Codable, Equatable { + public let width: Int? + public let height: Int? + } + + public let original: Meta? + } + + public enum SupportedType: String { + case image, gifv, video, audio + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let id: String + public let type: String + public var supportedType: SupportedType? { + SupportedType(rawValue: type) + } + + public var localizedTypeDescription: String? { + if let supportedType { + switch supportedType { + case .image: + return NSLocalizedString("accessibility.media.supported-type.image.label", bundle: .main, comment: "A localized description of SupportedType.image") + case .gifv: + return NSLocalizedString("accessibility.media.supported-type.gifv.label", bundle: .main, comment: "A localized description of SupportedType.gifv") + case .video: + return NSLocalizedString("accessibility.media.supported-type.video.label", bundle: .main, comment: "A localized description of SupportedType.video") + case .audio: + return NSLocalizedString("accessibility.media.supported-type.audio.label", bundle: .main, comment: "A localized description of SupportedType.audio") + } + } + return nil + } + + public let url: URL? + public let previewUrl: URL? + public let description: String? + public let meta: MetaContainer? + + public static func imageWith(url: URL) -> MediaAttachment { + .init(id: UUID().uuidString, + type: "image", + url: url, + previewUrl: url, + description: "demo alt text here", + meta: nil) + } +} + +extension MediaAttachment: Sendable {} +extension MediaAttachment.MetaContainer: Sendable {} +extension MediaAttachment.MetaContainer.Meta: Sendable {} +extension MediaAttachment.SupportedType: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Mention.swift b/Threaded/Packages/Models/Sources/Models/Mention.swift new file mode 100644 index 0000000..588b3d8 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Mention.swift @@ -0,0 +1,10 @@ +import Foundation + +public struct Mention: Codable, Equatable, Hashable { + public let id: String + public let username: String + public let url: URL + public let acct: String +} + +extension Mention: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Notification.swift b/Threaded/Packages/Models/Sources/Models/Notification.swift new file mode 100644 index 0000000..405a2de --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Notification.swift @@ -0,0 +1,28 @@ +import Foundation + +public struct Notification: Decodable, Identifiable, Equatable { + public enum NotificationType: String, CaseIterable { + case follow, follow_request, mention, reblog, status, favourite, poll, update + } + + public let id: String + public let type: String + public let createdAt: ServerDate + public let account: Account + public let status: Status? + + public var supportedType: NotificationType? { + .init(rawValue: type) + } + + public static func placeholder() -> Notification { + .init(id: UUID().uuidString, + type: NotificationType.favourite.rawValue, + createdAt: ServerDate(), + account: .placeholder(), + status: .placeholder()) + } +} + +extension Notification: Sendable {} +extension Notification.NotificationType: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/OauthToken.swift b/Threaded/Packages/Models/Sources/Models/OauthToken.swift new file mode 100644 index 0000000..78fb741 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/OauthToken.swift @@ -0,0 +1,8 @@ +import Foundation + +public struct OauthToken: Codable, Hashable, Sendable { + public let accessToken: String + public let tokenType: String + public let scope: String + public let createdAt: Double +} diff --git a/Threaded/Packages/Models/Sources/Models/Poll.swift b/Threaded/Packages/Models/Sources/Models/Poll.swift new file mode 100644 index 0000000..7baab2d --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Poll.swift @@ -0,0 +1,54 @@ +import Foundation + +public struct Poll: Codable, Equatable, Hashable { + public static func == (lhs: Poll, rhs: Poll) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public struct Option: Identifiable, Codable { + enum CodingKeys: String, CodingKey { + case title, votesCount + } + + public var id = UUID().uuidString + public let title: String + public let votesCount: Int? + } + + public let id: String + public let expiresAt: NullableString + public let expired: Bool + public let multiple: Bool + public let votesCount: Int + public let votersCount: Int? + public let voted: Bool? + public let ownVotes: [Int]? + public let options: [Option] + + // the votersCount can be null according to the docs when multiple is false. + // Didn't find that to be true, but we make sure + public var safeVotersCount: Int { + votersCount ?? votesCount + } +} + +public struct NullableString: Codable, Equatable, Hashable { + public let value: ServerDate? + + public init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + value = try container.decode(ServerDate.self) + } catch { + value = nil + } + } +} + +extension Poll: Sendable {} +extension Poll.Option: Sendable {} +extension NullableString: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/PushSubscription.swift b/Threaded/Packages/Models/Sources/Models/PushSubscription.swift new file mode 100644 index 0000000..d17075b --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/PushSubscription.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct PushSubscription: Identifiable, Decodable { + public struct Alerts: Decodable { + public let follow: Bool + public let favourite: Bool + public let reblog: Bool + public let mention: Bool + public let poll: Bool + public let status: Bool + } + + public let id: Int + public let endpoint: URL + public let serverKey: String + public let alerts: Alerts +} + +extension PushSubscription: Sendable {} +extension PushSubscription.Alerts: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Relationship.swift b/Threaded/Packages/Models/Sources/Models/Relationship.swift new file mode 100644 index 0000000..7dbf8f6 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Relationship.swift @@ -0,0 +1,54 @@ +import Foundation + +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 {} diff --git a/Threaded/Packages/Models/Sources/Models/SearchResults.swift b/Threaded/Packages/Models/Sources/Models/SearchResults.swift new file mode 100644 index 0000000..6c8514c --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/SearchResults.swift @@ -0,0 +1,18 @@ +import Foundation + +public struct SearchResults: Decodable { + enum CodingKeys: String, CodingKey { + case accounts, statuses, hashtags + } + + public let accounts: [Account] + public var relationships: [Relationship] = [] + public let statuses: [Status] + public let hashtags: [Tag] + + public var isEmpty: Bool { + accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty + } +} + +extension SearchResults: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/ServerError.swift b/Threaded/Packages/Models/Sources/Models/ServerError.swift new file mode 100644 index 0000000..b81bf67 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/ServerError.swift @@ -0,0 +1,8 @@ +import Foundation + +public struct ServerError: Decodable, Error { + public let error: String? + public var httpCode: Int? +} + +extension ServerError: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/ServerFilter.swift b/Threaded/Packages/Models/Sources/Models/ServerFilter.swift new file mode 100644 index 0000000..0ca949c --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/ServerFilter.swift @@ -0,0 +1,80 @@ +import Foundation + +public struct ServerFilter: Codable, Identifiable, Hashable, Sendable { + public struct Keyword: Codable, Identifiable, Hashable, Sendable { + public let id: String + public let keyword: String + public let wholeWord: Bool + } + + public enum Context: String, Codable, CaseIterable, Sendable { + case home, notifications, `public`, thread, account + } + + public enum Action: String, Codable, CaseIterable, Sendable { + case warn, hide + } + + public let id: String + public let title: String + public let keywords: [Keyword] + public let filterAction: Action + public let context: [Context] + public let expiresIn: Int? + public let expiresAt: ServerDate? + + public func hasExpiry() -> Bool { + expiresAt != nil + } + + public func isExpired() -> Bool { + if let expiresAtDate = expiresAt?.asDate { + expiresAtDate < Date() + } else { + false + } + } +} + +public extension ServerFilter.Context { + var iconName: String { + switch self { + case .home: + "rectangle.stack" + case .notifications: + "bell" + case .public: + "globe.americas" + case .thread: + "bubble.left.and.bubble.right" + case .account: + "person.crop.circle" + } + } + + var name: String { + switch self { + case .home: + NSLocalizedString("filter.contexts.home", comment: "") + case .notifications: + NSLocalizedString("filter.contexts.notifications", comment: "") + case .public: + NSLocalizedString("filter.contexts.public", comment: "") + case .thread: + NSLocalizedString("filter.contexts.conversations", comment: "") + case .account: + NSLocalizedString("filter.contexts.profiles", comment: "") + } + } +} + +public extension ServerFilter.Action { + var label: String { + switch self { + case .warn: + NSLocalizedString("filter.action.warning", comment: "") + case .hide: + NSLocalizedString("filter.action.hide", comment: "") + } + } +} diff --git a/Threaded/Packages/Models/Sources/Models/ServerPreferences.swift b/Threaded/Packages/Models/Sources/Models/ServerPreferences.swift new file mode 100644 index 0000000..9b4f8ce --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/ServerPreferences.swift @@ -0,0 +1,38 @@ +import Foundation +import SwiftUI + +public struct ServerPreferences: Decodable { + public let postVisibility: Visibility? + public let postIsSensitive: Bool? + public let postLanguage: String? + public let autoExpandMedia: AutoExpandMedia? + public let autoExpandSpoilers: Bool? + + public enum AutoExpandMedia: String, Decodable, CaseIterable { + case showAll = "show_all" + case hideAll = "hide_all" + case hideSensitive = "default" + + public var description: LocalizedStringKey { + switch self { + case .showAll: + "enum.expand-media.show" + case .hideAll: + "enum.expand-media.hide" + case .hideSensitive: + "enum.expand-media.hide-sensitive" + } + } + } + + enum CodingKeys: String, CodingKey { + case postVisibility = "posting:default:visibility" + case postIsSensitive = "posting:default:sensitive" + case postLanguage = "posting:default:language" + case autoExpandMedia = "reading:expand:media" + case autoExpandSpoilers = "reading:expand:spoilers" + } +} + +extension ServerPreferences: Sendable {} +extension ServerPreferences.AutoExpandMedia: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Status.swift b/Threaded/Packages/Models/Sources/Models/Status.swift new file mode 100644 index 0000000..b2b0029 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Status.swift @@ -0,0 +1,260 @@ +import Foundation + +public enum Visibility: String, Codable, CaseIterable, Hashable, Equatable, Sendable { + case pub = "public" + case unlisted + case priv = "private" + case direct +} + +public protocol AnyStatus { + var viewId: StatusViewId { get } + var id: String { get } + var content: HTMLString { get } + var account: Account { get } + var createdAt: ServerDate { get } + var editedAt: ServerDate? { get } + var mediaAttachments: [MediaAttachment] { get } + var mentions: [Mention] { get } + var repliesCount: Int { get } + var reblogsCount: Int { get } + var favouritesCount: Int { get } + var card: Card? { get } + var favourited: Bool? { get } + var reblogged: Bool? { get } + var pinned: Bool? { get } + var bookmarked: Bool? { get } + var emojis: [Emoji] { get } + var url: String? { get } + var application: Application? { get } + var inReplyToId: String? { get } + var inReplyToAccountId: String? { get } + var visibility: Visibility { get } + var poll: Poll? { get } + var spoilerText: HTMLString { get } + var filtered: [Filtered]? { get } + var sensitive: Bool { get } + var language: String? { get } +} + +public struct StatusViewId: Hashable { + let id: String + let editedAt: Date? +} + +public extension AnyStatus { + var viewId: StatusViewId { + StatusViewId(id: id, editedAt: editedAt?.asDate) + } +} + +public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable { + public static func == (lhs: Status, rhs: Status) -> Bool { + lhs.id == rhs.id && lhs.viewId == rhs.viewId + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let id: String + public let content: HTMLString + public let account: Account + public let createdAt: ServerDate + public let editedAt: ServerDate? + public let reblog: ReblogStatus? + public let mediaAttachments: [MediaAttachment] + public let mentions: [Mention] + public let repliesCount: Int + public let reblogsCount: Int + public let favouritesCount: Int + public let card: Card? + public let favourited: Bool? + public let reblogged: Bool? + public let pinned: Bool? + public let bookmarked: Bool? + public let emojis: [Emoji] + public let url: String? + public let application: Application? + public let inReplyToId: String? + public let inReplyToAccountId: String? + public let visibility: Visibility + public let poll: Poll? + public let spoilerText: HTMLString + public let filtered: [Filtered]? + public let sensitive: Bool + public let language: String? + + public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { + self.id = id + self.content = content + self.account = account + self.createdAt = createdAt + self.editedAt = editedAt + self.reblog = reblog + self.mediaAttachments = mediaAttachments + self.mentions = mentions + self.repliesCount = repliesCount + self.reblogsCount = reblogsCount + self.favouritesCount = favouritesCount + self.card = card + self.favourited = favourited + self.reblogged = reblogged + self.pinned = pinned + self.bookmarked = bookmarked + self.emojis = emojis + self.url = url + self.application = application + self.inReplyToId = inReplyToId + self.inReplyToAccountId = inReplyToAccountId + self.visibility = visibility + self.poll = poll + self.spoilerText = spoilerText + self.filtered = filtered + self.sensitive = sensitive + self.language = language + } + + public static func placeholder(forSettings: Bool = false, language: String? = nil) -> Status { + .init(id: UUID().uuidString, + content: .init(stringValue: "Here's to the [#crazy](#) ones. The misfits.\nThe [@rebels](#). The troublemakers.", + parseMarkdown: forSettings), + + account: .placeholder(), + createdAt: ServerDate(), + editedAt: nil, + reblog: nil, + mediaAttachments: [], + mentions: [], + repliesCount: 0, + reblogsCount: 0, + favouritesCount: 0, + card: nil, + favourited: false, + reblogged: false, + pinned: false, + bookmarked: false, + emojis: [], + url: "https://example.com", + application: nil, + inReplyToId: nil, + inReplyToAccountId: nil, + visibility: .pub, + poll: nil, + spoilerText: .init(stringValue: ""), + filtered: [], + sensitive: false, + language: language) + } + + public static func placeholders() -> [Status] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } + + public var reblogAsAsStatus: Status? { + if let reblog { + return .init(id: reblog.id, + content: reblog.content, + account: reblog.account, + createdAt: reblog.createdAt, + editedAt: reblog.editedAt, + reblog: nil, + mediaAttachments: reblog.mediaAttachments, + mentions: reblog.mentions, + repliesCount: reblog.repliesCount, + reblogsCount: reblog.reblogsCount, + favouritesCount: reblog.favouritesCount, + card: reblog.card, + favourited: reblog.favourited, + reblogged: reblog.reblogged, + pinned: reblog.pinned, + bookmarked: reblog.bookmarked, + emojis: reblog.emojis, + url: reblog.url, + application: reblog.application, + inReplyToId: reblog.inReplyToId, + inReplyToAccountId: reblog.inReplyToAccountId, + visibility: reblog.visibility, + poll: reblog.poll, + spoilerText: reblog.spoilerText, + filtered: reblog.filtered, + sensitive: reblog.sensitive, + language: reblog.language) + } + return nil + } +} + +public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Hashable { + public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let id: String + public let content: HTMLString + public let account: Account + public let createdAt: ServerDate + public let editedAt: ServerDate? + public let mediaAttachments: [MediaAttachment] + public let mentions: [Mention] + public let repliesCount: Int + public let reblogsCount: Int + public let favouritesCount: Int + public let card: Card? + public let favourited: Bool? + public let reblogged: Bool? + public let pinned: Bool? + public let bookmarked: Bool? + public let emojis: [Emoji] + public let url: String? + public let application: Application? + public let inReplyToId: String? + public let inReplyToAccountId: String? + public let visibility: Visibility + public let poll: Poll? + public let spoilerText: HTMLString + public let filtered: [Filtered]? + public let sensitive: Bool + public let language: String? + + public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application? = nil, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { + self.id = id + self.content = content + self.account = account + self.createdAt = createdAt + self.editedAt = editedAt + self.mediaAttachments = mediaAttachments + self.mentions = mentions + self.repliesCount = repliesCount + self.reblogsCount = reblogsCount + self.favouritesCount = favouritesCount + self.card = card + self.favourited = favourited + self.reblogged = reblogged + self.pinned = pinned + self.bookmarked = bookmarked + self.emojis = emojis + self.url = url + self.application = application + self.inReplyToId = inReplyToId + self.inReplyToAccountId = inReplyToAccountId + self.visibility = visibility + self.poll = poll + self.spoilerText = spoilerText + self.filtered = filtered + self.sensitive = sensitive + self.language = language + } +} + +extension StatusViewId: Sendable {} + +// Every property in Status is immutable. +extension Status: Sendable {} + +// Every property in ReblogStatus is immutable. +extension ReblogStatus: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/StatusContext.swift b/Threaded/Packages/Models/Sources/Models/StatusContext.swift new file mode 100644 index 0000000..63f81bd --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/StatusContext.swift @@ -0,0 +1,12 @@ +import Foundation + +public struct StatusContext: Decodable { + public let ancestors: [Status] + public let descendants: [Status] + + public static func empty() -> StatusContext { + .init(ancestors: [], descendants: []) + } +} + +extension StatusContext: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/StatusHistory.swift b/Threaded/Packages/Models/Sources/Models/StatusHistory.swift new file mode 100644 index 0000000..1d0f7fb --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/StatusHistory.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct StatusHistory: Decodable, Identifiable { + public var id: String { + createdAt.asDate.description + } + + public let content: HTMLString + public let createdAt: ServerDate + public let emojis: [Emoji] +} + +extension StatusHistory: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Stream/StreamEvent.swift b/Threaded/Packages/Models/Sources/Models/Stream/StreamEvent.swift new file mode 100644 index 0000000..1e06719 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Stream/StreamEvent.swift @@ -0,0 +1,57 @@ +import Foundation + +public struct RawStreamEvent: Decodable { + public let event: String + public let stream: [String] + public let payload: String +} + +public protocol StreamEvent: Identifiable { + var date: Date { get } + var id: String { get } +} + +public struct StreamEventUpdate: StreamEvent { + public let date = Date() + public var id: String { status.id } + public let status: Status + public init(status: Status) { + self.status = status + } +} + +public struct StreamEventStatusUpdate: StreamEvent { + public let date = Date() + public var id: String { status.id } + public let status: Status + public init(status: Status) { + self.status = status + } +} + +public struct StreamEventDelete: StreamEvent { + public let date = Date() + public var id: String { status + date.description } + public let status: String + public init(status: String) { + self.status = status + } +} + +public struct StreamEventNotification: StreamEvent { + public let date = Date() + public var id: String { notification.id } + public let notification: Notification + public init(notification: Notification) { + self.notification = notification + } +} + +public struct StreamEventConversation: StreamEvent { + public let date = Date() + public var id: String { conversation.id } + public let conversation: Conversation + public init(conversation: Conversation) { + self.conversation = conversation + } +} diff --git a/Threaded/Packages/Models/Sources/Models/Stream/StreamMessage.swift b/Threaded/Packages/Models/Sources/Models/Stream/StreamMessage.swift new file mode 100644 index 0000000..c667987 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Stream/StreamMessage.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct StreamMessage: Encodable { + public let type: String + public let stream: String + + public init(type: String, stream: String) { + self.type = type + self.stream = stream + } +} diff --git a/Threaded/Packages/Models/Sources/Models/SwiftData/Draft.swift b/Threaded/Packages/Models/Sources/Models/SwiftData/Draft.swift new file mode 100644 index 0000000..34418c8 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/SwiftData/Draft.swift @@ -0,0 +1,13 @@ +import Foundation +import SwiftData +import SwiftUI + +@Model public class Draft { + public var content: String = "" + public var creationDate: Date = Date() + + public init(content: String) { + self.content = content + creationDate = Date() + } +} diff --git a/Threaded/Packages/Models/Sources/Models/SwiftData/LocalTimeline.swift b/Threaded/Packages/Models/Sources/Models/SwiftData/LocalTimeline.swift new file mode 100644 index 0000000..84319a1 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/SwiftData/LocalTimeline.swift @@ -0,0 +1,13 @@ +import Foundation +import SwiftData +import SwiftUI + +@Model public class LocalTimeline { + public var instance: String = "" + public var creationDate: Date = Date() + + public init(instance: String) { + self.instance = instance + creationDate = Date() + } +} diff --git a/Threaded/Packages/Models/Sources/Models/SwiftData/TagGroup.swift b/Threaded/Packages/Models/Sources/Models/SwiftData/TagGroup.swift new file mode 100644 index 0000000..de8acfe --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/SwiftData/TagGroup.swift @@ -0,0 +1,17 @@ +import Foundation +import SwiftData +import SwiftUI + +@Model public class TagGroup: Equatable { + public var title: String = "" + public var symbolName: String = "" + public var tags: [String] = [] + public var creationDate: Date = Date() + + public init(title: String, symbolName: String, tags: [String]) { + self.title = title + self.symbolName = symbolName + self.tags = tags + creationDate = Date() + } +} diff --git a/Threaded/Packages/Models/Sources/Models/Tag.swift b/Threaded/Packages/Models/Sources/Models/Tag.swift new file mode 100644 index 0000000..0069dc4 --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Tag.swift @@ -0,0 +1,67 @@ +import Foundation + +public struct Tag: Codable, Identifiable, Equatable, Hashable { + public struct History: Codable, Identifiable { + public var id: String { + day + } + public let day: String + public let accounts: String + public let uses: String + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + } + + public static func == (lhs: Tag, rhs: Tag) -> Bool { + lhs.name == rhs.name + } + + public var id: String { + name + } + + public let name: String + public let url: String + public let following: Bool + public let history: [History] + + public var totalUses: Int { + history.compactMap { Int($0.uses) }.reduce(0, +) + } + + public var totalAccounts: Int { + history.compactMap { Int($0.accounts) }.reduce(0, +) + } +} + +public struct FeaturedTag: Codable, Identifiable { + public let id: String + public let name: String + public let url: URL + public let statusesCount: String + public var statusesCountInt: Int { + Int(statusesCount) ?? 0 + } + + private enum CodingKeys: String, CodingKey { + case id, name, url, statusesCount + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + url = try container.decode(URL.self, forKey: .url) + do { + statusesCount = try container.decode(String.self, forKey: .statusesCount) + } catch DecodingError.typeMismatch { + statusesCount = try String(container.decode(Int.self, forKey: .statusesCount)) + } + } +} + +extension Tag: Sendable {} +extension Tag.History: Sendable {} +extension FeaturedTag: Sendable {} diff --git a/Threaded/Packages/Models/Sources/Models/Translation.swift b/Threaded/Packages/Models/Sources/Models/Translation.swift new file mode 100644 index 0000000..2b16e5f --- /dev/null +++ b/Threaded/Packages/Models/Sources/Models/Translation.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct Translation: Decodable { + public let content: HTMLString + public let detectedSourceLanguage: String + public let provider: String + + public init(content: String, detectedSourceLanguage: String, provider: String) { + self.content = .init(stringValue: content) + self.detectedSourceLanguage = detectedSourceLanguage + self.provider = provider + } +} + +extension Translation: Sendable {} diff --git a/Threaded/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift b/Threaded/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift new file mode 100644 index 0000000..9b47238 --- /dev/null +++ b/Threaded/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift @@ -0,0 +1,91 @@ +@testable import Models +import XCTest + +final class HTMLStringTests: XCTestCase { + func testURLInit() throws { + XCTAssertNil(URL(string: "go to www.google.com", encodePath: true)) + XCTAssertNil(URL(string: "go to www.google.com", encodePath: false)) + XCTAssertNil(URL(string: "", encodePath: true)) + + let simpleUrl = URL(string: "https://www.google.com", encodePath: true) + XCTAssertEqual("https://www.google.com", simpleUrl?.absoluteString) + + let urlWithTrailingSlash = URL(string: "https://www.google.com/", encodePath: true) + XCTAssertEqual("https://www.google.com/", urlWithTrailingSlash?.absoluteString) + + let extendedCharPath = URL(string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: true) + XCTAssertEqual("https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", extendedCharPath?.absoluteString) + XCTAssertNil(URL(string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: false)) + + let extendedCharQuery = URL(string: "http://test.com/blah/city?name=京都市", encodePath: true) + XCTAssertEqual("http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82", extendedCharQuery?.absoluteString) + + // Double encoding will happen if you ask to encodePath on an already encoded string + let alreadyEncodedPath = URL(string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true) + XCTAssertEqual("https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station", alreadyEncodedPath?.absoluteString) + } + + func testHTMLStringInit() throws { + let decoder = JSONDecoder() + + let basicContent = "\"

    This is a test

    \"" + var htmlString = try decoder.decode(HTMLString.self, from: Data(basicContent.utf8)) + XCTAssertEqual("This is a test", htmlString.asRawText) + XCTAssertEqual("

    This is a test

    ", htmlString.htmlValue) + XCTAssertEqual("This is a test", htmlString.asMarkdown) + XCTAssertEqual(0, htmlString.statusesURLs.count) + XCTAssertEqual(0, htmlString.links.count) + + let basicLink = "\"

    This is a test

    \"" + htmlString = try decoder.decode(HTMLString.self, from: Data(basicLink.utf8)) + XCTAssertEqual("This is a test", htmlString.asRawText) + XCTAssertEqual("

    This is a test

    ", htmlString.htmlValue) + XCTAssertEqual("This is a [test](https://test.com)", htmlString.asMarkdown) + XCTAssertEqual(0, htmlString.statusesURLs.count) + XCTAssertEqual(1, htmlString.links.count) + XCTAssertEqual("https://test.com", htmlString.links[0].url.absoluteString) + XCTAssertEqual("test", htmlString.links[0].displayString) + + let extendedCharLink = "\"

    This is a test

    \"" + htmlString = try decoder.decode(HTMLString.self, from: Data(extendedCharLink.utf8)) + XCTAssertEqual("This is a test", htmlString.asRawText) + XCTAssertEqual("

    This is a test

    ", htmlString.htmlValue) + XCTAssertEqual("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)", htmlString.asMarkdown) + XCTAssertEqual(0, htmlString.statusesURLs.count) + XCTAssertEqual(1, htmlString.links.count) + XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString) + XCTAssertEqual("test", htmlString.links[0].displayString) + + let alreadyEncodedLink = "\"

    This is a test

    \"" + htmlString = try decoder.decode(HTMLString.self, from: Data(alreadyEncodedLink.utf8)) + XCTAssertEqual("This is a test", htmlString.asRawText) + XCTAssertEqual("

    This is a test

    ", htmlString.htmlValue) + XCTAssertEqual("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)", htmlString.asMarkdown) + XCTAssertEqual(0, htmlString.statusesURLs.count) + XCTAssertEqual(1, htmlString.links.count) + XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString) + XCTAssertEqual("test", htmlString.links[0].displayString) + } + + func testHTMLStringInit_markdownEscaping() throws { + let decoder = JSONDecoder() + + let stdMarkdownContent = "\"

    This [*is*] `a`\\n**test**

    \"" + var htmlString = try decoder.decode(HTMLString.self, from: Data(stdMarkdownContent.utf8)) + XCTAssertEqual("This [*is*] `a`\n**test**", htmlString.asRawText) + XCTAssertEqual("

    This [*is*] `a`\n**test**

    ", htmlString.htmlValue) + XCTAssertEqual("This \\[\\*is\\*] \\`a\\` \\*\\*test\\*\\*", htmlString.asMarkdown) + + let underscoreContent = "\"

    This _is_ an :emoji_maybe:

    \"" + htmlString = try decoder.decode(HTMLString.self, from: Data(underscoreContent.utf8)) + XCTAssertEqual("This _is_ an :emoji_maybe:", htmlString.asRawText) + XCTAssertEqual("

    This _is_ an :emoji_maybe:

    ", htmlString.htmlValue) + XCTAssertEqual("This \\_is\\_ an :emoji_maybe:", htmlString.asMarkdown) + + let strikeContent = "\"

    This ~is~ a\\n`test`

    \"" + htmlString = try decoder.decode(HTMLString.self, from: Data(strikeContent.utf8)) + XCTAssertEqual("This ~is~ a\n`test`", htmlString.asRawText) + XCTAssertEqual("

    This ~is~ a\n`test`

    ", htmlString.htmlValue) + XCTAssertEqual("This \\~is\\~ a \\`test\\`", htmlString.asMarkdown) + } +} diff --git a/Threaded/Preview Content/Preview Assets.xcassets/Contents.json b/Threaded/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Threaded/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Threaded/ThreadedApp.swift b/Threaded/ThreadedApp.swift new file mode 100644 index 0000000..5576cbc --- /dev/null +++ b/Threaded/ThreadedApp.swift @@ -0,0 +1,17 @@ +//Made by Lumaa + +import SwiftUI + +@main +struct ThreadedApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .tint(Color(uiColor: .label)) + .background(Color.appBackground) + .onAppear { + HapticManager.prepareHaptics() + } + } + } +} diff --git a/Threaded/Views/AccountView.swift b/Threaded/Views/AccountView.swift new file mode 100644 index 0000000..8519981 --- /dev/null +++ b/Threaded/Views/AccountView.swift @@ -0,0 +1,179 @@ +//Made by Lumaa + +import SwiftUI + +struct AccountView: View { + @Environment(Client.self) private var client: Client + + @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 account: Account + private let animPicCurve = Animation.smooth(duration: 0.25, extraBounce: 0.0) + + var body: some View { + ZStack (alignment: .center) { + if account != Account.placeholder() { + if biggerPicture { + big + } else { + wholeSmall + } + } else { + loading + } + } + .refreshable { + if let ref: Account = try? await client.get(endpoint: Accounts.accounts(id: account.id)) { + account = ref + } + } + .background(Color.appBackground) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(Color.appBackground, for: .navigationBar) + .toolbarBackground(.automatic, for: .navigationBar) + } + + // MARK: - Headers + + var wholeSmall: some View { + ScrollView { + VStack { + unbig + + HStack { + Text(account.note.asRawText) + .font(.body) + .multilineTextAlignment(.leading) + + Spacer() + } + } + .safeAreaPadding(.vertical) + .padding(.horizontal) + } + .withAppRouter() + } + + var loading: some View { + ScrollView { + VStack { + unbig + .redacted(reason: .placeholder) + + HStack { + Text(account.note.asRawText) + .font(.body) + .multilineTextAlignment(.leading) + .redacted(reason: .placeholder) + + Spacer() + } + } + .safeAreaPadding(.vertical) + .padding(.horizontal) + } + .withAppRouter() + } + + var unbig: some View { + HStack { + if account.displayName != nil { + VStack(alignment: .leading) { + Text(account.displayName!) + .font(.title2.bold()) + .multilineTextAlignment(.leading) + .lineLimit(1) + + let server = account.acct.split(separator: "@").last + + HStack(alignment: .center) { + if server != nil { + if server! != account.username { + Text("\(account.username)") + .font(.body) + .multilineTextAlignment(.leading) + + Text("\(server!.description)") + .font(.caption) + .foregroundStyle(Color.gray) + .multilineTextAlignment(.leading) + .pill() + } else { + Text("\(account.username)") + .font(.body) + .multilineTextAlignment(.leading) + + Text("\(client.server)") + .font(.caption) + .foregroundStyle(Color.gray) + .multilineTextAlignment(.leading) + .pill() + } + } else { + Text("\(account.username)") + .font(.body) + .multilineTextAlignment(.leading) + + Text("\(client.server)") + .font(.caption) + .foregroundStyle(Color.gray) + .multilineTextAlignment(.leading) + .pill() + } + } + } + } else { + Text(account.acct) + .font(.headline) + } + + Spacer() + + profilePicture + .frame(width: 75, height: 75) + } + } + + var big: some View { + ZStack (alignment: .center) { + Rectangle() + .fill(Material.ultraThin) + .ignoresSafeArea() + .onTapGesture { + withAnimation(animPicCurve) { + biggerPicture.toggle() + } + } + + profilePicture + .frame(width: 300, height: 300) + } + .zIndex(20) + } + + var profilePicture: some View { + OnlineImage(url: account.avatar) + .clipShape(.circle) + .matchedGeometryEffect(id: animPicture, in: accountAnims) + .onTapGesture { + withAnimation(animPicCurve) { + biggerPicture.toggle() + } + } + } +} + +private extension View { + func pill() -> some View { + self + .padding([.horizontal], 10) + .padding([.vertical], 5) + .background(Color(uiColor: UIColor.label).opacity(0.1)) + .clipShape(.capsule) + } +} diff --git a/Threaded/Views/AddInstanceView.swift b/Threaded/Views/AddInstanceView.swift new file mode 100644 index 0000000..bfe4dab --- /dev/null +++ b/Threaded/Views/AddInstanceView.swift @@ -0,0 +1,160 @@ +//Made by Lumaa + +import SwiftUI +import AuthenticationServices + +struct AddInstanceView: View { + @Environment(\.webAuthenticationSession) private var webAuthenticationSession + @Environment(\.dismiss) private var dismiss + + // Instance URL and verify + @State private var instanceUrl: String = "" + @State private var verifying: Bool = false + @State private var verified: Bool = false + @State private var verifyError: Bool = false + @State private var instanceInfo: Instance? + + @State private var signInClient: Client? + @Binding public var logged: Bool + + var body: some View { + Form { + Section { + TextField("login.mastodon.instance", text: $instanceUrl) + .keyboardType(.URL) + .textContentType(.URL) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .disabled(verifying) + + if !verifying { + if !verified { + Button { + verify() + } label: { + Text("login.mastodon.verify") + .disabled(instanceUrl.isEmpty) + } + .buttonStyle(.bordered) + + if verifyError == true { + Text("login.mastodon.verify-error") + .foregroundStyle(.red) + } + } else { + Button { + Task { + await signIn() + } + } label: { + Text("login.mastodon.login") + .disabled(instanceUrl.isEmpty) + } + .buttonStyle(.bordered) + } + } else { + ProgressView() + .progressViewStyle(.circular) + .tint(Color.white) + .foregroundStyle(Color.white) + } + } + + if verified && instanceInfo != nil { + Section { + VStack(alignment: .leading) { + Text(instanceInfo!.title) + .font(.headline) + Text(instanceInfo!.shortDescription) + } + + VStack(alignment: .leading, spacing: 15) { + Text("instance.rules") + .font(.headline) + + if !(instanceInfo!.rules?.isEmpty ?? true) { + ForEach(instanceInfo!.rules!) { rule in + Text(rule.text) + } + } + } + } + } + } + .onChange(of: instanceUrl) { _, newValue in + guard !self.verifying else { return } + verified = false + } + } + + func verify() { + withAnimation { + verifying = true + verified = false + verifyError = false + } + + let cleanInstance = instanceUrl + .replacingOccurrences(of: "http://", with: "") + .replacingOccurrences(of: "https://", with: "") + + let client = Client(server: cleanInstance) + + Task { + do { + let instance: Instance = try await client.get(endpoint: Instances.instance) + + withAnimation { + instanceInfo = instance + verifying = false + verified = true + verifyError = false + } + } catch { + print(error.localizedDescription) + + withAnimation { + verifying = false + verified = false + verifyError = true + } + } + } + } + + private func signIn() async { + let cleanInstance = instanceUrl + .replacingOccurrences(of: "http://", with: "") + .replacingOccurrences(of: "https://", with: "") + + signInClient = .init(server: cleanInstance) + if let oauthURL = try? await signInClient?.oauthURL(), + let url = try? await webAuthenticationSession.authenticate(using: oauthURL, callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: "")) { + await continueSignIn(url: url) + } + } + + private func continueSignIn(url: URL) async { + guard let client = signInClient else { + return + } + + do { + let oauthToken = try await client.continueOauthFlow(url: url) + let client = Client(server: client.server, oauthToken: oauthToken) + let account: Account = try await client.get(endpoint: Accounts.verifyCredentials) + let appAcc = AppAccount(server: client.server, accountName: "\(account.acct)@\(client.server)", oauthToken: oauthToken) + try appAcc.saveAsCurrent() + + signInClient = client + logged = true + dismiss() + } catch { + print(error) + } + } +} + +#Preview { + AddInstanceView(logged: .constant(false)) +} diff --git a/Threaded/Views/ConnectView.swift b/Threaded/Views/ConnectView.swift new file mode 100644 index 0000000..d1d8e5c --- /dev/null +++ b/Threaded/Views/ConnectView.swift @@ -0,0 +1,82 @@ +//Made by Lumaa + +import SwiftUI + +struct ConnectView: View { + @Environment(\.dismiss) private var dismiss + + @State private var sheet: SheetDestination? + @State private var logged: Bool = false + + var body: some View { + VStack { + Text("login.title") + .font(.title.bold()) + .multilineTextAlignment(.center) + + Spacer() + + VStack(spacing: 30) { + Button { + sheet = .mastodonLogin(logged: $logged) + } label: { + mastodon + } + .buttonStyle(LargeButton()) + + Button { + print("go directly") + } label: { + noAccount + } + .buttonStyle(LargeButton()) + } + .padding(.vertical, 100) + } + .withSheets(sheetDestination: $sheet) + .safeAreaPadding() + .onChange(of: logged) { _, newValue in + if newValue == true { + dismiss() + } + } + } + + var mastodon: some View { + HStack(alignment: .top) { + VStack(alignment: .leading) { + Text("login.mastodon") + Text("login.mastodon.footer") + .multilineTextAlignment(.leading) + .font(.caption) + .foregroundStyle(.gray) + } + Spacer() + Image("MastodonMark") + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + } + } + + var noAccount: some View { + HStack(alignment: .top) { + VStack(alignment: .leading) { + Text("login.no-account") + Text("login.no-account.footer") + .multilineTextAlignment(.leading) + .font(.caption) + .foregroundStyle(.gray) + } + Spacer() + Image(systemName: "person.crop.circle.dashed") + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + } + } +} + +#Preview { + ConnectView() +} diff --git a/Threaded/Views/ContentView.swift b/Threaded/Views/ContentView.swift new file mode 100644 index 0000000..068acad --- /dev/null +++ b/Threaded/Views/ContentView.swift @@ -0,0 +1,76 @@ +//Made by Lumaa + +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? + + var body: some View { + TabView(selection: $navigator.selectedTab, content: { + ZStack { + if client != nil { + TimelineView(timelineModel: FetchTimeline(client: self.client!)) + .background(Color.appBackground) + .safeAreaPadding() + } else { + ZStack { + Color.appBackground + .ignoresSafeArea() + } + } + } + .background(Color.appBackground) + .tag(TabDestination.timeline) + + Text(String("Search")) + .background(Color.appBackground) + .tag(TabDestination.search) + + Text(String("Activity")) + .background(Color.appBackground) + .tag(TabDestination.activity) + + ProfileView(account: currentAccount ?? .placeholder()) + .background(Color.appBackground) + .tag(TabDestination.profile) + }) + .overlay(alignment: .bottom) { + TabsView(navigator: navigator) + .safeAreaPadding(.vertical) + .zIndex(10) + } + .withCovers(sheetDestination: $sheet) + .environment(navigator) + .environment(client) + .onAppear { + 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) + } + } + } + } + + init() { + let appearance = UITabBarAppearance() + appearance.configureWithTransparentBackground() + appearance.stackedLayoutAppearance.normal.iconColor = .white + appearance.stackedLayoutAppearance.normal.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] + + appearance.stackedLayoutAppearance.selected.iconColor = UIColor(Color.accentColor) + appearance.stackedLayoutAppearance.selected.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)] + + UITabBar.appearance().standardAppearance = appearance + } +} + +#Preview { + ContentView() +} diff --git a/Threaded/Views/ProfileView.swift b/Threaded/Views/ProfileView.swift new file mode 100644 index 0000000..f388a36 --- /dev/null +++ b/Threaded/Views/ProfileView.swift @@ -0,0 +1,240 @@ +//Made by Lumaa + +import SwiftUI + +struct ProfileView: View { + @Environment(Client.self) private var client: Client + + @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 account: Account + private let isCurrent: Bool = true + private let animPicCurve = Animation.smooth(duration: 0.25, extraBounce: 0.0) + + var body: some View { + NavigationStack(path: $navigator.path) { + ZStack (alignment: .center) { + if account != Account.placeholder() { + if biggerPicture { + big + } else { + wholeSmall + } + } else { + loading + } + } + } + .refreshable { + if isCurrent { + do { + if let saved: AppAccount = try AppAccount.loadAsCurrent() { + let cli: Client = Client(server: saved.server, oauthToken: saved.oauthToken) + let acc: Account? = try await client.get(endpoint: Accounts.verifyCredentials) + account = acc ?? Account.placeholder() + } + } catch { + print(error) + } + } else { + if let ref: Account = try? await client.get(endpoint: Accounts.accounts(id: account.id)) { + account = ref + } + } + } + .environment(navigator) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(Color(uiColor: UIColor.systemBackground), for: .navigationBar) + .toolbarBackground(.automatic, for: .navigationBar) + } + + // MARK: - Headers + + var wholeSmall: some View { + ScrollView { + VStack { + unbig + + HStack { + Text(account.note.asRawText) + .font(.body) + .multilineTextAlignment(.leading) + + Spacer() + } + } + .safeAreaPadding(.vertical) + .padding(.horizontal) + .offset(y: 50) + .overlay(alignment: .top) { + 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) + } + } + .safeAreaPadding() + .background(Color(uiColor: UIColor.systemBackground)) + } + } + .withAppRouter() + } + + var loading: some View { + ScrollView { + VStack { + unbig + .redacted(reason: .placeholder) + + HStack { + Text(account.note.asRawText) + .font(.body) + .multilineTextAlignment(.leading) + .redacted(reason: .placeholder) + + Spacer() + } + } + .safeAreaPadding(.vertical) + .padding(.horizontal) + .offset(y: 50) + .overlay(alignment: .top) { + HStack { + Button { + navigator.navigate(to: .privacy) + } label: { + Image(systemName: "globe") + .font(.title2) + } + .disabled(true) + + Spacer() // middle seperation + + Button { + navigator.navigate(to: .settings) + } label: { + Image(systemName: "text.alignright") + .font(.title2) + } + .disabled(true) + } + .safeAreaPadding() + .background(Color(uiColor: UIColor.systemBackground)) + } + } + .withAppRouter() + } + + var unbig: some View { + HStack { + if account.displayName != nil { + VStack(alignment: .leading) { + Text(account.displayName!) + .font(.title2.bold()) + .multilineTextAlignment(.leading) + .lineLimit(1) + + let server = account.acct.split(separator: "@").last + + HStack(alignment: .center) { + if server != nil { + if server! != account.username { + Text("\(account.username)") + .font(.body) + .multilineTextAlignment(.leading) + + Text("\(server!.description)") + .font(.caption) + .foregroundStyle(Color.gray) + .multilineTextAlignment(.leading) + .pill() + } else { + Text("\(account.username)") + .font(.body) + .multilineTextAlignment(.leading) + + Text("\(client.server)") + .font(.caption) + .foregroundStyle(Color.gray) + .multilineTextAlignment(.leading) + .pill() + } + } else { + Text("\(account.username)") + .font(.body) + .multilineTextAlignment(.leading) + + Text("\(client.server)") + .font(.caption) + .foregroundStyle(Color.gray) + .multilineTextAlignment(.leading) + .pill() + } + } + } + } else { + Text(account.acct) + .font(.headline) + } + + Spacer() + + profilePicture + .frame(width: 75, height: 75) + } + } + + var big: some View { + ZStack (alignment: .center) { + Rectangle() + .fill(Material.ultraThin) + .ignoresSafeArea() + .onTapGesture { + withAnimation(animPicCurve) { + biggerPicture.toggle() + } + } + + profilePicture + .frame(width: 300, height: 300) + } + .zIndex(20) + } + + var profilePicture: some View { + OnlineImage(url: account.avatar) + .clipShape(.circle) + .matchedGeometryEffect(id: animPicture, in: accountAnims) + .onTapGesture { + withAnimation(animPicCurve) { + biggerPicture.toggle() + } + } + } +} + +private extension View { + func pill() -> some View { + self + .padding([.horizontal], 10) + .padding([.vertical], 5) + .background(Color(uiColor: UIColor.label).opacity(0.1)) + .clipShape(.capsule) + } +} diff --git a/Threaded/Views/Settings/PrivacyView.swift b/Threaded/Views/Settings/PrivacyView.swift new file mode 100644 index 0000000..0af9936 --- /dev/null +++ b/Threaded/Views/Settings/PrivacyView.swift @@ -0,0 +1,13 @@ +//Made by Lumaa + +import SwiftUI + +struct PrivacyView: View { + var body: some View { + Text("setting.privacy") + } +} + +#Preview { + PrivacyView() +} diff --git a/Threaded/Views/Settings/SettingsView.swift b/Threaded/Views/Settings/SettingsView.swift new file mode 100644 index 0000000..ebbe61a --- /dev/null +++ b/Threaded/Views/Settings/SettingsView.swift @@ -0,0 +1,36 @@ +//Made by Lumaa + +import SwiftUI + +struct SettingsView: View { + @Environment(Navigator.self) private var navigator: Navigator + @State private var sheet: SheetDestination? + + var body: some View { + List { + Button { + navigator.navigate(to: .privacy) + } label: { + Label("setting.privacy", systemImage: "lock") + } +// .listRowSeparator(.hidden) + .listRowSeparator(.visible) + + Button { + UserDefaults.standard.removeObject(forKey: AppAccount.saveKey) + sheet = .welcome + } label: { + Text("logout") + } + .tint(Color.red) + .listRowSeparator(.hidden) + } + .withCovers(sheetDestination: $sheet) + .listStyle(.inset) + .navigationTitle("settings") + } +} + +#Preview { + SettingsView() +} diff --git a/Threaded/Views/TimelineView.swift b/Threaded/Views/TimelineView.swift new file mode 100644 index 0000000..7ecef21 --- /dev/null +++ b/Threaded/Views/TimelineView.swift @@ -0,0 +1,52 @@ +//Made by Lumaa + +import SwiftUI + +struct TimelineView: View { + @Environment(Client.self) private var client: Client + + @State private var navigator: Navigator = Navigator() + @State private var statuses: [Status]? + + @State var timelineModel: FetchTimeline + + var body: some View { + NavigationStack(path: $navigator.path) { + if statuses != nil { + ScrollView(showsIndicators: false) { + Image("HeroIcon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30) + .padding(.bottom) + + ForEach(statuses!, id: \.id) { status in + VStack(spacing: 2) { + CompactPostView(status: status, navigator: navigator) + } + } + } + .padding(.top) + .background(Color.appBackground) + .withAppRouter() + } else { + ZStack { + Color.appBackground + .ignoresSafeArea() + .onAppear { + Task { + statuses = try? await client.get(endpoint: Timelines.home(sinceId: nil, maxId: nil, minId: nil)) + } + } + + ProgressView() + .progressViewStyle(.circular) + } + } + } + .background(Color.appBackground) + .toolbarBackground(Color.appBackground, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .safeAreaPadding() + } +}

    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 some View { + configuration.label + .padding() + .background { + if filled { + Color(uiColor: UIColor.label) + } + } + .foregroundStyle(filled ? Color(uiColor: UIColor.systemBackground) : Color(uiColor: UIColor.label)) + .bold(filled) + .clipShape(.rect(cornerRadius: 15)) + .opacity(configuration.isPressed ? 0.3 : 1) + .overlay { + if !filled { + RoundedRectangle(cornerRadius: 15) + .stroke(Color(uiColor: UIColor.tertiaryLabel)) + .opacity(configuration.isPressed ? 0.3 : 1) + } + } + } +} + +struct NoTapAnimationStyle: PrimitiveButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .contentShape(Rectangle()) + .onTapGesture(perform: configuration.trigger) + } +} + +#Preview { + Button {} label: { + Text("Hello world") + } + .buttonStyle(NoTapAnimationStyle()) +} diff --git a/Threaded/Components/CompactPostView.swift b/Threaded/Components/CompactPostView.swift new file mode 100644 index 0000000..d53af92 --- /dev/null +++ b/Threaded/Components/CompactPostView.swift @@ -0,0 +1,345 @@ +//Made by Lumaa + +import SwiftUI + +struct CompactPostView: View { + @Environment(Client.self) private var client: Client + var status: Status + var navigator: Navigator + + @State private var isLiked: Bool = false + @State private var isReposted: Bool = false + + @State private var incrLike: Bool = false + + var body: some View { + VStack { + if status.reblog != nil { + VStack(alignment: .leading) { + repostNotice + .padding(.leading, 40) + + statusRepost + } + } else { + statusPost + } + + Rectangle() + .fill(Color.gray.opacity(0.2)) + .frame(width: .infinity, height: 1) + .padding(.bottom, 3) + } + .onAppear { + isLiked = status.reblog != nil ? status.reblog!.favourited ?? false : status.favourited ?? false + isReposted = status.reblog != nil ? status.reblog!.reblogged ?? false : status.reblogged ?? false + } + } + + func likePost() async throws { + 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) + + isLiked = !isLiked + let newStatus: Status = try await client.post(endpoint: endpoint) + if isLiked != newStatus.favourited { + isLiked = newStatus.favourited ?? !isLiked + } + } + + func repostPost() async throws { + 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) + + isReposted = !isReposted + let newStatus: Status = try await client.post(endpoint: endpoint) + if isReposted != newStatus.reblogged { + isReposted = newStatus.reblogged ?? !isReposted + } + } + + var statusPost: 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 { + profilePicture + .onTapGesture { + navigator.navigate(to: .account(acc: status.account)) + } + } + + VStack(alignment: .leading) { + // MARK: Status main content + VStack(alignment: .leading, spacing: 10) { + Text(status.account.username) + .multilineTextAlignment(.leading) + .bold() + .onTapGesture { + navigator.navigate(to: .account(acc: status.account)) + } + + Text(status.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 { + HapticManager.playHaptics(haptics: Haptic.tap) + try await likePost() + } 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 { + HapticManager.playHaptics(haptics: Haptic.tap) + try await repostPost() + } catch { + HapticManager.playHaptics(haptics: Haptic.error) + print("Error: \(error.localizedDescription)") + } + } + ShareLink(item: URL(string: status.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 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 { + HapticManager.playHaptics(haptics: Haptic.tap) + try await likePost() + incrLike = isLiked + } 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 { + HapticManager.playHaptics(haptics: Haptic.tap) + try await repostPost() + } 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 repostNotice: some View { + HStack (alignment:.center, spacing: 5) { + Image(systemName: "bolt.horizontal") + + Text("status.reposted-by.\(status.account.username)") + } + .padding(.leading, 25) + .multilineTextAlignment(.leading) + .lineLimit(1) + .font(.caption) + .foregroundStyle(Color(uiColor: UIColor.label).opacity(0.3)) + } + + var profilePicture: some View { + if status.reblog != nil { + OnlineImage(url: status.reblog!.account.avatar) + .frame(width: 40, height: 40) + .padding(.horizontal) + .clipShape(.circle) + } else { + OnlineImage(url: status.account.avatar) + .frame(width: 40, height: 40) + .padding(.horizontal) + .clipShape(.circle) + } + } + + var stats: some View { + if status.reblog == nil { + HStack { + if status.repliesCount > 0 { + Text("status.replies-\(status.repliesCount)") + .monospacedDigit() + .foregroundStyle(.gray) + } + + if status.repliesCount > 0 && (status.favouritesCount > 0 || isLiked) { + Text("•") + .foregroundStyle(.gray) + } + + if status.favouritesCount > 0 || isLiked { + let addedLike: Int = incrLike ? 1 : 0 + Text("status.favourites-\(status.favouritesCount + addedLike)") + .monospacedDigit() + .foregroundStyle(.gray) + .contentTransition(.numericText(value: Double(status.favouritesCount + addedLike))) + .transaction { t in + t.animation = .default + } + } + } + } else { + HStack { + if status.reblog!.repliesCount > 0 { + Text("status.replies-\(status.reblog!.repliesCount)") + .monospacedDigit() + .foregroundStyle(.gray) + } + + if status.reblog!.repliesCount > 0 && (status.reblog!.favouritesCount > 0 || isLiked) { + Text("•") + .foregroundStyle(.gray) + } + + if status.reblog!.favouritesCount > 0 || isLiked { + let addedLike: Int = incrLike ? 1 : 0 + Text("status.favourites-\(status.reblog!.favouritesCount + addedLike)") + .monospacedDigit() + .foregroundStyle(.gray) + .contentTransition(.numericText(value: Double(status.reblog!.favouritesCount + addedLike))) + .transaction { t in + t.animation = .default + } + } + } + } + } + + @ViewBuilder + func actionButton(_ image: String, action: @escaping () -> Void) -> some View { + Button { + action() + } label: { + Image(systemName: image) + .font(.title2) + } + .tint(Color(uiColor: UIColor.label)) + } + + @ViewBuilder + func asyncActionButton(_ image: String, action: @escaping () async -> Void) -> some View { + Button { + Task { + await action() + } + } label: { + Image(systemName: image) + .font(.title2) + } + .tint(Color(uiColor: UIColor.label)) + } +} + +#Preview { + ScrollView { + VStack { + ForEach(Status.placeholders()) { status in + CompactPostView(status: status, navigator: Navigator()) + .environment(Client.init(server: AppInfo.defaultServer)) + } + } + } +} diff --git a/Threaded/Components/OnlineImage.swift b/Threaded/Components/OnlineImage.swift new file mode 100644 index 0000000..7bc5f1a --- /dev/null +++ b/Threaded/Components/OnlineImage.swift @@ -0,0 +1,23 @@ +//Made by Lumaa + +import SwiftUI + +struct OnlineImage: View { + var url: URL + + var body: some View { + AsyncImage(url: url) { element in + element + .resizable() + .scaledToFit() + .aspectRatio(1.0, contentMode: .fit) + } placeholder: { + Rectangle() + .fill(Color.gray) + .overlay { + ProgressView() + .progressViewStyle(.circular) + } + } + } +} diff --git a/Threaded/Components/TabsNavs/TabsView.swift b/Threaded/Components/TabsNavs/TabsView.swift new file mode 100644 index 0000000..8929743 --- /dev/null +++ b/Threaded/Components/TabsNavs/TabsView.swift @@ -0,0 +1,138 @@ +//Made by Lumaa + +import SwiftUI + +struct TabsView: View { + @State var navigator: Navigator + + var body: some View { + HStack(alignment: .center) { + Button { + navigator.selectedTab = .timeline + } label: { + if navigator.selectedTab == .timeline { + Tabs.timeline.imageFill + } else { + Tabs.timeline.image + } + } + .buttonStyle(NoTapAnimationStyle()) + + Spacer() + + Button { + navigator.selectedTab = .search + } label: { + if navigator.selectedTab == .search { + Tabs.search.imageFill + } else { + Tabs.search.image + } + } + .buttonStyle(NoTapAnimationStyle()) + + Spacer() + + Button { + navigator.presentedSheet = .post + } label: { + Tabs.post.image + } + .buttonStyle(NoTapAnimationStyle()) + + Spacer() + + Button { + navigator.selectedTab = .activity + } label: { + if navigator.selectedTab == .activity { + Tabs.activity.imageFill + } else { + Tabs.activity.image + } + } + .buttonStyle(NoTapAnimationStyle()) + + Spacer() + + Button { + navigator.selectedTab = .profile + } label: { + if navigator.selectedTab == .profile { + Tabs.profile.imageFill + } else { + Tabs.profile.image + } + } + .buttonStyle(NoTapAnimationStyle()) + } + .withSheets(sheetDestination: $navigator.presentedSheet) + .padding(.horizontal, 30) + .background(Color.appBackground) + } +} + +enum Tabs { + case timeline + case search + case post + case activity + case profile + + @ViewBuilder + var image: some View { + switch self { + case .timeline: + Image(systemName: "house") + .tabBarify() + case .search: + Image(systemName: "magnifyingglass") + .tabBarify() + case .post: + Image(systemName: "square.and.pencil") + .tabBarify() + case .activity: + Image(systemName: "heart") + .tabBarify() + case .profile: + Image(systemName: "person") + .tabBarify() + + } + } + + @ViewBuilder + var imageFill: some View { + switch self { + case .timeline: + Image(systemName: "house.fill") + .tabBarify(false) + case .search: + Image(systemName: "magnifyingglass") + .tabBarify(false) + case .post: + Image(systemName: "square.and.pencil") + .tabBarify(false) + case .activity: + Image(systemName: "heart.fill") + .tabBarify(false) + case .profile: + Image(systemName: "person.fill") + .tabBarify(false) + + } + } +} + +extension Image { + func tabBarify(_ neutral: Bool = true) -> some View { + self + .font(.title2) + .opacity(neutral ? 0.3 : 1) + } +} + +#Preview { + TabsView(navigator: Navigator()) + .previewLayout(.sizeThatFits) +} diff --git a/Threaded/Data/Accounts/Account+Elms.swift b/Threaded/Data/Accounts/Account+Elms.swift new file mode 100644 index 0000000..065b286 --- /dev/null +++ b/Threaded/Data/Accounts/Account+Elms.swift @@ -0,0 +1,75 @@ +//Made by Lumaa + +import Foundation + +public enum Visibility: String, Codable, CaseIterable, Hashable, Equatable, Sendable { + case pub = "public" + case unlisted + case priv = "private" + case direct +} + +private enum CodingKeys: CodingKey { + case asDate +} + +public struct ServerDate: Codable, Hashable, Equatable, Sendable { + public let asDate: Date + + public var relativeFormatted: String { + DateFormatterCache.shared.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date()) + } + + public var shortDateFormatted: String { + DateFormatterCache.shared.createdAtShortDateFormatted.string(from: asDate) + } + + private static let calendar = Calendar(identifier: .gregorian) + + public init() { + asDate = Date() - 100 + } + + public init(from decoder: Decoder) throws { + do { + // Decode from server + let container = try decoder.singleValueContainer() + let stringDate = try container.decode(String.self) + asDate = DateFormatterCache.shared.createdAtDateFormatter.date(from: stringDate) ?? Date() + } catch { + // Decode from cache + let container = try decoder.container(keyedBy: CodingKeys.self) + asDate = try container.decode(Date.self, forKey: .asDate) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(asDate, forKey: .asDate) + } +} + +class DateFormatterCache: @unchecked Sendable { + static let shared = DateFormatterCache() + + let createdAtRelativeFormatter: RelativeDateTimeFormatter + let createdAtShortDateFormatted: DateFormatter + let createdAtDateFormatter: DateFormatter + + init() { + let createdAtRelativeFormatter = RelativeDateTimeFormatter() + createdAtRelativeFormatter.unitsStyle = .short + self.createdAtRelativeFormatter = createdAtRelativeFormatter + + let createdAtShortDateFormatted = DateFormatter() + createdAtShortDateFormatted.dateStyle = .short + createdAtShortDateFormatted.timeStyle = .none + self.createdAtShortDateFormatted = createdAtShortDateFormatted + + let createdAtDateFormatter = DateFormatter() + createdAtDateFormatter.calendar = .init(identifier: .iso8601) + createdAtDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" + createdAtDateFormatter.timeZone = .init(abbreviation: "UTC") + self.createdAtDateFormatter = createdAtDateFormatter + } +} diff --git a/Threaded/Data/Accounts/Account.swift b/Threaded/Data/Accounts/Account.swift new file mode 100644 index 0000000..f5e40fc --- /dev/null +++ b/Threaded/Data/Accounts/Account.swift @@ -0,0 +1,127 @@ +//Made by Lumaa + +import Foundation + +public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable { + public static func == (lhs: Account, rhs: Account) -> Bool { + lhs.id == rhs.id && + lhs.username == rhs.username && + lhs.note.asRawText == rhs.note.asRawText && + lhs.statusesCount == rhs.statusesCount && + lhs.followersCount == rhs.followersCount && + lhs.followingCount == rhs.followingCount && + lhs.acct == rhs.acct && + lhs.displayName == rhs.displayName && + lhs.fields == rhs.fields && + lhs.lastStatusAt == rhs.lastStatusAt && + lhs.discoverable == rhs.discoverable && + lhs.bot == rhs.bot && + lhs.locked == rhs.locked + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public struct Field: Codable, Equatable, Identifiable, Sendable { + public var id: String { + value.asRawText + name + } + + public let name: String + public let value: HTMLString + public let verifiedAt: String? + } + + public struct Source: Codable, Equatable, Sendable { + public let privacy: Visibility + public let sensitive: Bool + public let language: String? + public let note: String + public let fields: [Field] + } + + public let id: String + public let username: String + public let displayName: String? + public let avatar: URL + public let header: URL + public let acct: String + public let note: HTMLString + public let createdAt: ServerDate? + public let followersCount: Int? + public let followingCount: Int? + public let statusesCount: Int? + public let lastStatusAt: String? + public let fields: [Field] + public let locked: Bool + public let emojis: [Emoji] + public let url: URL? + public let source: Source? + public let bot: Bool + public let discoverable: Bool? + + public var haveAvatar: Bool { + avatar.lastPathComponent != "missing.png" + } + + public var haveHeader: Bool { + header.lastPathComponent != "missing.png" + } + + public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil) { + self.id = id + self.username = username + self.displayName = displayName + self.avatar = avatar + self.header = header + self.acct = acct + self.note = note + self.createdAt = createdAt + self.followersCount = followersCount + self.followingCount = followingCount + self.statusesCount = statusesCount + self.lastStatusAt = lastStatusAt + self.fields = fields + self.locked = locked + self.emojis = emojis + self.url = url + self.source = source + self.bot = bot + self.discoverable = discoverable + } + + public static func placeholder() -> Account { + .init(id: UUID().uuidString, + username: "Username", + displayName: "John Mastodon", + avatar: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!, + header: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!, + acct: "johnm@example.com", + note: .init(stringValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent ullamcorper lectus ac finibus dui."), + createdAt: ServerDate(), + followersCount: 10, + followingCount: 10, + statusesCount: 10, + lastStatusAt: nil, + fields: [], + locked: false, + emojis: [], + url: nil, + source: nil, + bot: false, + discoverable: true) + } + + public static func placeholders() -> [Account] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } +} + +public struct FamiliarAccounts: Decodable { + public let id: String + public let accounts: [Account] +} + +extension FamiliarAccounts: Sendable {} diff --git a/Threaded/Data/Accounts/AccountManager.swift b/Threaded/Data/Accounts/AccountManager.swift new file mode 100644 index 0000000..f62e472 --- /dev/null +++ b/Threaded/Data/Accounts/AccountManager.swift @@ -0,0 +1,101 @@ +//Made by Lumaa + +import Foundation + +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" + + public var key: String { + if let oauthToken { + "\(server):\(oauthToken.createdAt)" + } else { + "\(server):anonymous" + } + } + + public var id: String { + key + } + + public init(server: String, + accountName: String?, + oauthToken: OauthToken? = nil) + { + self.server = server + self.accountName = accountName + self.oauthToken = oauthToken + } + + func saveAsCurrent() throws { + let encoder = JSONEncoder() + let json = try encoder.encode(self) + UserDefaults.standard.setValue(json, forKey: AppAccount.saveKey) + } + + static func loadAsCurrent() throws -> AppAccount? { + let decoder = JSONDecoder() + if let data = UserDefaults.standard.data(forKey: AppAccount.saveKey) { + let account = try decoder.decode(AppAccount.self, from: data) + return account + } + return nil + } +} + +extension AppAccount: Sendable {} + +public enum Oauth: Endpoint { + case authorize(clientId: String) + case token(code: String, clientId: String, clientSecret: String) + + public func path() -> String { + switch self { + case .authorize: + "oauth/authorize" + case .token: + "oauth/token" + } + } + + public var jsonValue: Encodable? { + switch self { + case let .token(code, clientId, clientSecret): + TokenData(clientId: clientId, clientSecret: clientSecret, code: code) + default: + nil + } + } + + public struct TokenData: Encodable { + public let grantType = "authorization_code" + public let clientId: String + public let clientSecret: String + public let redirectUri = AppInfo.scheme + public let code: String + public let scope = AppInfo.scopes + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case let .authorize(clientId): + return [ + .init(name: "response_type", value: "code"), + .init(name: "client_id", value: clientId), + .init(name: "redirect_uri", value: AppInfo.scheme), + .init(name: "scope", value: AppInfo.scopes), + ] + default: + return nil + } + } +} + +public struct OauthToken: Codable, Hashable, Sendable { + public let accessToken: String + public let tokenType: String + public let scope: String + public let createdAt: Double +} diff --git a/Threaded/Data/Accounts/TimelineFilter.swift b/Threaded/Data/Accounts/TimelineFilter.swift new file mode 100644 index 0000000..5aaa068 --- /dev/null +++ b/Threaded/Data/Accounts/TimelineFilter.swift @@ -0,0 +1,340 @@ +//Made by Lumaa + +import Foundation +import SwiftUI + +public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable { + case local, federated, trending + + public func localizedTitle() -> LocalizedStringKey { + switch self { + case .federated: + "timeline.federated" + case .local: + "timeline.local" + case .trending: + "timeline.trending" + } + } + + public func iconName() -> String { + switch self { + case .federated: + "globe.americas" + case .local: + "person.2" + case .trending: + "chart.line.uptrend.xyaxis" + } + } +} + +public enum TimelineFilter: Hashable, Equatable { + case home, local, federated, trending + case hashtag(tag: String, accountId: String?) + case tagGroup(title: String, tags: [String]) + case list(list: AccountsList) + case remoteLocal(server: String, filter: RemoteTimelineFilter) + case latest + + public func hash(into hasher: inout Hasher) { + hasher.combine(title) + } + + public static func availableTimeline(client: Client) -> [TimelineFilter] { + if !client.isAuth { + return [.local, .federated, .trending] + } + return [.home, .local, .federated, .trending] + } + + public var supportNewestPagination: Bool { + switch self { + case .trending: + false + case let .remoteLocal(_, filter): + filter != .trending + default: + true + } + } + + public var title: String { + switch self { + case .latest: + "Latest" + case .federated: + "Federated" + case .local: + "Local" + case .trending: + "Trending" + case .home: + "Home" + case let .hashtag(tag, _): + "#\(tag)" + case let .tagGroup(title, _): + title + case let .list(list): + list.title + case let .remoteLocal(server, _): + server + } + } + + public func localizedTitle() -> LocalizedStringKey { + switch self { + case .latest: + "timeline.latest" + case .federated: + "timeline.federated" + case .local: + "timeline.local" + case .trending: + "timeline.trending" + case .home: + "timeline.home" + case let .hashtag(tag, _): + "#\(tag)" + case let .tagGroup(title, _): + LocalizedStringKey(title) // ?? not sure since this can't be localized. + case let .list(list): + LocalizedStringKey(list.title) + case let .remoteLocal(server, _): + LocalizedStringKey(server) + } + } + + public func iconName() -> String? { + switch self { + case .latest: + "arrow.counterclockwise" + case .federated: + "globe.americas" + case .local: + "person.2" + case .trending: + "chart.line.uptrend.xyaxis" + case .home: + "house" + case .list: + "list.bullet" + case .remoteLocal: + "dot.radiowaves.right" + default: + nil + } + } + + public func endpoint(sinceId: String?, maxId: String?, minId: String?, offset: Int?) -> Endpoint { + switch self { + case .federated: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) + case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true) + case let .remoteLocal(_, filter): + switch filter { + case .local: + return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true) + case .federated: + return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) + case .trending: + return Trends.statuses(offset: offset) + } + case .latest: return Timelines.home(sinceId: nil, maxId: nil, minId: nil) + case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId) + case .trending: return Trends.statuses(offset: offset) + case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId) + case let .hashtag(tag, accountId): + if let accountId { + return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil) + } else { + return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId) + } + case let .tagGroup(_, tags): + var tags = tags + if !tags.isEmpty { + let tag = tags.removeFirst() + return Timelines.hashtag(tag: tag, additional: tags, maxId: maxId) + } else { + return Timelines.hashtag(tag: "", additional: tags, maxId: maxId) + } + } + } +} + +extension TimelineFilter: Codable { + enum CodingKeys: String, CodingKey { + case home + case local + case federated + case trending + case hashtag + case tagGroup + case list + case remoteLocal + case latest + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let key = container.allKeys.first + switch key { + case .home: + self = .home + case .local: + self = .local + case .federated: + self = .federated + case .trending: + self = .trending + case .hashtag: + var nestedContainer = try container.nestedUnkeyedContainer(forKey: .hashtag) + let tag = try nestedContainer.decode(String.self) + let accountId = try nestedContainer.decode(String?.self) + self = .hashtag( + tag: tag, + accountId: accountId + ) + case .tagGroup: + var nestedContainer = try container.nestedUnkeyedContainer(forKey: .hashtag) + let title = try nestedContainer.decode(String.self) + let tags = try nestedContainer.decode([String].self) + self = .tagGroup( + title: title, + tags: tags + ) + case .list: + let list = try container.decode( + AccountsList.self, + forKey: .list + ) + self = .list(list: list) + case .remoteLocal: + var nestedContainer = try container.nestedUnkeyedContainer(forKey: .remoteLocal) + let server = try nestedContainer.decode(String.self) + let filter = try nestedContainer.decode(RemoteTimelineFilter.self) + self = .remoteLocal( + server: server, + filter: filter + ) + case .latest: + self = .latest + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Unabled to decode enum." + ) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .home: + try container.encode(CodingKeys.home.rawValue, forKey: .home) + case .local: + try container.encode(CodingKeys.local.rawValue, forKey: .local) + case .federated: + try container.encode(CodingKeys.federated.rawValue, forKey: .federated) + case .trending: + try container.encode(CodingKeys.trending.rawValue, forKey: .trending) + case let .hashtag(tag, accountId): + var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag) + try nestedContainer.encode(tag) + try nestedContainer.encode(accountId) + case let .tagGroup(title, tags): + var nestedContainer = container.nestedUnkeyedContainer(forKey: .tagGroup) + try nestedContainer.encode(title) + try nestedContainer.encode(tags) + case let .list(list): + try container.encode(list, forKey: .list) + case let .remoteLocal(server, filter): + var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag) + try nestedContainer.encode(server) + try nestedContainer.encode(filter) + case .latest: + try container.encode(CodingKeys.latest.rawValue, forKey: .latest) + } + } +} + +extension TimelineFilter: RawRepresentable { + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode(TimelineFilter.self, from: data) + else { + return nil + } + self = result + } + + public var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } +} + +extension RemoteTimelineFilter: Codable { + enum CodingKeys: String, CodingKey { + case local + case federated + case trending + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let key = container.allKeys.first + switch key { + case .local: + self = .local + case .federated: + self = .federated + case .trending: + self = .trending + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Unabled to decode enum." + ) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .local: + try container.encode(CodingKeys.local.rawValue, forKey: .local) + case .federated: + try container.encode(CodingKeys.federated.rawValue, forKey: .federated) + case .trending: + try container.encode(CodingKeys.trending.rawValue, forKey: .trending) + } + } +} + +extension RemoteTimelineFilter: RawRepresentable { + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode(RemoteTimelineFilter.self, from: data) + else { + return nil + } + self = result + } + + public var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } +} diff --git a/Threaded/Data/AccountsList.swift b/Threaded/Data/AccountsList.swift new file mode 100644 index 0000000..9efd5ef --- /dev/null +++ b/Threaded/Data/AccountsList.swift @@ -0,0 +1,20 @@ +//Made by Lumaa + +import Foundation + +public struct AccountsList: Codable, Identifiable, Equatable, Hashable { + public let id: String + public let title: String + public let repliesPolicy: RepliesPolicy? + public let exclusive: Bool? + + public enum RepliesPolicy: String, Sendable, Codable, CaseIterable, Identifiable { + public var id: String { + rawValue + } + + case followed, list, `none` + } +} + +extension AccountsList: Sendable {} diff --git a/Threaded/Data/AppInfo.swift b/Threaded/Data/AppInfo.swift new file mode 100644 index 0000000..4e93393 --- /dev/null +++ b/Threaded/Data/AppInfo.swift @@ -0,0 +1,11 @@ +//Made by Lumaa + +import Foundation + +public enum AppInfo { + public static let scopes = "read write follow" + public static let scheme = "threaded://" + public static let clientName = "ThreadedApp" + public static let defaultServer = "mastodon.social" + public static let website = "https://apps.lumaa.fr/" +} diff --git a/Threaded/Data/Client.swift b/Threaded/Data/Client.swift new file mode 100644 index 0000000..71f38a8 --- /dev/null +++ b/Threaded/Data/Client.swift @@ -0,0 +1,278 @@ +//Made by Lumaa + +import Combine +import Foundation +import Observation +import os +import SwiftUI + +@Observable +public final class Client: Equatable, Identifiable, Hashable { + public static func == (lhs: Client, rhs: Client) -> Bool { + let lhsToken = lhs.critical.withLock { $0.oauthToken } + let rhsToken = rhs.critical.withLock { $0.oauthToken } + + return (lhsToken != nil) == (rhsToken != nil) && + lhs.server == rhs.server && + lhsToken?.accessToken == rhsToken?.accessToken + } + + public enum Version: String, Sendable { + case v1, v2 + } + + public enum ClientError: Error { + case unexpectedRequest + } + + public enum OauthError: Error { + case missingApp + case invalidRedirectURL + } + + public var id: String { + critical.withLock { + let isAuth = $0.oauthToken != nil + return "\(isAuth)\(server)\($0.oauthToken?.createdAt ?? 0)" + } + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let server: String + public let version: Version + private let urlSession: URLSession + private let decoder = JSONDecoder() + + // Putting all mutable state inside an `OSAllocatedUnfairLock` makes `Client` + // provably `Sendable`. The lock is a struct, but it uses a `ManagedBuffer` + // reference type to hold its associated state. + private let critical: OSAllocatedUnfairLock + private struct Critical: Sendable { + /// Only used as a transitionary app while in the oauth flow. + var oauthApp: InstanceApp? + var oauthToken: OauthToken? + var connections: Set = [] + } + + public var isAuth: Bool { + critical.withLock { $0.oauthToken != nil } + } + + public var connections: Set { + critical.withLock { $0.connections } + } + + public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) { + self.server = server + self.version = version + critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server])) + urlSession = URLSession.shared + decoder.keyDecodingStrategy = .convertFromSnakeCase + } + + public func addConnections(_ connections: [String]) { + critical.withLock { + $0.connections.formUnion(connections) + } + } + + public func hasConnection(with url: URL) -> Bool { + guard let host = url.host else { return false } + return critical.withLock { + if let rootHost = host.split(separator: ".", maxSplits: 1).last { + // Sometimes the connection is with the root host instead of a subdomain + // eg. Mastodon runs on mastdon.domain.com but the connection is with domain.com + $0.connections.contains(host) || $0.connections.contains(String(rootHost)) + } else { + $0.connections.contains(host) + } + } + } + + private func makeURL(scheme: String = "https", + endpoint: Endpoint, + forceVersion: Version? = nil, + forceServer: String? = nil) throws -> URL + { + var components = URLComponents() + components.scheme = scheme + components.host = forceServer ?? server + if type(of: endpoint) == Oauth.self { + components.path += "/\(endpoint.path())" + } else { + components.path += "/api/\(forceVersion?.rawValue ?? version.rawValue)/\(endpoint.path())" + } + components.queryItems = endpoint.queryItems() + guard let url = components.url else { + throw ClientError.unexpectedRequest + } + return url + } + + private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = httpMethod + if let oauthToken = critical.withLock({ $0.oauthToken }) { + request.setValue("Bearer \(oauthToken.accessToken)", forHTTPHeaderField: "Authorization") + } + if let json = endpoint.jsonValue { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.outputFormatting = .sortedKeys + do { + let jsonData = try encoder.encode(json) + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } catch { + print("Client Error encoding JSON: \(error.localizedDescription)") + } + } + return request + } + + private func makeGet(endpoint: Endpoint) throws -> URLRequest { + let url = try makeURL(endpoint: endpoint) + return makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET") + } + + public func get(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { + try await makeEntityRequest(endpoint: endpoint, method: "GET", forceVersion: forceVersion) + } + + public func getWithLink(endpoint: Endpoint) async throws -> (Entity, LinkHandler?) { + let (data, httpResponse) = try await urlSession.data(for: makeGet(endpoint: endpoint)) + var linkHandler: LinkHandler? + if let response = httpResponse as? HTTPURLResponse, + let link = response.allHeaderFields["Link"] as? String + { + linkHandler = .init(rawLink: link) + } + logResponseOnError(httpResponse: httpResponse, data: data) + return try (decoder.decode(Entity.self, from: data), linkHandler) + } + + public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { + try await makeEntityRequest(endpoint: endpoint, method: "POST", forceVersion: forceVersion) + } + + public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? { + let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion) + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "POST") + let (_, httpResponse) = try await urlSession.data(for: request) + return httpResponse as? HTTPURLResponse + } + + public func patch(endpoint: Endpoint) async throws -> HTTPURLResponse? { + let url = try makeURL(endpoint: endpoint) + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "PATCH") + let (_, httpResponse) = try await urlSession.data(for: request) + return httpResponse as? HTTPURLResponse + } + + public func put(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { + try await makeEntityRequest(endpoint: endpoint, method: "PUT", forceVersion: forceVersion) + } + + public func delete(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? { + let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion) + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "DELETE") + let (_, httpResponse) = try await urlSession.data(for: request) + return httpResponse as? HTTPURLResponse + } + + private func makeEntityRequest(endpoint: Endpoint, + method: String, + forceVersion: Version? = nil) async throws -> Entity + { + let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion) + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) + let (data, httpResponse) = try await urlSession.data(for: request) + logResponseOnError(httpResponse: httpResponse, data: data) + do { + return try decoder.decode(Entity.self, from: data) + } catch { + print(error) + if var serverError = try? decoder.decode(ServerError.self, from: data) { + if let httpResponse = httpResponse as? HTTPURLResponse { + serverError.httpCode = httpResponse.statusCode + } + throw serverError + } + throw error + } + } + + public func oauthURL() async throws -> URL { + let app: InstanceApp = try await post(endpoint: Apps.registerApp) + critical.withLock { $0.oauthApp = app } + return try makeURL(endpoint: Oauth.authorize(clientId: app.clientId)) + } + + public func continueOauthFlow(url: URL) async throws -> OauthToken { + guard let app = critical.withLock({ $0.oauthApp }) else { + throw OauthError.missingApp + } + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let code = components.queryItems?.first(where: { $0.name == "code" })?.value + else { + throw OauthError.invalidRedirectURL + } + let token: OauthToken = try await post(endpoint: Oauth.token(code: code, + clientId: app.clientId, + clientSecret: app.clientSecret)) + critical.withLock { $0.oauthToken = token } + return token + } + + public func makeWebSocketTask(endpoint: Endpoint, instanceStreamingURL: URL?) throws -> URLSessionWebSocketTask { + let url = try makeURL(scheme: "wss", endpoint: endpoint, forceServer: instanceStreamingURL?.host) + var subprotocols: [String] = [] + if let oauthToken = critical.withLock({ $0.oauthToken }) { + subprotocols.append(oauthToken.accessToken) + } + return urlSession.webSocketTask(with: url, protocols: subprotocols) + } + + public func mediaUpload(endpoint: Endpoint, + version: Version, + method: String, + mimeType: String, + filename: String, + data: Data) async throws -> Entity + { + let url = try makeURL(endpoint: endpoint, forceVersion: version) + var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) + let boundary = UUID().uuidString + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + let httpBody = NSMutableData() + httpBody.append("--\(boundary)\r\n".data(using: .utf8)!) + httpBody.append("Content-Disposition: form-data; name=\"\(filename)\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!) + httpBody.append("Content-Type: \(mimeType)\r\n".data(using: .utf8)!) + httpBody.append("\r\n".data(using: .utf8)!) + httpBody.append(data) + httpBody.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + request.httpBody = httpBody as Data + let (data, httpResponse) = try await urlSession.data(for: request) + logResponseOnError(httpResponse: httpResponse, data: data) + do { + return try decoder.decode(Entity.self, from: data) + } catch { + if let serverError = try? decoder.decode(ServerError.self, from: data) { + throw serverError + } + throw error + } + } + + private func logResponseOnError(httpResponse: URLResponse, data: Data) { + if let httpResponse = httpResponse as? HTTPURLResponse, httpResponse.statusCode > 299 { + print(httpResponse) + print(String(data: data, encoding: .utf8) ?? "") + } + } +} + +extension Client: Sendable {} diff --git a/Threaded/Data/Emoji.swift b/Threaded/Data/Emoji.swift new file mode 100644 index 0000000..ba05878 --- /dev/null +++ b/Threaded/Data/Emoji.swift @@ -0,0 +1,19 @@ +//Made by Lumaa + +import Foundation + +public struct Emoji: Codable, Hashable, Identifiable, Equatable, Sendable { + public func hash(into hasher: inout Hasher) { + hasher.combine(shortcode) + } + + public var id: String { + shortcode + } + + public let shortcode: String + public let url: URL + public let staticUrl: URL + public let visibleInPicker: Bool + public let category: String? +} diff --git a/Threaded/Data/FetchTimeline.swift b/Threaded/Data/FetchTimeline.swift new file mode 100644 index 0000000..efb0130 --- /dev/null +++ b/Threaded/Data/FetchTimeline.swift @@ -0,0 +1,42 @@ +//Made by Lumaa + +import Foundation + +struct FetchTimeline { + var client: Client + private var datasource: [Status] = [] + public var statusesState: LoadingState = .loading + + private var timeline: TimelineFilter = .home + + init(client: Client) { + self.client = client + } + + public mutating func fetch(client: Client) async { + self.statusesState = .loading + self.datasource = await fetchNewestStatuses() + } + + private mutating func fetchNewestStatuses() async -> [Status] { + do { + let statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil, minId: nil, offset: 0)) + self.statusesState = .loaded + return statuses + } catch { + statusesState = .error(error: error) + print("timeline parse error: \(error)") + } + return [] + } + + func getStatuses() -> [Status] { + return datasource + } + + enum LoadingState { + case loading + case loaded + case error(error: Error) + } +} diff --git a/Threaded/Data/HTMLString.swift b/Threaded/Data/HTMLString.swift new file mode 100644 index 0000000..7aee77b --- /dev/null +++ b/Threaded/Data/HTMLString.swift @@ -0,0 +1,293 @@ +//Made by Lumaa + +import Foundation +import SwiftSoup +import SwiftUI + +private enum CodingKeys: CodingKey { + case htmlValue, asMarkdown, asRawText, statusesURLs, links +} + +public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { + public var htmlValue: String = "" + public var asMarkdown: String = "" + public var asRawText: String = "" + public var statusesURLs = [URL]() + public private(set) var links = [Link]() + + public var asSafeMarkdownAttributedString: AttributedString = .init() + private var main_regex: NSRegularExpression? + private var underscore_regex: NSRegularExpression? + public init(from decoder: Decoder) { + var alreadyDecoded = false + do { + let container = try decoder.singleValueContainer() + htmlValue = try container.decode(String.self) + } catch { + do { + alreadyDecoded = true + let container = try decoder.container(keyedBy: CodingKeys.self) + htmlValue = try container.decode(String.self, forKey: .htmlValue) + asMarkdown = try container.decode(String.self, forKey: .asMarkdown) + asRawText = try container.decode(String.self, forKey: .asRawText) + statusesURLs = try container.decode([URL].self, forKey: .statusesURLs) + links = try container.decode([Link].self, forKey: .links) + } catch { + htmlValue = "" + } + } + + if !alreadyDecoded { + // https://daringfireball.net/projects/markdown/syntax + // Pre-escape \ ` _ * ~ and [ as these are the only + // characters the markdown parser uses when it renders + // to attributed text. Note that ~ for strikethrough is + // not documented in the syntax docs but is used by + // AttributedString. + main_regex = try? NSRegularExpression(pattern: "([\\*\\`\\~\\[\\\\])", options: .caseInsensitive) + // don't escape underscores that are between colons, they are most likely custom emoji + underscore_regex = try? NSRegularExpression(pattern: "(?!\\B:[^:]*)(_)(?![^:]*:\\B)", options: .caseInsensitive) + + asMarkdown = "" + do { + let document: Document = try SwiftSoup.parse(htmlValue) + handleNode(node: document) + + document.outputSettings(OutputSettings().prettyPrint(pretty: false)) + try document.select("br").after("\n") + try document.select("p").after("\n\n") + let html = try document.html() + var text = try SwiftSoup.clean(html, "", Whitelist.none(), OutputSettings().prettyPrint(pretty: false)) ?? "" + // Remove the two last line break added after the last paragraph. + if text.hasSuffix("\n\n") { + _ = text.removeLast() + _ = text.removeLast() + } + asRawText = text + + if asMarkdown.hasPrefix("\n") { + _ = asMarkdown.removeFirst() + } + + } catch { + asRawText = htmlValue + } + } + + do { + let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, + interpretedSyntax: .inlineOnlyPreservingWhitespace) + asSafeMarkdownAttributedString = try AttributedString(markdown: asMarkdown, options: options) + } catch { + asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) + } + } + + public init(stringValue: String, parseMarkdown: Bool = false) { + htmlValue = stringValue + asMarkdown = stringValue + asRawText = stringValue + statusesURLs = [] + + if parseMarkdown { + do { + let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, + interpretedSyntax: .inlineOnlyPreservingWhitespace) + asSafeMarkdownAttributedString = try AttributedString(markdown: asMarkdown, options: options) + } catch { + asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) + } + } else { + asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(htmlValue, forKey: .htmlValue) + try container.encode(asMarkdown, forKey: .asMarkdown) + try container.encode(asRawText, forKey: .asRawText) + try container.encode(statusesURLs, forKey: .statusesURLs) + try container.encode(links, forKey: .links) + } + + private mutating func handleNode(node: SwiftSoup.Node) { + do { + if let className = try? node.attr("class") { + if className == "invisible" { + // don't display + return + } + + if className == "ellipsis" { + // descend into this one now and + // append the ellipsis + for nn in node.getChildNodes() { + handleNode(node: nn) + } + asMarkdown += "…" + return + } + } + + if node.nodeName() == "p" { + if asMarkdown.count > 0 { // ignore first opening