From 8abc56227d378d22ece96195c136394a8a601508 Mon Sep 17 00:00:00 2001 From: Lumaa Date: Thu, 7 Mar 2024 17:22:11 +0100 Subject: [PATCH] Widget, began Tenor integration, other imporvements and fixes --- Threaded.xcodeproj/project.pbxproj | 214 ++++++++++++++++- .../xcshareddata/WorkspaceSettings.xcsettings | 5 + Threaded/Components/Post/QuotePostView.swift | 1 - Threaded/Data/Accounts/AccountManager.swift | 119 ---------- Threaded/Data/Accounts/LoggedAccounts.swift | 77 +++++- Threaded/Data/Client.swift | 17 +- Threaded/Data/Content/Tenor.swift | 50 ++++ Threaded/Data/MastodonRequest.swift | 46 ++++ .../Data/Notifications/Notification.swift | 7 + Threaded/Localizable.xcstrings | 2 +- Threaded/Threaded.entitlements | 12 + Threaded/Views/AttachmentView.swift | 2 +- Threaded/Views/NotificationsView.swift | 7 - Threaded/Views/PostDetailsView.swift | 3 + Threaded/Views/Settings/ShopView.swift | 40 ++-- ThreadedWidgets/AppIntent.swift | 92 +++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + ThreadedWidgets/Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 38 +++ ThreadedWidgets/Info.plist | 11 + ThreadedWidgets/Localizable.xcstrings | 59 +++++ ThreadedWidgets/Redeclarations.swift | 224 ++++++++++++++++++ ThreadedWidgets/ThreadedWidgets.swift | 186 +++++++++++++++ ThreadedWidgets/ThreadedWidgetsBundle.swift | 12 + .../ThreadedWidgetsExtension.entitlements | 12 + 26 files changed, 1106 insertions(+), 160 deletions(-) create mode 100644 Threaded.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Threaded/Data/Content/Tenor.swift create mode 100644 Threaded/Threaded.entitlements create mode 100644 ThreadedWidgets/AppIntent.swift create mode 100644 ThreadedWidgets/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ThreadedWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ThreadedWidgets/Assets.xcassets/Contents.json create mode 100644 ThreadedWidgets/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 ThreadedWidgets/Info.plist create mode 100644 ThreadedWidgets/Localizable.xcstrings create mode 100644 ThreadedWidgets/Redeclarations.swift create mode 100644 ThreadedWidgets/ThreadedWidgets.swift create mode 100644 ThreadedWidgets/ThreadedWidgetsBundle.swift create mode 100644 ThreadedWidgets/ThreadedWidgetsExtension.entitlements diff --git a/Threaded.xcodeproj/project.pbxproj b/Threaded.xcodeproj/project.pbxproj index ec7803c..3b40144 100644 --- a/Threaded.xcodeproj/project.pbxproj +++ b/Threaded.xcodeproj/project.pbxproj @@ -50,10 +50,25 @@ B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B222B447B8000BBC82D /* PostCardView.swift */; }; B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B242B44997400BBC82D /* QuotePostView.swift */; }; B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B262B449CDC00BBC82D /* SearchResults.swift */; }; + B9BCC3182B90B3BC00211976 /* Tenor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BCC3172B90B3BC00211976 /* Tenor.swift */; }; B9BED5162B5D5E6500C9B715 /* PostInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5152B5D5E6500C9B715 /* PostInteractor.swift */; }; B9BED5182B5D649C00C9B715 /* PostMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5172B5D649C00C9B715 /* PostMenu.swift */; }; B9BED51A2B5D662D00C9B715 /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */; }; B9BF54072B6B6823004B24E7 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B9BF54062B6B6823004B24E7 /* KeychainSwift */; }; + B9C20D0B2B921C78004DC9B3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9C20D0A2B921C78004DC9B3 /* WidgetKit.framework */; }; + B9C20D0D2B921C78004DC9B3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9C20D0C2B921C78004DC9B3 /* SwiftUI.framework */; }; + B9C20D102B921C78004DC9B3 /* ThreadedWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */; }; + B9C20D122B921C78004DC9B3 /* ThreadedWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D112B921C78004DC9B3 /* ThreadedWidgets.swift */; }; + B9C20D142B921C78004DC9B3 /* AppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D132B921C78004DC9B3 /* AppIntent.swift */; }; + B9C20D162B921C7B004DC9B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9C20D152B921C7B004DC9B3 /* Assets.xcassets */; }; + B9C20D1A2B921C7B004DC9B3 /* ThreadedWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + B9C20D1F2B921E81004DC9B3 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B9C20D1E2B921E81004DC9B3 /* Localizable.xcstrings */; }; + B9C20D282B9229DF004DC9B3 /* LoggedAccounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98627302B86F23500844245 /* LoggedAccounts.swift */; }; + B9C20D342B9229EC004DC9B3 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB949A2B2EF09A00D81C07 /* Client.swift */; }; + B9C20D372B9229EC004DC9B3 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94A12B2EF24A00D81C07 /* AppInfo.swift */; }; + B9C20D3D2B9229EC004DC9B3 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94872B2E223E00D81C07 /* Emoji.swift */; }; + B9C20D562B922DAC004DC9B3 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B9C20D552B922DAC004DC9B3 /* KeychainSwift */; }; + B9C20D612B949AD7004DC9B3 /* Redeclarations.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */; }; B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */; }; B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */; }; B9D365612B79A1BE004C1255 /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D365602B79A1BE004C1255 /* MailView.swift */; }; @@ -102,6 +117,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + B9C20D182B921C7B004DC9B3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B9FB944F2B2DEECE00D81C07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B9C20D082B921C78004DC9B3; + remoteInfo = ThreadedWidgetsExtension; + }; B9FB94B22B2F009F00D81C07 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B9FB944F2B2DEECE00D81C07 /* Project object */; @@ -119,6 +141,7 @@ dstSubfolderSpec = 13; files = ( B9FB94B42B2F009F00D81C07 /* ThreadedAuthService.appex in Embed Foundation Extensions */, + B9C20D1A2B921C7B004DC9B3 /* ThreadedWidgetsExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -162,9 +185,22 @@ B9B63B222B447B8000BBC82D /* PostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardView.swift; sourceTree = ""; }; B9B63B242B44997400BBC82D /* QuotePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePostView.swift; sourceTree = ""; }; B9B63B262B449CDC00BBC82D /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = ""; }; + B9BCC3172B90B3BC00211976 /* Tenor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tenor.swift; sourceTree = ""; }; B9BED5152B5D5E6500C9B715 /* PostInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostInteractor.swift; sourceTree = ""; }; B9BED5172B5D649C00C9B715 /* PostMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostMenu.swift; sourceTree = ""; }; B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = ""; }; + B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ThreadedWidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + B9C20D0A2B921C78004DC9B3 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + B9C20D0C2B921C78004DC9B3 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedWidgetsBundle.swift; sourceTree = ""; }; + B9C20D112B921C78004DC9B3 /* ThreadedWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedWidgets.swift; sourceTree = ""; }; + B9C20D132B921C78004DC9B3 /* AppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntent.swift; sourceTree = ""; }; + B9C20D152B921C7B004DC9B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B9C20D172B921C7B004DC9B3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B9C20D1E2B921E81004DC9B3 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + B9C20D582B923CDD004DC9B3 /* ThreadedWidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ThreadedWidgetsExtension.entitlements; sourceTree = ""; }; + B9C20D592B923D53004DC9B3 /* Threaded.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Threaded.entitlements; sourceTree = ""; }; + B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redeclarations.swift; sourceTree = ""; }; B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; B9CC45B92B40AA1E001E4FA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchStoryboard.storyboard; sourceTree = ""; }; @@ -216,6 +252,16 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + B9C20D062B921C78004DC9B3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B9C20D0D2B921C78004DC9B3 /* SwiftUI.framework in Frameworks */, + B9C20D0B2B921C78004DC9B3 /* WidgetKit.framework in Frameworks */, + B9C20D562B922DAC004DC9B3 /* KeychainSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9FB94542B2DEECE00D81C07 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -255,6 +301,7 @@ B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */, B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */, B98F47992B653CAE0092000F /* Compressor.swift */, + B9BCC3172B90B3BC00211976 /* Tenor.swift */, ); path = Content; sourceTree = ""; @@ -273,6 +320,21 @@ path = Post; sourceTree = ""; }; + B9C20D0E2B921C78004DC9B3 /* ThreadedWidgets */ = { + isa = PBXGroup; + children = ( + B9C20D582B923CDD004DC9B3 /* ThreadedWidgetsExtension.entitlements */, + B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */, + B9C20D112B921C78004DC9B3 /* ThreadedWidgets.swift */, + B9C20D132B921C78004DC9B3 /* AppIntent.swift */, + B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */, + B9C20D152B921C7B004DC9B3 /* Assets.xcassets */, + B9C20D1E2B921E81004DC9B3 /* Localizable.xcstrings */, + B9C20D172B921C7B004DC9B3 /* Info.plist */, + ); + path = ThreadedWidgets; + sourceTree = ""; + }; B9D9C6BF2B6A56D500C26A41 /* Notifications */ = { isa = PBXGroup; children = ( @@ -296,6 +358,7 @@ children = ( B9CC45B92B40AA1E001E4FA5 /* README.md */, B9FB94592B2DEECE00D81C07 /* Threaded */, + B9C20D0E2B921C78004DC9B3 /* ThreadedWidgets */, B9FB94AB2B2F009F00D81C07 /* AuthService */, B9FB94A82B2F009F00D81C07 /* Frameworks */, B97BCE292B3ED2C80044756D /* LICENSE */, @@ -309,6 +372,7 @@ children = ( B9FB94572B2DEECE00D81C07 /* Threaded.app */, B9FB94A72B2F009F00D81C07 /* ThreadedAuthService.appex */, + B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */, ); name = Products; sourceTree = ""; @@ -316,6 +380,7 @@ B9FB94592B2DEECE00D81C07 /* Threaded */ = { isa = PBXGroup; children = ( + B9C20D592B923D53004DC9B3 /* Threaded.entitlements */, B9FB94A02B2EF23100D81C07 /* Info.plist */, B9029FC12B81259400AA9B68 /* Secret.plist */, B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */, @@ -423,6 +488,8 @@ children = ( B9DC692C2B79362700E625B9 /* StoreKit.framework */, B9FB94A92B2F009F00D81C07 /* AuthenticationServices.framework */, + B9C20D0A2B921C78004DC9B3 /* WidgetKit.framework */, + B9C20D0C2B921C78004DC9B3 /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -452,6 +519,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = B9C20D1D2B921C7B004DC9B3 /* Build configuration list for PBXNativeTarget "ThreadedWidgetsExtension" */; + buildPhases = ( + B9C20D052B921C78004DC9B3 /* Sources */, + B9C20D062B921C78004DC9B3 /* Frameworks */, + B9C20D072B921C78004DC9B3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ThreadedWidgetsExtension; + packageProductDependencies = ( + B9C20D552B922DAC004DC9B3 /* KeychainSwift */, + ); + productName = ThreadedWidgetsExtension; + productReference = B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; B9FB94562B2DEECE00D81C07 /* Threaded */ = { isa = PBXNativeTarget; buildConfigurationList = B9FB94672B2DEECF00D81C07 /* Build configuration list for PBXNativeTarget "Threaded" */; @@ -465,6 +552,7 @@ ); dependencies = ( B9FB94B32B2F009F00D81C07 /* PBXTargetDependency */, + B9C20D192B921C7B004DC9B3 /* PBXTargetDependency */, ); name = Threaded; packageProductDependencies = ( @@ -506,9 +594,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1510; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1510; TargetAttributes = { + B9C20D082B921C78004DC9B3 = { + CreatedOnToolsVersion = 15.2; + }; B9FB94562B2DEECE00D81C07 = { CreatedOnToolsVersion = 15.1; }; @@ -540,11 +631,21 @@ targets = ( B9FB94562B2DEECE00D81C07 /* Threaded */, B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */, + B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + B9C20D072B921C78004DC9B3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9C20D1F2B921E81004DC9B3 /* Localizable.xcstrings in Resources */, + B9C20D162B921C7B004DC9B3 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9FB94552B2DEECE00D81C07 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -572,6 +673,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + B9C20D052B921C78004DC9B3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9C20D282B9229DF004DC9B3 /* LoggedAccounts.swift in Sources */, + B9C20D612B949AD7004DC9B3 /* Redeclarations.swift in Sources */, + B9C20D122B921C78004DC9B3 /* ThreadedWidgets.swift in Sources */, + B9C20D102B921C78004DC9B3 /* ThreadedWidgetsBundle.swift in Sources */, + B9C20D142B921C78004DC9B3 /* AppIntent.swift in Sources */, + B9C20D372B9229EC004DC9B3 /* AppInfo.swift in Sources */, + B9C20D3D2B9229EC004DC9B3 /* Emoji.swift in Sources */, + B9C20D342B9229EC004DC9B3 /* Client.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9FB94532B2DEECE00D81C07 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -636,6 +752,7 @@ B98627312B86F23500844245 /* LoggedAccounts.swift in Sources */, B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */, B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */, + B9BCC3182B90B3BC00211976 /* Tenor.swift in Sources */, B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */, B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */, B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */, @@ -659,6 +776,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + B9C20D192B921C7B004DC9B3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */; + targetProxy = B9C20D182B921C7B004DC9B3 /* PBXContainerItemProxy */; + }; B9FB94B32B2F009F00D81C07 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */; @@ -678,6 +800,72 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + B9C20D1B2B921C7B004DC9B3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = ThreadedWidgets/ThreadedWidgetsExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HB5P3BML86; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ThreadedWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ThreadedWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.ThreadedWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + B9C20D1C2B921C7B004DC9B3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = ThreadedWidgets/ThreadedWidgetsExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HB5P3BML86; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ThreadedWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ThreadedWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.ThreadedWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; B9FB94652B2DEECF00D81C07 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -803,8 +991,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Threaded/Threaded.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1425; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\""; DEVELOPMENT_TEAM = HB5P3BML86; ENABLE_PREVIEWS = YES; @@ -823,7 +1012,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.2.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -842,8 +1031,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Threaded/Threaded.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1425; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\""; DEVELOPMENT_TEAM = HB5P3BML86; ENABLE_PREVIEWS = YES; @@ -862,7 +1052,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.2.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -938,6 +1128,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + B9C20D1D2B921C7B004DC9B3 /* Build configuration list for PBXNativeTarget "ThreadedWidgetsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9C20D1B2B921C7B004DC9B3 /* Debug */, + B9C20D1C2B921C7B004DC9B3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; B9FB94522B2DEECE00D81C07 /* Build configuration list for PBXProject "Threaded" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1051,6 +1250,11 @@ package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; + B9C20D552B922DAC004DC9B3 /* KeychainSwift */ = { + isa = XCSwiftPackageProductDependency; + package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */; + productName = KeychainSwift; + }; B9FB94832B2E20AF00D81C07 /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */; diff --git a/Threaded.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Threaded.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Threaded.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Threaded/Components/Post/QuotePostView.swift b/Threaded/Components/Post/QuotePostView.swift index 65520c2..a616735 100644 --- a/Threaded/Components/Post/QuotePostView.swift +++ b/Threaded/Components/Post/QuotePostView.swift @@ -8,7 +8,6 @@ struct QuotePostView: View { var body: some View { CompactPostView(status: status, quoted: true) - .environmentObject(navigator) .frame(maxWidth: 250, maxHeight: 200) .padding(15) .padding([.horizontal], 20) diff --git a/Threaded/Data/Accounts/AccountManager.swift b/Threaded/Data/Accounts/AccountManager.swift index 79d0997..96b0814 100644 --- a/Threaded/Data/Accounts/AccountManager.swift +++ b/Threaded/Data/Accounts/AccountManager.swift @@ -1,7 +1,6 @@ //Made by Lumaa import Foundation -import KeychainSwift @Observable public class AccountManager: ObservableObject { @@ -52,121 +51,3 @@ public class AccountManager: ObservableObject { return account } } - -public struct AppAccount: Codable, Identifiable, Hashable { - public let server: String - public var accountName: String? - public let oauthToken: OauthToken? - - private static let saveKey: String = "threaded-appaccount.current" - private static var keychain: KeychainSwift { - let kc = KeychainSwift() - // synchronise later - return kc - } - - 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 - } - - public static func clear() { - Self.keychain.delete(Self.saveKey) - } - - public func clear() { - Self.clear() - } - - /// This function only works with the given `AppAccount` - public func saveAsCurrent(_ appAccount: AppAccount? = nil) { - let encoder = JSONEncoder() - if let data = try? encoder.encode(appAccount ?? self) { - Self.keychain.set(data, forKey: Self.saveKey, withAccess: .accessibleWhenUnlocked) - } else { - fatalError("Couldn't encode AppAccount correctly to save") - } - } - - /// This function only works with the last saved `AppAccount` or with the given `Data` - public static func loadAsCurrent(_ data: Data? = nil) -> Self? { - let decoder = JSONDecoder() - if let newData = data ?? keychain.getData(Self.saveKey) { - if let decoded = try? decoder.decode(Self.self, from: newData) { - return decoded - } else { - return nil - } - } else { - 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/LoggedAccounts.swift b/Threaded/Data/Accounts/LoggedAccounts.swift index 6ccb539..7eca887 100644 --- a/Threaded/Data/Accounts/LoggedAccounts.swift +++ b/Threaded/Data/Accounts/LoggedAccounts.swift @@ -3,14 +3,15 @@ import Foundation import SwiftUI import SwiftData +import KeychainSwift @Model class LoggedAccount { - let token: OauthToken + let token: OauthToken = OauthToken(accessToken: "ABC", tokenType: "ABC", scope: "ABC", createdAt: 0.0) let acct: String let app: AppAccount? - init(token: OauthToken, acct: String) { + init(token: OauthToken = OauthToken(accessToken: "ABC", tokenType: "ABC", scope: "ABC", createdAt: 0.0), acct: String) { self.token = token self.acct = acct self.app = nil @@ -31,3 +32,75 @@ public extension View { .modelContainer(for: LoggedAccount.self) } } + +public struct AppAccount: Codable, Identifiable, Hashable { + public let server: String + public var accountName: String? + public let oauthToken: OauthToken? + + private static let saveKey: String = "threaded-appaccount.current" + private static var keychain: KeychainSwift { + let kc = KeychainSwift() + // synchronise later + return kc + } + + 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 + } + + public static func clear() { + Self.keychain.delete(Self.saveKey) + } + + public func clear() { + Self.clear() + } + + /// This function only works with the given `AppAccount` + public func saveAsCurrent(_ appAccount: AppAccount? = nil) { + let encoder = JSONEncoder() + if let data = try? encoder.encode(appAccount ?? self) { + Self.keychain.set(data, forKey: Self.saveKey, withAccess: .accessibleWhenUnlocked) + } else { + fatalError("Couldn't encode AppAccount correctly to save") + } + } + + /// This function only works with the last saved `AppAccount` or with the given `Data` + public static func loadAsCurrent(_ data: Data? = nil) -> Self? { + let decoder = JSONDecoder() + if let newData = data ?? keychain.getData(Self.saveKey) { + if let decoded = try? decoder.decode(Self.self, from: newData) { + return decoded + } else { + return nil + } + } else { + return nil + } + } +} + +extension AppAccount: Sendable {} + +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/Client.swift b/Threaded/Data/Client.swift index 84cd7ae..3b4cb02 100644 --- a/Threaded/Data/Client.swift +++ b/Threaded/Data/Client.swift @@ -139,6 +139,10 @@ public final class Client: Equatable, Identifiable, Hashable { try await makeEntityRequest(endpoint: endpoint, method: "GET", forceVersion: forceVersion) } + public func getString(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> String { + try await makeEntityRequestForString(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? @@ -180,10 +184,7 @@ public final class Client: Equatable, Identifiable, Hashable { return httpResponse as? HTTPURLResponse } - private func makeEntityRequest(endpoint: Endpoint, - method: String, - forceVersion: Version? = nil) async throws -> Entity - { + 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) @@ -202,6 +203,14 @@ public final class Client: Equatable, Identifiable, Hashable { } } + private func makeEntityRequestForString(endpoint: Endpoint, method: String, forceVersion: Version? = nil) async throws -> String { + 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) + return String(data: data, encoding: .utf8) ?? "" + } + public func oauthURL() async throws -> URL { let app: InstanceApp = try await post(endpoint: Apps.registerApp) critical.withLock { $0.oauthApp = app } diff --git a/Threaded/Data/Content/Tenor.swift b/Threaded/Data/Content/Tenor.swift new file mode 100644 index 0000000..8032baf --- /dev/null +++ b/Threaded/Data/Content/Tenor.swift @@ -0,0 +1,50 @@ +//Made by Lumaa + +import Foundation + +class Tenor { + private static let url: String = "https://tenor.googleapis.com/v2" + private static var token: String = "" + + private static let limit: Int = 20 + private static let contentFilter: String = "off" // all content other than nudity + private static let mediaFilter: String = "preview,tinygif,gif,tinywebm,webm" + + init(token: String) { + _ = Self.getToken() + } + + static func getToken() -> String? { + guard let plist = AppDelegate.readSecret() else { return nil } + Self.token = plist["Tenor_Token"] ?? "" + return Self.token + } + + func search(query: String) { + let params: [URLQueryItem] = [ + .init(name: "q", value: query), + .init(name: "key", value: Self.token), + .init(name: "client_key", value: "\(AppInfo.clientName)-\(AppInfo.appVersion)"), + .init(name: "contentfilter", value: Self.contentFilter), + .init(name: "media_filter", value: Self.mediaFilter) + ] + + if var comp = URLComponents(string: "\(Self.url)/search") { + comp.queryItems = params + if let url = comp.url { + var req = URLRequest(url: url) + req.httpMethod = "GET" + + let semaphore = DispatchSemaphore(value: 0) + + var jsonResponse: [String: Any]? + URLSession.shared.dataTask(with: req) { (data, response, error) in + defer { semaphore.signal() } + if let data = data { + jsonResponse = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } + }.resume() + } + } + } +} diff --git a/Threaded/Data/MastodonRequest.swift b/Threaded/Data/MastodonRequest.swift index 2ded951..5156e8a 100644 --- a/Threaded/Data/MastodonRequest.swift +++ b/Threaded/Data/MastodonRequest.swift @@ -28,6 +28,52 @@ extension Endpoint { } } +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 LinkHandler { public let rawLink: String diff --git a/Threaded/Data/Notifications/Notification.swift b/Threaded/Data/Notifications/Notification.swift index dee5be1..d98413d 100644 --- a/Threaded/Data/Notifications/Notification.swift +++ b/Threaded/Data/Notifications/Notification.swift @@ -49,6 +49,13 @@ extension [Notification] { } } +public extension Array where Element: Hashable { + func uniqued() -> [Element] { + var seen = Set() + return filter { seen.insert($0).inserted } + } +} + public struct GroupedNotification: Identifiable, Hashable { public var id: String? { notifications.first?.id } public var notifications: [Notification] diff --git a/Threaded/Localizable.xcstrings b/Threaded/Localizable.xcstrings index 10dc84a..ba58a67 100644 --- a/Threaded/Localizable.xcstrings +++ b/Threaded/Localizable.xcstrings @@ -2916,4 +2916,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Threaded/Threaded.entitlements b/Threaded/Threaded.entitlements new file mode 100644 index 0000000..8f07b74 --- /dev/null +++ b/Threaded/Threaded.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.lumaa.ThreadedApp + + + diff --git a/Threaded/Views/AttachmentView.swift b/Threaded/Views/AttachmentView.swift index af8bc7d..cc35593 100644 --- a/Threaded/Views/AttachmentView.swift +++ b/Threaded/Views/AttachmentView.swift @@ -318,6 +318,6 @@ private extension View { } #Preview { - AttachmentView(attachments: [.init(id: "ABC", type: "image", url: URL(string: "https://i.stack.imgur.com/HX3Aj.png"), previewUrl: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), description: String("This displays the TabView with a page indicator at the bottom"), meta: nil), .init(id: "DEF", type: "image", url: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), previewUrl: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), description: nil, meta: nil)]) + AttachmentView(attachments: [.init(id: "ABC", type: "image", url: URL(string: "https://i.stack.imgur.com/HX3Aj.png"), previewUrl: URL.placeholder, description: String("This displays the TabView with a page indicator at the bottom"), meta: nil), .init(id: "DEF", type: "image", url: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), previewUrl: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), description: nil, meta: nil)]) .environment(AppDelegate()) } diff --git a/Threaded/Views/NotificationsView.swift b/Threaded/Views/NotificationsView.swift index 0779160..026761f 100644 --- a/Threaded/Views/NotificationsView.swift +++ b/Threaded/Views/NotificationsView.swift @@ -147,10 +147,3 @@ struct NotificationsView: View { var image: Image? = Image(systemName: "paperplane") } } - -public extension Array where Element: Hashable { - func uniqued() -> [Element] { - var seen = Set() - return filter { seen.insert($0).inserted } - } -} diff --git a/Threaded/Views/PostDetailsView.swift b/Threaded/Views/PostDetailsView.swift index df19fb2..4e16c11 100644 --- a/Threaded/Views/PostDetailsView.swift +++ b/Threaded/Views/PostDetailsView.swift @@ -5,6 +5,7 @@ import SwiftUI struct PostDetailsView: View { @EnvironmentObject private var navigator: Navigator @Environment(AccountManager.self) private var accountManager: AccountManager + @Environment(AppDelegate.self) private var delegate: AppDelegate var detailedStatus: Status @@ -128,6 +129,8 @@ struct PostDetailsView: View { statusesContext.append(contentsOf: data.context.descendants) statuses = statusesContext + +// await loadEmbeddedStatus() } catch { if let error = error as? ServerError, error.httpCode == 404 { _ = navigator.path.popLast() diff --git a/Threaded/Views/Settings/ShopView.swift b/Threaded/Views/Settings/ShopView.swift index e56551d..76c8899 100644 --- a/Threaded/Views/Settings/ShopView.swift +++ b/Threaded/Views/Settings/ShopView.swift @@ -304,26 +304,6 @@ extension ShopView { // .environment(\.locale, Locale(identifier: "en-us")) } -private func hasActuallyPlus(customerInfo: CustomerInfo?) -> Bool { - return customerInfo?.entitlements[PlusEntitlements.lifetime.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.monthly.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.yearly.getEntitlementId()]?.isActive == true -} - -private func purchase(entitlement: PlusEntitlements) { - Purchases.shared.getOfferings { (offerings, error) in - if let product = entitlement.toPackage(offerings: offerings) { - Purchases.shared.purchase(package: product) { (transaction, customerInfo, error, userCancelled) in - if hasActuallyPlus(customerInfo: customerInfo) { - print("BOUGHT PLUS") - AppDelegate.premium = true - } - } - } - if let e = error { - print(e) - } - } -} - enum PlusEntitlements: String { case monthly case yearly @@ -355,3 +335,23 @@ enum PlusEntitlements: String { } } } + +private func hasActuallyPlus(customerInfo: CustomerInfo?) -> Bool { + return customerInfo?.entitlements[PlusEntitlements.lifetime.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.monthly.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.yearly.getEntitlementId()]?.isActive == true +} + +private func purchase(entitlement: PlusEntitlements) { + Purchases.shared.getOfferings { (offerings, error) in + if let product = entitlement.toPackage(offerings: offerings) { + Purchases.shared.purchase(package: product) { (transaction, customerInfo, error, userCancelled) in + if hasActuallyPlus(customerInfo: customerInfo) { + print("BOUGHT PLUS") + AppDelegate.premium = true + } + } + } + if let e = error { + print(e) + } + } +} diff --git a/ThreadedWidgets/AppIntent.swift b/ThreadedWidgets/AppIntent.swift new file mode 100644 index 0000000..9c65da5 --- /dev/null +++ b/ThreadedWidgets/AppIntent.swift @@ -0,0 +1,92 @@ +//Made by Lumaa + +import SwiftUI +import SwiftData +import WidgetKit +import AppIntents + +/// Widgets that require to select an account will use this `ConfigurationIntent` +struct AccountAppIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "widget.follow-count" + static var description = IntentDescription("widget.follow-count.description") + + @Parameter(title: "widget.select-account") + var account: AccountEntity? +} + +struct AccountEntity: AppEntity { + let client: Client + let id: String + let username: String + /// Bearer token + let token: OauthToken + + static var typeDisplayRepresentation: TypeDisplayRepresentation = "account" + static var defaultQuery = AccountQuery() + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(stringLiteral: "@\(username)") + } + + init(acct: String, username: String, token: OauthToken) { + self.client = Client(server: String(acct.split(separator: "@")[1]), version: .v2, oauthToken: token) + self.id = acct + self.username = username + self.token = token + } + + init(loggedAccount: LoggedAccount) { + self.client = Client(server: String(loggedAccount.acct.split(separator: "@")[1]), version: .v2, oauthToken: loggedAccount.token) + self.id = loggedAccount.acct + self.username = String(loggedAccount.acct.split(separator: "@")[0]) + self.token = loggedAccount.token + } + + func toUIImage() -> URL? { + Task { [self] in + if let account: String = try? await client.getString(endpoint: Accounts.verifyCredentials) { + do { + if let serialized: [String : Any] = try JSONSerialization.jsonObject(with: account.data(using: String.Encoding.utf8) ?? Data()) as? [String : Any] { + let avatar: String = serialized["avatar"] as! String + return URL(string: avatar) ?? URL.placeholder + } + } catch { + print("Error fetching image data: \(error)") + } + } + return URL.placeholder + } + return URL.placeholder + } +} + + +struct AccountQuery: EntityQuery { + private static func getAccountsQuery() -> [LoggedAccount] { + let query = Query() + let loggedAccounts: [LoggedAccount] = query.wrappedValue + return loggedAccounts + } + + private static func getAccounts() -> [LoggedAccount] { + guard let modelContainer: ModelContainer = try? ModelContainer(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: false)) else { return [] } + let modelContext = ModelContext(modelContainer) + let loggedAccounts = try? modelContext.fetch(FetchDescriptor()) + + return loggedAccounts ?? [] + } + + func entities(for identifiers: [AccountEntity.ID]) async throws -> [AccountEntity] { + let accountEntities: [AccountEntity] = Self.getAccounts().map({ return AccountEntity(loggedAccount: $0) }) + return accountEntities.filter({ identifiers.contains($0.id) }) + } + + func suggestedEntities() async throws -> [AccountEntity] { + let accountEntities: [AccountEntity] = Self.getAccounts().map({ return AccountEntity(loggedAccount: $0) }) + return accountEntities + } + + func defaultResult() async -> AccountEntity? { + try? await suggestedEntities().first + } +} diff --git a/ThreadedWidgets/Assets.xcassets/AccentColor.colorset/Contents.json b/ThreadedWidgets/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/ThreadedWidgets/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ThreadedWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json b/ThreadedWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/ThreadedWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ThreadedWidgets/Assets.xcassets/Contents.json b/ThreadedWidgets/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ThreadedWidgets/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ThreadedWidgets/Assets.xcassets/WidgetBackground.colorset/Contents.json b/ThreadedWidgets/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..fe808bf --- /dev/null +++ b/ThreadedWidgets/Assets.xcassets/WidgetBackground.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/ThreadedWidgets/Info.plist b/ThreadedWidgets/Info.plist new file mode 100644 index 0000000..0f118fb --- /dev/null +++ b/ThreadedWidgets/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/ThreadedWidgets/Localizable.xcstrings b/ThreadedWidgets/Localizable.xcstrings new file mode 100644 index 0000000..f9a6fdb --- /dev/null +++ b/ThreadedWidgets/Localizable.xcstrings @@ -0,0 +1,59 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "@%@" : { + + }, + "account" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account" + } + } + } + }, + "widget.follow-count" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Follower Count" + } + } + } + }, + "widget.follow-count.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Get the current amount of followers of the accounts you have logged in Threaded" + } + } + } + }, + "widget.followers" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Followers" + } + } + } + }, + "widget.select-account" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select an account" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/ThreadedWidgets/Redeclarations.swift b/ThreadedWidgets/Redeclarations.swift new file mode 100644 index 0000000..5d845ef --- /dev/null +++ b/ThreadedWidgets/Redeclarations.swift @@ -0,0 +1,224 @@ +//Made by Lumaa + +import Foundation +import SwiftUI +import UIKit + +// this is messy but it's alr + +public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicationDelegate { + public var window: UIWindow? + public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width + public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height + public private(set) var secret: [String: String] = [:] + + public func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { return } + window = windowScene.keyWindow + } + + override public init() { + super.init() + + if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") { + let url = URL(fileURLWithPath: path) + let data = try! Data(contentsOf: url) + if let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] { + secret = plist + } + } + + windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width + windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height + Self.observedSceneDelegate.insert(self) + _ = Self.observer // just for activating the lazy static property + } + + static func readSecret() -> [String: String]? { + if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") { + let url = URL(fileURLWithPath: path) + let data = try! Data(contentsOf: url) + if let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] { + return plist + } + } + + return nil + } + + deinit { + Task { @MainActor in + Self.observedSceneDelegate.remove(self) + } + } + + private static var observedSceneDelegate: Set = [] + private static let observer = Task { + while true { + try? await Task.sleep(for: .seconds(0.1)) + for delegate in observedSceneDelegate { + let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width + if delegate.windowWidth != newWidth { + delegate.windowWidth = newWidth + } + let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height + if delegate.windowHeight != newHeight { + delegate.windowHeight = newHeight + } + + } + } + } +} + +public extension URL { + static let placeholder: URL = URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg")! +} + +extension AppInfo { + static var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" +} + +public protocol Endpoint: Sendable { + func path() -> 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 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 enum Accounts: Endpoint { + case verifyCredentials + + public func path() -> String { + switch self { + case .verifyCredentials: + "accounts/verify_credentials" + } + } + + public func queryItems() -> [URLQueryItem]? { + nil + } +} + +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 {} + +public struct ServerError: Decodable, Error { + public let error: String? + public var httpCode: Int? +} + +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 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 {} diff --git a/ThreadedWidgets/ThreadedWidgets.swift b/ThreadedWidgets/ThreadedWidgets.swift new file mode 100644 index 0000000..c08de36 --- /dev/null +++ b/ThreadedWidgets/ThreadedWidgets.swift @@ -0,0 +1,186 @@ +//Made by Lumaa + +import WidgetKit +import SwiftUI +import SwiftData + +struct Provider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> SimpleEntry { + let placeholder: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage() + placeholder.withTintColor(UIColor.label) + return SimpleEntry(date: Date(), pfp: placeholder, followers: 38, configuration: AccountAppIntent()) + } + + func snapshot(for configuration: AccountAppIntent, in context: Context) async -> SimpleEntry { + let data = await getData(configuration: configuration) + return SimpleEntry(date: Date(), pfp: data.0, followers: data.1, configuration: configuration) + } + + func timeline(for configuration: AccountAppIntent, in context: Context) async -> Timeline { + var entries: [SimpleEntry] = [] + + let data = await getData(configuration: configuration) + + // Generate a timeline consisting of two entries an hour apart, starting from the current date. + let currentDate = Date() + for hourOffset in 0 ..< 2 { + let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! + let entry = SimpleEntry(date: entryDate, pfp: data.0, followers: data.1, configuration: configuration) + entries.append(entry) + } + + return Timeline(entries: entries, policy: .atEnd) + } + + private func getData(configuration: AccountAppIntent) async -> (UIImage, Int) { + var pfp: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage() + pfp.withTintColor(UIColor.label) + if let account = configuration.account { + do { + let acc = try await account.client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1) + + if let serialized: [String : Any] = try JSONSerialization.jsonObject(with: acc.data(using: String.Encoding.utf8) ?? Data()) as? [String : Any] { + let avatar: String = serialized["avatar"] as! String + let task = try await URLSession.shared.data(from: URL(string: avatar)!) + pfp = UIImage(data: task.0) ?? UIImage() + + let followers: Int = serialized["followers_count"] as! Int + return (pfp, followers) + } + } catch { + print(error) + } + } + return (pfp, 0) + } +} + +struct SimpleEntry: TimelineEntry { + let date: Date + let pfp: UIImage + let followers: Int + let configuration: AccountAppIntent +} + +struct FollowCountWidgetView: View { + @Environment(\.widgetFamily) private var family: WidgetFamily + var entry: Provider.Entry + + var body: some View { + if let account = entry.configuration.account { + ZStack { + if family == WidgetFamily.systemSmall { + small + } else if family == WidgetFamily.systemMedium { + medium + } else if family == WidgetFamily.accessoryRectangular { + rectangular + } else { + Text(String("Unsupported WidgetFamily")) + .font(.caption) + } + } + .modelContainer(for: [LoggedAccount.self]) + } else { + Text("widget.select-account") + .font(.caption) + } + } + + var small: some View { + VStack(alignment: .center) { + Image(uiImage: entry.pfp) + .resizable() + .scaledToFit() + .frame(width: 60, height: 60) + .foregroundStyle(Color.white) + .clipShape(Circle()) + + Spacer() + + Text(entry.followers, format: .number.notation(.compactName)) + .font(.title.monospacedDigit().bold()) + .contentTransition(.numericText()) + Text("widget.followers") + .font(.caption) + .foregroundStyle(Color.gray) + + Spacer() + + Text("@\(entry.configuration.account!.username)") + .redacted(reason: .privacy) + .font(.caption.bold()) + } + } + + var medium: some View { + HStack { + Spacer() + + VStack(alignment: .center, spacing: 10) { + Image(uiImage: entry.pfp) + .resizable() + .scaledToFit() + .frame(width: 60, height: 60) + .foregroundStyle(Color.white) + .clipShape(Circle()) + + Text("@\(entry.configuration.account!.username)") + .redacted(reason: .privacy) + .font(.caption.bold()) + } + + Spacer() + + VStack { + Text(entry.followers, format: .number.notation(.compactName)) + .font(.title.monospacedDigit().bold()) + .contentTransition(.numericText()) + Text("widget.followers") + .font(.caption) + .foregroundStyle(Color.gray) + } + + Spacer() + } + } + + var rectangular: some View { + HStack { + VStack(alignment: .leading) { + Text("@\(entry.configuration.account!.username)") + .multilineTextAlignment(.leading) + .font(.caption) + .opacity(0.7) + + Text(entry.followers, format: .number.notation(.compactName)) + .multilineTextAlignment(.leading) + .font(.system(size: 32, weight: .bold).monospacedDigit()) + .contentTransition(.numericText()) + } + .padding(.horizontal, 7.5) + + Spacer() + } + } +} + +struct FollowCountWidget: Widget { + let kind: String = "FollowCountWidget" + let modelContainer: ModelContainer + + init() { + guard let modelContainer: ModelContainer = try? .init(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true)) else { fatalError("Couldn't get LoggedAccounts") } + self.modelContainer = modelContainer + } + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, intent: AccountAppIntent.self, provider: Provider()) { entry in + FollowCountWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground"), for: .widget) + } + .configurationDisplayName("widget.follow-count") + .description("widget.follow-count.description") + .supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular]) + } +} diff --git a/ThreadedWidgets/ThreadedWidgetsBundle.swift b/ThreadedWidgets/ThreadedWidgetsBundle.swift new file mode 100644 index 0000000..f2e563f --- /dev/null +++ b/ThreadedWidgets/ThreadedWidgetsBundle.swift @@ -0,0 +1,12 @@ +//Made by Lumaa + +import WidgetKit +import SwiftUI +import UIKit + +@main +struct ThreadedWidgetsBundle: WidgetBundle { + var body: some Widget { + FollowCountWidget() + } +} diff --git a/ThreadedWidgets/ThreadedWidgetsExtension.entitlements b/ThreadedWidgets/ThreadedWidgetsExtension.entitlements new file mode 100644 index 0000000..8f07b74 --- /dev/null +++ b/ThreadedWidgets/ThreadedWidgetsExtension.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.lumaa.ThreadedApp + + +