Compare commits

...

78 commits

Author SHA1 Message Date
Thomas Ricouard 04af087c4b Bump version to 1.10.41 2024-05-16 07:05:17 +02:00
Thomas Ricouard a9398c25af fix #2064 2024-05-15 10:52:41 +02:00
Thomas Ricouard 13d721912b Add lists widget 2024-05-15 09:27:35 +02:00
Thomas Ricouard e3d4e693d2 More improvement to alt edit 2024-05-15 08:30:57 +02:00
Thomas Ricouard 86c053344b Improve media alt edit 2024-05-15 08:28:05 +02:00
Thomas Ricouard a996aace80 Add translate for image alt 2024-05-14 19:43:52 +02:00
Jesús Jiménez Sánchez 18a1d17230
Update ES localization (#2076) 2024-05-14 19:37:40 +02:00
Thomas Ricouard 69cb9a20f9 Add native translate for media description edit + profile bio 2024-05-14 19:36:25 +02:00
Thomas Ricouard bab2b4be9c Fix localization 2024-05-14 12:20:25 +02:00
Thomas Ricouard bb005386df Merge branch 'main' of https://github.com/Dimillian/IceCubesApp 2024-05-13 22:20:45 +02:00
Thomas Ricouard c77bb992b4 Update OpenAI models to gpt-4o 2024-05-13 22:20:43 +02:00
Paul Schuetz 7caf00d07d
Resolve escaped characters in a status (#2071)
* Resolve escaped characters in a status

Escaped characters are now returned to their original form for
HTMLString.asRawText.

* Unescape the markdown version too

The HTMLString.asMarkdown string is now also unescaped, & and
similar are resolved.

* Fix a internal fallback

If one of the unescape(...) commands fails, the original, unescaped
text is used instead of an empty string.
2024-05-13 21:32:38 +02:00
Thomas Ricouard 6ed760a775 Bump version to 1.10.40 2024-05-13 20:20:24 +02:00
Thomas Ricouard ecd149b3d2 Fix a crash in quote post editor on macOS 2024-05-13 19:26:48 +02:00
Xabi 9aaf0b2350
Update EU localisation (#2062)
Round 2
2024-05-13 13:28:24 +02:00
Cthulhux 2d6cce6b01
de: translated one more string (#2063) 2024-05-13 13:27:58 +02:00
Paul Schuetz 48faddebea
Implement Apple Translate (#2065)
* Implement a first version of Apple's Translation

The user can now choose between his instance's server, DeepL (with API
key) and Apple's Translation framework. A translation is cleared if
the translation type is changed. The strings aren't yet written, but
the translations settings view's inconsistent background is now fixed.

* Transfer the old "always_use_deepl" setting

The "always_use_deepl"-setting is now deleted, but its content is
transferred to the equivalent value in "preferred_translation_type".

* Show the user if the DeepL-API key is still stored

The user is now shown a prompt if they've switched away from
.useDeepl, but there's still an API key stored. The API key is not
deleted if the user doesn't instruct the app to do so, so this change
makes it more transparent, since a user might not expect the key to
be stored and might not want this to be the case.

* Localize Labels

The labels for the buttons and options are now localized. "DeepL API Key" is written consistently (with uppercase Key)

* Run all the strings through localization

The strings "DeepL" and "Apple Translate" are now also saved in
localizable.strings and addressed through keys. They were taken
directly previously, which was inconsistent.

* Fix storage

The selected value for preferredTranslationType wasn't stored, the
synchronization between UserPreferences and Storage is now in place.

* Hide Apple Translate if not yet on iOS 17.4

The Apple Translate option is hidden if the user hasn't updated their
phone to at least iOS 17.4. If the Apple Translate option is selected
but the user has downgraded to before iOS 17.4, the standard instance
option is selected.

* Consistently show Apple Translate

Apple Translate was previously only shown if the standard translate
button was visible, that is now fixed. It's now attached to the
StatusRowView, which is always present.

* Animate the removal of translations

The reset of a translation when the translation type is changed is now
animated, which is important for iPad users if they've translated a
post in the sidebar.

* Add support for the Mac Catalyst build

The Mac Catalyst Version doesn't allow the import of the api, so
compiler flags now check if the import isn't allowed and then remove
all references to Apple Translate.

* Swift Format

* Revert "Run all the strings through localization"

This reverts commit 86c5099662.

# Conflicts:
#	Packages/Env/Sources/Env/TranslationType.swift

* Remove the DeepL fallback

The DeepL fallback for the instance translation service is removed,
error messages are shown if a translation fails.

* Allow for the use of an User API Key as fallback

The DeepL fallback is reinstated if the user has put in their own API
Key

* Make the localization keys clear strings

* Make Apple and the instance a fallback

Apple Translate is now a fallback for both other translation types,
the instance service is a fallback for DeepL.
2024-05-13 13:27:21 +02:00
Thomas Ricouard a8039df22d Don't open link on secondary column 2024-05-13 09:27:24 +02:00
Thomas Ricouard e21ec0bd1f Add expanded sidebar layout 2024-05-08 11:51:28 +02:00
Thomas Ricouard 9c42a3d7cc Add copy button for alt text 2024-05-08 11:03:25 +02:00
Thomas Ricouard 54a16b2c9a Fix unboost icon 2024-05-08 11:00:40 +02:00
Thomas Ricouard a6f3068728 Add accounts list placeholder 2024-05-08 10:59:31 +02:00
Thomas Ricouard f04258ec04 Revert "Delete unused functions in TimelineDatasource.swift (#2037)"
This reverts commit e9a2d3e151.
2024-05-08 10:50:22 +02:00
Cthulhux 8468e51c17
de: Update Localizable.xcstrings (#2057)
(Not entirely sure whether to translate "TimelineFilter" et al.)
2024-05-08 10:39:09 +02:00
Noah Martin e9a2d3e151
Delete unused functions in TimelineDatasource.swift (#2037) 2024-05-08 10:38:36 +02:00
Igor Camilo 1f56fa1b9b
Add tooltip to sidebar buttons. (#2040) 2024-05-08 10:38:27 +02:00
Jerry Zhang ccad00a094
Update Simplified Chinese localization (#2052)
* Update Simplified Chinese localization

* Fix

---------

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2024-05-08 10:32:26 +02:00
Thomas Ricouard 51fecb01f5 Bump version to 1.10.39 2024-05-06 09:25:36 +02:00
Thomas Ricouard c29de44d8c Widget: Mentions only allow large size 2024-05-06 09:20:01 +02:00
Thomas Ricouard 1d79832544 Bump version to 1.10.38 2024-05-06 08:41:33 +02:00
Thomas Ricouard a37316c56f Lint 2024-05-06 08:38:37 +02:00
Thomas Ricouard 189e10f2b4 Widget: Add mentions widget 2024-05-06 08:37:58 +02:00
Thomas Ricouard 24d5ecd119 Shortcuts: Fix image 2024-05-05 21:13:34 +02:00
Thomas Ricouard ee6f003073 Widget: Improve text size 2024-05-05 20:03:12 +02:00
Thomas Ricouard 7328c00006 Widget: Remove optional parameters 2024-05-05 19:41:04 +02:00
Thomas Ricouard a6fd8d1137 Widget: Renaming 2024-05-05 19:37:06 +02:00
Thomas Ricouard ea31cda3c2 Widget: Add Hashtag widget 2024-05-05 19:31:28 +02:00
Thomas Ricouard 8ab7b5ac69 Fix app group 2024-05-05 19:03:25 +02:00
Thomas Ricouard 7aebe530dd Widget: More UI refinements 2024-05-05 18:29:48 +02:00
Thomas Ricouard a2afd4f58f Fix widget bundle identifier 2024-05-05 18:16:20 +02:00
Thomas Ricouard 88218cd6ec Style fix 2024-05-05 18:10:32 +02:00
Thomas Ricouard c4dee39efe More fix for timeline widget 2024-05-05 18:06:47 +02:00
Thomas Ricouard 73651cb7f1 Polish on timeline widget 2024-05-05 17:47:08 +02:00
Thomas Ricouard dd1615f0e3 Fix widget entitlements 2024-05-05 17:30:09 +02:00
Thomas Ricouard 6bd14e0f8d Don't embed widgets on visionOS 2024-05-05 13:34:22 +02:00
Thomas Ricouard 1ca4a74ff0 Initial widget support 2024-05-05 13:12:19 +02:00
Thomas Ricouard c3edabb183 Lint 2024-05-04 13:19:19 +02:00
Thomas Ricouard ba4cc899f8 Add inline post Intent 2024-05-04 13:12:43 +02:00
Thomas Ricouard 5a93184c6d Rename Intent 2024-05-04 11:34:51 +02:00
Thomas Ricouard 66754ecc7c Fix editor progress bar 2024-05-04 11:27:52 +02:00
Thomas Ricouard e857439a02 Bump version to 1.10.37 2024-05-03 16:25:30 +02:00
Jesús Jiménez Sánchez ed620e86ca
Update ES localization (#2048)
Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2024-05-03 16:21:40 +02:00
Cthulhux 936bc96ff7
de:Update Localizable.xcstrings (#2051)
Time for a few new strings

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2024-05-03 16:19:32 +02:00
Xabi 37b441a43d
Update EU localisation (#2049)
Added recent strings
2024-05-03 16:18:28 +02:00
Thomas Ricouard 07af494dcb Allow text connect 2024-05-02 12:05:48 +02:00
Thomas Ricouard 49a5c6a56a Add more shortcuts 2024-05-02 11:37:38 +02:00
Thomas Ricouard 4e4d903c44 Add AppIntent service + post to Mastodon intent 2024-05-02 09:32:19 +02:00
Thomas Ricouard abcd4cc321 Add muted and blocked accounts list 2024-05-02 08:43:58 +02:00
Euigyom Kim 6a7df1065d
Fix scrolling issue on emoji picker (#2032)
* Fix scrolling issue in emoji picker

* Fix design on emoji section header
2024-04-22 16:38:31 +02:00
Thomas Ricouard c0b855ea55 Bump version to 1.10.36 2024-04-21 15:07:48 +02:00
Thomas Ricouard 4c3047b0b9 Fix packages 2024-04-19 06:56:48 -07:00
Thomas Ricouard 899b92e390 Revert "Fix StatusRowContentView invade SwipeActions area (#2007)"
This reverts commit 3782300b27.
2024-04-19 06:46:56 -07:00
Thomas Ricouard e71c55b488 Bump version to 1.10.35 2024-04-19 06:46:49 -07:00
Thomas Ricouard 361b5f1d84 Fix tests 2024-04-17 11:11:22 -07:00
Thomas Ricouard ad61600328 Fix localizations 2024-04-17 11:05:21 -07:00
Thomas Ricouard 5f1f71068c Update packages 2024-04-17 10:55:00 -07:00
tkgka 3782300b27
Fix StatusRowContentView invade SwipeActions area (#2007)
* Fix StatusRowContentView invade SwipeActions area

* ./ add padding inside StatusRowMediaPreviewView
2024-04-17 10:54:46 -07:00
Andrzej Rózga 7d47834903
Polish localization update (#2029) 2024-04-07 09:16:47 +02:00
Jesús Jiménez Sánchez 65a83fa636
Update ES localization (#2028) 2024-04-07 09:16:41 +02:00
Nathan Reed 8038e8e6af
Improve deep link handling on cold start (#2026)
Previously, if the app was not already running when the Safari action extension was used to open a post in the app, the post would open in the in-app Safari instead of using the Ice Cubes UI.
The action extension only worked well if Ice Cubes was already running but backgrounded when it was used.
This was because of the `hasConnection(with:)` check used to ensure that the current server has a federation relationship with the server the post is on.
Early in app launch, the list of federated peers has not come back from the API request yet, so `hasConnection(with:)` was always returning `false`.

To fix, issue a request to fetch the peers as part of the URL handling process, before checking `hasConnection(with:)` to make the final navigation decision.
As an optimization, only do this if `hasConnection(with:)` returns `false` initially -- if it returns `true`, we already know a connection exists so no need to check again.
2024-04-02 08:26:58 +02:00
Ahnaf Mahmud eb82a67671
Update menu bar and copyright (#2025)
* Update menu bar

* Update copyright
2024-04-02 08:25:52 +02:00
Ege Sucu bc5bb8272a
TR language updated & Changes Reviewed (#2023) 2024-03-29 10:24:44 +01:00
Xabi d2ead5b6d1
Update EU localisation (#2017)
newest strings
2024-03-28 15:32:06 +01:00
Jerry Zhang d22370959c
Update Simplified Chinese localization (#2018)
* Update Simplified Chinese localization

* fix SC localization
2024-03-28 15:31:59 +01:00
Alessio Mason 2c9b841f30
Latest Italian localizations (#2020) 2024-03-28 15:31:39 +01:00
Cthulhux 2e3cf4aace
de: translated the new filters (#2022) 2024-03-28 15:31:31 +01:00
Thomas Ricouard 7de563a6eb Fix packages 2024-03-27 09:17:33 +01:00
Thomas Ricouard 3aae2e6623 bump version to 1.10.34 2024-03-27 09:04:13 +01:00
82 changed files with 4513 additions and 760 deletions

View file

@ -26,8 +26,6 @@
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; };
9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; };
9F2A540729699698009B2D7C /* SupportAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A540629699698009B2D7C /* SupportAppView.swift */; };
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540929699705009B2D7C /* ReceiptParser */; };
9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540B29699705009B2D7C /* RevenueCat */; };
9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F2A540D2969A0B0009B2D7C /* StoreKit.framework */; };
9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5410296A1429009B2D7C /* PushNotificationsView.swift */; };
9F2A5419296AB631009B2D7C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5418296AB631009B2D7C /* NotificationService.swift */; };
@ -44,6 +42,11 @@
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; };
9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; };
9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4B2952005C00B3281A /* MessagesTab.swift */; };
9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */; };
9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */; };
9F37BDDF2BE37C35007F28AD /* TabIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */; };
9F37BDE12BE38646007F28AD /* PostImageIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDE02BE38646007F28AD /* PostImageIntent.swift */; };
9F37BDE32BE393A7007F28AD /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */; };
9F38A7332ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; };
9F38A7342ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; };
9F38A7352ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; };
@ -54,6 +57,9 @@
9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4A48182976B21900A1A038 /* ProfileTab.swift */; };
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F55C68C2955968700F94077 /* ExploreTab.swift */; };
9F55C6902955993C00F94077 /* Explore in Frameworks */ = {isa = PBXBuildFile; productRef = 9F55C68F2955993C00F94077 /* Explore */; };
9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6252BF48FE10074387E /* ListEntity.swift */; };
9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */; };
9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6212BF48FBA0074387E /* ListsWidget.swift */; };
9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; };
9F6028562B3F36AE00476078 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028552B3F36AE00476078 /* AppView.swift */; };
9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028572B3F3B7600476078 /* ToolbarTab.swift */; };
@ -63,10 +69,27 @@
9F7335EF29674F7100AFF0BA /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7335EE29674F7100AFF0BA /* QuickLook.framework */; };
9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */; };
9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */; };
9F7788C02BE63935004E6BEF /* InlinePostIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */; };
9F7788C72BE652B1004E6BEF /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C62BE652B1004E6BEF /* WidgetKit.framework */; };
9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */; };
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */; };
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */; };
9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */; };
9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F7788D12BE652B2004E6BEF /* Assets.xcassets */; };
9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DD2BE6543D004E6BEF /* Account */; };
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DF2BE6543D004E6BEF /* AppAccount */; };
9F7788E22BE6543D004E6BEF /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E12BE6543D004E6BEF /* Env */; };
9F7788E42BE6543D004E6BEF /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E32BE6543D004E6BEF /* Models */; };
9F7788E62BE6543D004E6BEF /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E52BE6543D004E6BEF /* Network */; };
9F7788E82BE65533004E6BEF /* AppAccountEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */; };
9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */; };
9F7788ED2BE78D75004E6BEF /* TimelineFilterEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */; };
9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */; };
9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788EF2BE78E77004E6BEF /* Timeline */; };
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; };
9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */; };
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9FAD85822971BF7200496AB1 /* Secret.plist */; };
9FAD858B29743F7400496AB1 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD858A29743F7400496AB1 /* ShareViewController.swift */; };
9FAD858E29743F7400496AB1 /* (null) in Resources */ = {isa = PBXBuildFile; };
9FAD859229743F7400496AB1 /* IceCubesShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -98,6 +121,13 @@
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE3DB56296FEFCA00628CB0 /* AppAccount */; };
9FE4CCAB2B4C848A00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = 9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */; };
9FE4CCAD2B4C849F00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE4CCAC2B4C849F00DA5F13 /* GiphyUISDK */; };
9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE6A42D2BD043A90055D388 /* RevenueCat */; };
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */; };
9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */; };
9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */; };
9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB682BE7F842001560CE /* SharedUtils.swift */; };
9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */; };
9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */; };
9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; };
9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; };
9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; };
@ -123,6 +153,13 @@
remoteGlobalIDString = 9F2A5415296AB631009B2D7C;
remoteInfo = IceCubesNotifications;
};
9F7788D42BE652B2004E6BEF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9FBFE631292A715500C250E9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9F7788C42BE652B1004E6BEF;
remoteInfo = IceCubesAppWidgetsExtensionExtension;
};
9FAD859029743F7400496AB1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9FBFE631292A715500C250E9 /* Project object */;
@ -149,6 +186,7 @@
E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */,
9F2A541D296AB631009B2D7C /* IceCubesNotifications.appex in Embed Foundation Extensions */,
9FAD859229743F7400496AB1 /* IceCubesShareExtension.appex in Embed Foundation Extensions */,
9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@ -197,6 +235,11 @@
9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = "<group>"; };
9F35DB4829506F7F00B3281A /* Notifications */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Notifications; path = Packages/Notifications; sourceTree = "<group>"; };
9F35DB4B2952005C00B3281A /* MessagesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTab.swift; sourceTree = "<group>"; };
9F37BDDA2BE36E22007F28AD /* PostIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostIntent.swift; sourceTree = "<group>"; };
9F37BDDC2BE37193007F28AD /* AppIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentService.swift; sourceTree = "<group>"; };
9F37BDDE2BE37C35007F28AD /* TabIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabIntent.swift; sourceTree = "<group>"; };
9F37BDE02BE38646007F28AD /* PostImageIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImageIntent.swift; sourceTree = "<group>"; };
9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; };
9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = "<group>"; };
9F398AA52935FE8A00A889F2 /* AppRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRegistry.swift; sourceTree = "<group>"; };
@ -205,6 +248,9 @@
9F4A48182976B21900A1A038 /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = "<group>"; };
9F55C68C2955968700F94077 /* ExploreTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTab.swift; sourceTree = "<group>"; };
9F55C68E295598F900F94077 /* Explore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Explore; path = Packages/Explore; sourceTree = "<group>"; };
9F5BE6212BF48FBA0074387E /* ListsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidget.swift; sourceTree = "<group>"; };
9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidgetConfiguration.swift; sourceTree = "<group>"; };
9F5BE6252BF48FE10074387E /* ListEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEntity.swift; sourceTree = "<group>"; };
9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = "<group>"; };
9F6028552B3F36AE00476078 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
9F6028572B3F3B7600476078 /* ToolbarTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarTab.swift; sourceTree = "<group>"; };
@ -215,10 +261,21 @@
9F7335EE29674F7100AFF0BA /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.2.sdk/System/Library/Frameworks/QuickLook.framework; sourceTree = DEVELOPER_DIR; };
9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoteTimelineView.swift; sourceTree = "<group>"; };
9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySettingsView.swift; sourceTree = "<group>"; };
9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinePostIntent.swift; sourceTree = "<group>"; };
9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesAppWidgetsExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
9F7788C62BE652B1004E6BEF /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
9F7788C82BE652B1004E6BEF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesAppWidgetsExtensionBundle.swift; sourceTree = "<group>"; };
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidget.swift; sourceTree = "<group>"; };
9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidgetConfiguration.swift; sourceTree = "<group>"; };
9F7788D12BE652B2004E6BEF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9F7788D32BE652B2004E6BEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtension.entitlements; sourceTree = "<group>"; };
9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountEntity.swift; sourceTree = "<group>"; };
9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilterEntity.swift; sourceTree = "<group>"; };
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = "<group>"; };
9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = "<group>"; };
9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
9FAD85822971BF7200496AB1 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secret.plist; sourceTree = "<group>"; };
9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
9FAD858F29743F7400496AB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -243,6 +300,12 @@
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidgetConfiguration.swift; sourceTree = "<group>"; };
9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidget.swift; sourceTree = "<group>"; };
9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsWidgetView.swift; sourceTree = "<group>"; };
9FF2FB682BE7F842001560CE /* SharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtils.swift; sourceTree = "<group>"; };
9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidget.swift; sourceTree = "<group>"; };
9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidgetConfiguration.swift; sourceTree = "<group>"; };
B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@ -278,6 +341,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
9F7788C22BE652B1004E6BEF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9F7788E42BE6543D004E6BEF /* Models in Frameworks */,
9F7788E62BE6543D004E6BEF /* Network in Frameworks */,
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */,
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */,
9F7788E22BE6543D004E6BEF /* Env in Frameworks */,
9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */,
9F7788C72BE652B1004E6BEF /* WidgetKit.framework in Frameworks */,
9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9FAD858529743F7400496AB1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -300,7 +378,6 @@
9F7335EF29674F7100AFF0BA /* QuickLook.framework in Frameworks */,
9FE4CCAB2B4C848A00DA5F13 /* GiphyUISDK in Frameworks */,
9F7335ED2967463400AFF0BA /* AVKit.framework in Frameworks */,
9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */,
9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */,
9F55C6902955993C00F94077 /* Explore in Frameworks */,
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
@ -311,8 +388,8 @@
9FD542E72962D2FF0045321A /* Lists in Frameworks */,
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
9F5E581929545BE700A53960 /* Env in Frameworks */,
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */,
DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */,
9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */,
9F295540292B6C3400E0E81B /* Timeline in Frameworks */,
9F35DB4A29506FA100B3281A /* Notifications in Frameworks */,
9FC2A38B2B49D19A00DFD1C1 /* StatusKit in Frameworks */,
@ -351,6 +428,22 @@
path = IceCubesNotifications;
sourceTree = "<group>";
};
9F37BDD92BE36E08007F28AD /* IceCubesAppIntents */ = {
isa = PBXGroup;
children = (
9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */,
9F37BDDC2BE37193007F28AD /* AppIntentService.swift */,
9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */,
9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */,
9F37BDE02BE38646007F28AD /* PostImageIntent.swift */,
9F37BDDA2BE36E22007F28AD /* PostIntent.swift */,
9F37BDDE2BE37C35007F28AD /* TabIntent.swift */,
9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */,
9F5BE6252BF48FE10074387E /* ListEntity.swift */,
);
path = IceCubesAppIntents;
sourceTree = "<group>";
};
9F398AB429360A5800A889F2 /* App */ = {
isa = PBXGroup;
children = (
@ -375,6 +468,15 @@
path = Resources;
sourceTree = "<group>";
};
9F5BE6202BF48FB20074387E /* ListsWidget */ = {
isa = PBXGroup;
children = (
9F5BE6212BF48FBA0074387E /* ListsWidget.swift */,
9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */,
);
path = ListsWidget;
sourceTree = "<group>";
};
9F654BF0299AC46200D27FA5 /* Report */ = {
isa = PBXGroup;
children = (
@ -392,6 +494,22 @@
path = Timeline;
sourceTree = "<group>";
};
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = {
isa = PBXGroup;
children = (
9F5BE6202BF48FB20074387E /* ListsWidget */,
9FF2FB6B2BE8AE78001560CE /* MentionWidget */,
9FF2FB642BE7F7FA001560CE /* Shared */,
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */,
9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */,
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */,
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */,
9F7788D12BE652B2004E6BEF /* Assets.xcassets */,
9F7788D32BE652B2004E6BEF /* Info.plist */,
);
path = IceCubesAppWidgetsExtension;
sourceTree = "<group>";
};
9FA0D2AC29921C1F008A143B /* Embeds */ = {
isa = PBXGroup;
children = (
@ -456,9 +574,11 @@
DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */,
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */,
9FBFE63B292A715500C250E9 /* IceCubesApp */,
9F37BDD92BE36E08007F28AD /* IceCubesAppIntents */,
E9DF41FD29830FEC0003AAD2 /* IceCubesActionExtension */,
9F2A5417296AB631009B2D7C /* IceCubesNotifications */,
9FAD858929743F7400496AB1 /* IceCubesShareExtension */,
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */,
9FBFE63A292A715500C250E9 /* Products */,
9FBFE64C292A72BD00C250E9 /* Frameworks */,
9FE3DB55296FEF5800628CB0 /* AppAccount */,
@ -484,6 +604,7 @@
9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */,
9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */,
E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */,
9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -496,7 +617,6 @@
9FAE4AC8293774FF00772766 /* Info.plist */,
9F398AB429360A5800A889F2 /* App */,
9F398AB529360A6100A889F2 /* Resources */,
9FAD85822971BF7200496AB1 /* Secret.plist */,
);
path = IceCubesApp;
sourceTree = "<group>";
@ -509,6 +629,8 @@
9F7335EE29674F7100AFF0BA /* QuickLook.framework */,
9F7335EB2967461B00AFF0BA /* AVKit.framework */,
E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */,
9F7788C62BE652B1004E6BEF /* WidgetKit.framework */,
9F7788C82BE652B1004E6BEF /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -538,6 +660,42 @@
path = Settings;
sourceTree = "<group>";
};
9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */ = {
isa = PBXGroup;
children = (
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */,
9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */,
);
path = LatestPostsWidget;
sourceTree = "<group>";
};
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */ = {
isa = PBXGroup;
children = (
9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */,
9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */,
);
path = HashtagPostsWidget;
sourceTree = "<group>";
};
9FF2FB642BE7F7FA001560CE /* Shared */ = {
isa = PBXGroup;
children = (
9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */,
9FF2FB682BE7F842001560CE /* SharedUtils.swift */,
);
path = Shared;
sourceTree = "<group>";
};
9FF2FB6B2BE8AE78001560CE /* MentionWidget */ = {
isa = PBXGroup;
children = (
9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */,
9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */,
);
path = MentionWidget;
sourceTree = "<group>";
};
E9B576C029743F2A00BCE646 /* Localization */ = {
isa = PBXGroup;
children = (
@ -589,6 +747,31 @@
productReference = 9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */;
productType = "com.apple.product-type.app-extension";
};
9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9F7788D82BE652B2004E6BEF /* Build configuration list for PBXNativeTarget "IceCubesAppWidgetsExtensionExtension" */;
buildPhases = (
9F7788C12BE652B1004E6BEF /* Sources */,
9F7788C22BE652B1004E6BEF /* Frameworks */,
9F7788C32BE652B1004E6BEF /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = IceCubesAppWidgetsExtensionExtension;
packageProductDependencies = (
9F7788DD2BE6543D004E6BEF /* Account */,
9F7788DF2BE6543D004E6BEF /* AppAccount */,
9F7788E12BE6543D004E6BEF /* Env */,
9F7788E32BE6543D004E6BEF /* Models */,
9F7788E52BE6543D004E6BEF /* Network */,
9F7788EF2BE78E77004E6BEF /* Timeline */,
);
productName = IceCubesAppWidgetsExtensionExtension;
productReference = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
9FAD858729743F7400496AB1 /* IceCubesShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9FAD859329743F7400496AB1 /* Build configuration list for PBXNativeTarget "IceCubesShareExtension" */;
@ -631,6 +814,7 @@
9F2A541C296AB631009B2D7C /* PBXTargetDependency */,
9FAD859129743F7400496AB1 /* PBXTargetDependency */,
E9DF420629830FEC0003AAD2 /* PBXTargetDependency */,
9F7788D52BE652B2004E6BEF /* PBXTargetDependency */,
);
name = IceCubesApp;
packageProductDependencies = (
@ -644,12 +828,11 @@
9F55C68F2955993C00F94077 /* Explore */,
9FD542E62962D2FF0045321A /* Lists */,
9F7335E92966B3F800AFF0BA /* Conversations */,
9F2A540929699705009B2D7C /* ReceiptParser */,
9F2A540B29699705009B2D7C /* RevenueCat */,
9FE3DB56296FEFCA00628CB0 /* AppAccount */,
DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */,
9FC2A38A2B49D19A00DFD1C1 /* StatusKit */,
9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */,
9FE6A42D2BD043A90055D388 /* RevenueCat */,
);
productName = IceCubesApp;
productReference = 9FBFE639292A715500C250E9 /* Ice Cubes.app */;
@ -683,12 +866,15 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1500;
TargetAttributes = {
9F2A5415296AB631009B2D7C = {
CreatedOnToolsVersion = 14.2;
};
9F7788C42BE652B1004E6BEF = {
CreatedOnToolsVersion = 15.3;
};
9FAD858729743F7400496AB1 = {
CreatedOnToolsVersion = 14.2;
};
@ -724,13 +910,14 @@
be,
uk,
"zh-Hant",
Base,
);
mainGroup = 9FBFE630292A715500C250E9;
packageReferences = (
9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */,
9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */,
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
9FE4CCA92B4C848A00DA5F13 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */,
9FE6A42C2BD043A80055D388 /* XCRemoteSwiftPackageReference "purchases-ios" */,
);
productRefGroup = 9FBFE63A292A715500C250E9 /* Products */;
projectDirPath = "";
@ -740,6 +927,7 @@
E9DF41F929830FEC0003AAD2 /* IceCubesActionExtension */,
9F2A5415296AB631009B2D7C /* IceCubesNotifications */,
9FAD858729743F7400496AB1 /* IceCubesShareExtension */,
9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */,
);
};
/* End PBXProject section */
@ -755,6 +943,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
9F7788C32BE652B1004E6BEF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9FAD858629743F7400496AB1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -778,7 +974,6 @@
9F18801829AE477F00D85459 /* favorite.wav in Resources */,
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */,
069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */,
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */,
9F18801229AE477F00D85459 /* tabSelection.wav in Resources */,
9F18801429AE477F00D85459 /* bookmark.wav in Resources */,
9F18801629AE477F00D85459 /* refresh.wav in Resources */,
@ -809,6 +1004,27 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
9F7788C12BE652B1004E6BEF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */,
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */,
9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */,
9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */,
9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */,
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */,
9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */,
9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */,
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */,
9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */,
9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */,
9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */,
9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */,
9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9FAD858429743F7400496AB1 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -830,6 +1046,11 @@
9F15D6042B3DC2180008C220 /* NavigationSheet.swift in Sources */,
9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */,
9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */,
9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */,
9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */,
9F37BDE12BE38646007F28AD /* PostImageIntent.swift in Sources */,
9F37BDDF2BE37C35007F28AD /* TabIntent.swift in Sources */,
9F7788C02BE63935004E6BEF /* InlinePostIntent.swift in Sources */,
9FAD85CF2975B68900496AB1 /* SideBarView.swift in Sources */,
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */,
9FC14EF42B494D940006CEE1 /* RemoteTimelinesSettingView.swift in Sources */,
@ -848,9 +1069,12 @@
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,
639CDF9C296AC82F00C35E58 /* SafariRouter.swift in Sources */,
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
9F7788ED2BE78D75004E6BEF /* TimelineFilterEntity.swift in Sources */,
9F37BDE32BE393A7007F28AD /* AppShortcuts.swift in Sources */,
9F654BEF299AC45B00D27FA5 /* ReportView.swift in Sources */,
D08A9C3529956CFA00204A4A /* SwipeActionsSettingsView.swift in Sources */,
9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */,
9F7788E82BE65533004E6BEF /* AppAccountEntity.swift in Sources */,
9FC14EF62B494DFF0006CEE1 /* RecenTagsSettingView.swift in Sources */,
9F6028562B3F36AE00476078 /* AppView.swift in Sources */,
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
@ -877,6 +1101,15 @@
target = 9F2A5415296AB631009B2D7C /* IceCubesNotifications */;
targetProxy = 9F2A541B296AB631009B2D7C /* PBXContainerItemProxy */;
};
9F7788D52BE652B2004E6BEF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilters = (
ios,
maccatalyst,
);
target = 9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */;
targetProxy = 9F7788D42BE652B2004E6BEF /* PBXContainerItemProxy */;
};
9FAD859129743F7400496AB1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilters = (
@ -940,7 +1173,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -975,7 +1208,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -991,6 +1224,78 @@
};
name = Release;
};
9F7788D92BE652B2004E6BEF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
9F7788DA2BE652B2004E6BEF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
9FAD859429743F7400496AB1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1011,7 +1316,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1045,7 +1350,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1193,6 +1498,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO;
CODE_SIGN_ENTITLEMENTS = IceCubesApp/App/IceCubesApp.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
@ -1209,6 +1515,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSCameraUsageDescription = "Upload photos & videos to attach to your Mastodon posts.";
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2024 Thomas Ricouard";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Upload photos & videos to Mastodon";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
@ -1225,7 +1532,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto;
@ -1247,6 +1554,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO;
CODE_SIGN_ENTITLEMENTS = "IceCubesApp/App/IceCubesApp-release.entitlements";
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
@ -1263,6 +1571,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSCameraUsageDescription = "Upload photos & videos to attach to your Mastodon posts.";
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2024 Thomas Ricouard";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Upload photos & videos to Mastodon";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
@ -1279,7 +1588,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto;
@ -1314,7 +1623,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1349,7 +1658,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.33;
MARKETING_VERSION = 1.10.41;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1376,6 +1685,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
9F7788D82BE652B2004E6BEF /* Build configuration list for PBXNativeTarget "IceCubesAppWidgetsExtensionExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9F7788D92BE652B2004E6BEF /* Debug */,
9F7788DA2BE652B2004E6BEF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
9FAD859329743F7400496AB1 /* Build configuration list for PBXNativeTarget "IceCubesShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -1415,14 +1733,6 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/RevenueCat/purchases-ios.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.0.0;
};
};
9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/evgenyneu/keychain-swift";
@ -1436,7 +1746,15 @@
repositoryURL = "https://github.com/Giphy/giphy-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.2.7;
minimumVersion = 2.2.8;
};
};
9FE6A42C2BD043A80055D388 /* XCRemoteSwiftPackageReference "purchases-ios" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/RevenueCat/purchases-ios";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.40.1;
};
};
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
@ -1454,16 +1772,6 @@
isa = XCSwiftPackageProductDependency;
productName = Timeline;
};
9F2A540929699705009B2D7C /* ReceiptParser */ = {
isa = XCSwiftPackageProductDependency;
package = 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */;
productName = ReceiptParser;
};
9F2A540B29699705009B2D7C /* RevenueCat */ = {
isa = XCSwiftPackageProductDependency;
package = 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */;
productName = RevenueCat;
};
9F2A5423296AB67A009B2D7C /* Env */ = {
isa = XCSwiftPackageProductDependency;
productName = Env;
@ -1501,6 +1809,30 @@
isa = XCSwiftPackageProductDependency;
productName = Conversations;
};
9F7788DD2BE6543D004E6BEF /* Account */ = {
isa = XCSwiftPackageProductDependency;
productName = Account;
};
9F7788DF2BE6543D004E6BEF /* AppAccount */ = {
isa = XCSwiftPackageProductDependency;
productName = AppAccount;
};
9F7788E12BE6543D004E6BEF /* Env */ = {
isa = XCSwiftPackageProductDependency;
productName = Env;
};
9F7788E32BE6543D004E6BEF /* Models */ = {
isa = XCSwiftPackageProductDependency;
productName = Models;
};
9F7788E52BE6543D004E6BEF /* Network */ = {
isa = XCSwiftPackageProductDependency;
productName = Network;
};
9F7788EF2BE78E77004E6BEF /* Timeline */ = {
isa = XCSwiftPackageProductDependency;
productName = Timeline;
};
9F7D93932980063100EE6B7A /* AppAccount */ = {
isa = XCSwiftPackageProductDependency;
productName = AppAccount;
@ -1569,6 +1901,11 @@
package = 9FE4CCA92B4C848A00DA5F13 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */;
productName = GiphyUISDK;
};
9FE6A42D2BD043A90055D388 /* RevenueCat */ = {
isa = XCSwiftPackageProductDependency;
package = 9FE6A42C2BD043A80055D388 /* XCRemoteSwiftPackageReference "purchases-ios" */;
productName = RevenueCat;
};
9FFF677B299B7B2C00FE700A /* Notifications */ = {
isa = XCSwiftPackageProductDependency;
productName = Notifications;

View file

@ -75,10 +75,10 @@
{
"identity" : "purchases-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/RevenueCat/purchases-ios.git",
"location" : "https://github.com/RevenueCat/purchases-ios",
"state" : {
"revision" : "b3597e0aa5e1193b04d9e1ff04eaf5cab3b8737d",
"version" : "4.34.0"
"revision" : "a9763ca482d52ea3d59aa2dfd2fc23427b02dada",
"version" : "4.40.1"
}
},
{

View file

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9F7788C42BE652B1004E6BEF"
BuildableName = "IceCubesAppWidgetsExtensionExtension.appex"
BlueprintName = "IceCubesAppWidgetsExtensionExtension"
ReferencedContainer = "container:IceCubesApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9FBFE638292A715500C250E9"
BuildableName = "Ice Cubes.app"
BlueprintName = "IceCubesApp"
ReferencedContainer = "container:IceCubesApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9FBFE638292A715500C250E9"
BuildableName = "Ice Cubes.app"
BlueprintName = "IceCubesApp"
ReferencedContainer = "container:IceCubesApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9FBFE638292A715500C250E9"
BuildableName = "Ice Cubes.app"
BlueprintName = "IceCubesApp"
ReferencedContainer = "container:IceCubesApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -8,10 +8,10 @@ import LinkPresentation
import Lists
import MediaUI
import Models
import Notifications
import StatusKit
import SwiftUI
import Timeline
import Notifications
@MainActor
extension View {
@ -67,9 +67,13 @@ extension View {
case .notificationsRequests:
NotificationsRequestsListView()
case let .notificationForAccount(accountId):
NotificationsListView(lockedType: nil ,
NotificationsListView(lockedType: nil,
lockedAccountId: accountId,
scrollToTopSignal: .constant(0))
case .blockedAccounts:
AccountsListView(mode: .blocked)
case .mutedAccounts:
AccountsListView(mode: .muted)
}
}
}
@ -81,7 +85,13 @@ extension View {
StatusEditor.MainView(mode: .replyTo(status: status))
.withEnvironments()
case let .newStatusEditor(visibility):
StatusEditor.MainView(mode: .new(visibility: visibility))
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
.withEnvironments()
case let .prefilledStatusEditor(text, visibility):
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
.withEnvironments()
case let .imageURL(urls, visibility):
StatusEditor.MainView(mode: .imageURL(urls: urls, visibility: visibility))
.withEnvironments()
case let .editStatusEditor(status):
StatusEditor.MainView(mode: .edit(status: status))

View file

@ -54,5 +54,11 @@ extension IceCubesApp {
}
.keyboardShortcut("l", modifiers: .shift)
}
CommandGroup(replacing: .help) {
Button("menu.help.github") {
let url = URL(string: "https://github.com/Dimillian/IceCubesApp/issues")!
UIApplication.shared.open(url)
}
}
}
}

View file

@ -1,3 +1,4 @@
import AppIntents
import Env
import MediaUI
import StatusKit
@ -22,6 +23,7 @@ extension IceCubesApp {
.environment(theme)
.environment(watcher)
.environment(pushNotificationsService)
.environment(appIntentService)
.environment(\.isSupporter, isSupporter)
.sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in
MediaUIView(selectedAttachment: selectedMediaAttachment,
@ -47,6 +49,12 @@ extension IceCubesApp {
}
}
}
.onChange(of: appIntentService.handledIntent) { _, _ in
if let intent = appIntentService.handledIntent?.intent {
handleIntent(intent)
appIntentService.handledIntent = nil
}
}
.withModelContainer()
}
#if targetEnvironment(macCatalyst)
@ -74,7 +82,9 @@ extension IceCubesApp {
Group {
switch destination.wrappedValue {
case let .newStatusEditor(visibility):
StatusEditor.MainView(mode: .new(visibility: visibility))
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
case let .prefilledStatusEditor(text, visibility):
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
case let .editStatusEditor(status):
StatusEditor.MainView(mode: .edit(status: status))
case let .quoteStatusEditor(status):
@ -90,6 +100,7 @@ extension IceCubesApp {
}
}
.withEnvironments()
.environment(RouterPath())
.withModelContainer()
.applyTheme(theme)
.frame(minWidth: 300, minHeight: 400)
@ -115,4 +126,23 @@ extension IceCubesApp {
.defaultSize(width: 1200, height: 1000)
.windowResizability(.contentMinSize)
}
private func handleIntent(_: any AppIntent) {
if let postIntent = appIntentService.handledIntent?.intent as? PostIntent {
#if os(visionOS) || os(macOS)
openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility))
#else
appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility)
#endif
} else if let tabIntent = appIntentService.handledIntent?.intent as? TabIntent {
selectedTab = tabIntent.tab.toAppTab
} else if let imageIntent = appIntentService.handledIntent?.intent as? PostImageIntent,
let urls = imageIntent.images?.compactMap({ $0.fileURL })
{
appRouterPath.presentedSheet = .imageURL(urls: urls,
visibility: userPreferences.postVisibility)
}
}
}

View file

@ -23,6 +23,7 @@ struct IceCubesApp: App {
@State var currentAccount = CurrentAccount.shared
@State var userPreferences = UserPreferences.shared
@State var pushNotificationsService = PushNotificationsService.shared
@State var appIntentService = AppIntentService.shared
@State var watcher = StreamWatcher.shared
@State var quickLook = QuickLook.shared
@State var theme = Theme.shared
@ -79,7 +80,7 @@ struct IceCubesApp: App {
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_: UIApplication,
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
{
@ -113,4 +114,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
return configuration
}
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
builder.remove(menu: .document)
builder.remove(menu: .toolbar)
builder.remove(menu: .sidebar)
}
}

View file

@ -13,6 +13,7 @@ extension View {
@MainActor
private struct SafariRouter: ViewModifier {
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
@Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var preferences
@Environment(RouterPath.self) private var routerPath
@ -25,13 +26,15 @@ private struct SafariRouter: ViewModifier {
content
.environment(\.openURL, OpenURLAction { url in
// Open internal URL.
routerPath.handle(url: url)
guard !isSecondaryColumn else { return .discarded }
return routerPath.handle(url: url)
})
.onOpenURL { url in
// Open external URL (from icecubesapp://)
guard !isSecondaryColumn else { return }
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
guard let url = URL(string: urlString), url.host != nil else { return }
_ = routerPath.handle(url: url)
_ = routerPath.handleDeepLink(url: url)
}
.onAppear {
routerPath.urlHandler = { url in

View file

@ -35,15 +35,23 @@ struct SideBarView<Content: View>: View {
}
private func makeIconForTab(tab: Tab) -> some View {
ZStack(alignment: .topTrailing) {
SideBarIcon(systemIconName: tab.iconName,
isSelected: tab == selectedTab)
let badge = badgeFor(tab: tab)
if badge > 0 {
makeBadgeView(count: badge)
HStack {
ZStack(alignment: .topTrailing) {
SideBarIcon(systemIconName: tab.iconName,
isSelected: tab == selectedTab)
let badge = badgeFor(tab: tab)
if badge > 0 {
makeBadgeView(count: badge)
}
}
if userPreferences.isSidebarExpanded {
Text(tab.title)
.font(.headline)
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(width: .sidebarWidth - 24, height: 50)
.frame(width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, height: 50)
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear,
in: RoundedRectangle(cornerRadius: 8))
}
@ -75,6 +83,7 @@ struct SideBarView<Content: View>: View {
.offset(x: 2, y: -2)
}
.buttonStyle(.borderedProminent)
.help(Tab.post.title)
}
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
@ -91,9 +100,19 @@ struct SideBarView<Content: View>: View {
}
} label: {
ZStack(alignment: .topTrailing) {
AppAccountView(viewModel: .init(appAccount: account, isCompact: true),
isParentPresented: .constant(false))
if showBadge,
if userPreferences.isSidebarExpanded {
AppAccountView(viewModel: .init(appAccount: account,
isCompact: false,
isInSettings: false),
isParentPresented: .constant(false))
} else {
AppAccountView(viewModel: .init(appAccount: account,
isCompact: true,
isInSettings: false),
isParentPresented: .constant(false))
}
if !userPreferences.isSidebarExpanded,
showBadge,
let token = account.oauthToken,
let notificationsCount = userPreferences.notificationsCount[token],
notificationsCount > 0
@ -101,13 +120,23 @@ struct SideBarView<Content: View>: View {
makeBadgeView(count: notificationsCount)
}
}
.padding(.leading, userPreferences.isSidebarExpanded ? 16 : 0)
}
.frame(width: .sidebarWidth, height: 50)
.help(accountButtonTitle(accountName: account.accountName))
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth, height: 50)
.padding(.vertical, 8)
.background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ?
theme.secondaryBackgroundColor : .clear)
}
private func accountButtonTitle(accountName: String?) -> LocalizedStringKey {
if let accountName {
"tab.profile-account-\(accountName)"
} else {
Tab.profile.title
}
}
private var tabsView: some View {
ForEach(tabs) { tab in
if tab != .profile && sidebarTabs.isEnabled(tab) {
@ -132,6 +161,7 @@ struct SideBarView<Content: View>: View {
} label: {
makeIconForTab(tab: tab)
}
.help(tab.title)
}
}
}
@ -155,15 +185,22 @@ struct SideBarView<Content: View>: View {
}
}
}
.frame(width: .sidebarWidth)
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
.scrollContentBackground(.hidden)
.background(.thinMaterial)
.safeAreaInset(edge: .bottom, content: {
HStack {
HStack(spacing: 16) {
postButton
.padding(.vertical, 24)
.padding(.leading, userPreferences.isSidebarExpanded ? 18 : 0)
if userPreferences.isSidebarExpanded {
Text("menu.new-post")
.font(.subheadline)
.foregroundColor(theme.labelColor)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(width: .sidebarWidth)
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
.background(.thinMaterial)
})
Divider().edgesIgnoringSafeArea(.all)
@ -196,6 +233,7 @@ private struct SideBarIcon: View {
self.isHovered = isHovered
}
}
.frame(width: 50, height: 40)
}
}

View file

@ -57,7 +57,7 @@ struct AboutView: View {
Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
}
} footer: {
Text("\(versionNumber)©2023 Thomas Ricouard")
Text("\(versionNumber)© 2024 Thomas Ricouard")
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)

View file

@ -29,7 +29,7 @@ struct AccountSettingsView: View {
Form {
Section {
Button {
routerPath.presentedSheet = .accountFiltersList
routerPath.presentedSheet = .accountEditInfo
} label: {
Label("account.action.edit-info", systemImage: "pencil")
.frame(maxWidth: .infinity, alignment: .leading)

View file

@ -31,7 +31,9 @@ struct SettingsTabs: View {
@Binding var popToRootTab: Tab
let isModal: Bool
@State private var startingPoint: SettingsStartingPoint? = nil
var body: some View {
NavigationStack(path: $routerPath.path) {
Form {
@ -64,6 +66,32 @@ struct SettingsTabs: View {
}
.withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
.onAppear {
startingPoint = RouterPath.settingsStartingPoint
RouterPath.settingsStartingPoint = nil
}
.navigationDestination(item: $startingPoint) { targetView in
switch targetView {
case .display:
DisplaySettingsView()
case .haptic:
HapticSettingsView()
case .remoteTimelines:
RemoteTimelinesSettingView()
case .tagGroups:
TagsGroupSettingView()
case .recentTags:
RecenTagsSettingView()
case .content:
ContentSettingsView()
case .swipeActions:
SwipeActionsSettingsView()
case .tabAndSidebarEntries:
EmptyView()
case .translation:
TranslationSettingsView()
}
}
}
.onAppear {
routerPath.client = client

View file

@ -11,16 +11,13 @@ struct TranslationSettingsView: View {
var body: some View {
Form {
deepLToggle
if preferences.alwaysUseDeepl {
translationSelector
if preferences.preferredTranslationType == .useDeepl {
Section("settings.translation.user-api-key") {
deepLPicker
SecureField("settings.translation.user-api-key", text: $apiKey)
.textContentType(.password)
}
.onAppear {
readValue()
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
@ -37,6 +34,7 @@ struct TranslationSettingsView: View {
#endif
}
}
backgroundAPIKey
autoDetectSection
}
.navigationTitle("settings.translation.navigation-title")
@ -48,19 +46,39 @@ struct TranslationSettingsView: View {
writeNewValue()
}
.onAppear(perform: updatePrefs)
.onAppear(perform: readValue)
}
@ViewBuilder
private var deepLToggle: some View {
private var translationSelector: some View {
@Bindable var preferences = preferences
Toggle(isOn: $preferences.alwaysUseDeepl) {
Text("settings.translation.always-deepl")
Picker("Translation Service", selection: $preferences.preferredTranslationType) {
ForEach(allTTCases, id: \.self) { type in
Text(type.description).tag(type)
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
var allTTCases: [TranslationType] {
TranslationType.allCases.filter { type in
if type != .useApple {
return true
}
#if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *) {
return true
} else {
return false
}
#else
return false
#endif
}
}
@ViewBuilder
private var deepLPicker: some View {
@Bindable var preferences = preferences
@ -80,6 +98,34 @@ struct TranslationSettingsView: View {
} footer: {
Text("settings.translation.auto-detect-post-language-footer")
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
@ViewBuilder
private var backgroundAPIKey: some View {
if preferences.preferredTranslationType != .useDeepl,
!apiKey.isEmpty
{
Section {
Text("The DeepL API Key is still stored!")
if preferences.preferredTranslationType == .useServerIfPossible {
Text("It can however still be used as a fallback for your instance's translation service.")
}
Button(role: .destructive) {
withAnimation {
writeNewValue(value: "")
readValue()
}
} label: {
Text("action.delete")
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
}
private func writeNewValue() {
@ -91,11 +137,7 @@ struct TranslationSettingsView: View {
}
private func readValue() {
if let apiKey = DeepLUserAPIHandler.readIfAllowed() {
self.apiKey = apiKey
} else {
apiKey = ""
}
apiKey = DeepLUserAPIHandler.readKey()
}
private func updatePrefs() {

View file

@ -1,4 +1,5 @@
import Account
import AppIntents
import DesignSystem
import Explore
import Foundation
@ -79,41 +80,47 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
@ViewBuilder
var label: some View {
if self != .other {
Label(title, systemImage: iconName)
}
}
var title: LocalizedStringKey {
switch self {
case .timeline:
Label("tab.timeline", systemImage: iconName)
"tab.timeline"
case .trending:
Label("tab.trending", systemImage: iconName)
"tab.trending"
case .local:
Label("tab.local", systemImage: iconName)
"tab.local"
case .federated:
Label("tab.federated", systemImage: iconName)
"tab.federated"
case .notifications:
Label("tab.notifications", systemImage: iconName)
"tab.notifications"
case .mentions:
Label("tab.mentions", systemImage: iconName)
"tab.mentions"
case .explore:
Label("tab.explore", systemImage: iconName)
"tab.explore"
case .messages:
Label("tab.messages", systemImage: iconName)
"tab.messages"
case .settings:
Label("tab.settings", systemImage: iconName)
"tab.settings"
case .profile:
Label("tab.profile", systemImage: iconName)
"tab.profile"
case .bookmarks:
Label("accessibility.tabs.profile.picker.bookmarks", systemImage: iconName)
"accessibility.tabs.profile.picker.bookmarks"
case .favorites:
Label("accessibility.tabs.profile.picker.favorites", systemImage: iconName)
"accessibility.tabs.profile.picker.favorites"
case .post:
Label("menu.new-post", systemImage: iconName)
"menu.new-post"
case .followedTags:
Label("timeline.filter.tags", systemImage: iconName)
"timeline.filter.tags"
case .lists:
Label("timeline.filter.lists", systemImage: iconName)
"timeline.filter.lists"
case .links:
Label("explore.section.trending.links", systemImage: iconName)
"explore.section.trending.links"
case .other:
EmptyView()
""
}
}

View file

@ -14,6 +14,21 @@ struct ToolbarTab: ToolbarContent {
var body: some ToolbarContent {
if !isSecondaryColumn {
ToolbarItem(placement: .topBarLeading) {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
Button {
withAnimation {
userPreferences.isSidebarExpanded.toggle()
}
} label: {
if userPreferences.isSidebarExpanded {
Image(systemName: "sidebar.squares.left")
} else {
Image(systemName: "sidebar.left")
}
}
}
}
statusEditorToolbarItem(routerPath: routerPath,
visibility: userPreferences.postVisibility)
if UIDevice.current.userInterfaceIdiom != .pad ||

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,43 @@
import AppAccount
import AppIntents
import Env
import Foundation
import Models
import Network
extension IntentDescription: @unchecked Sendable {}
extension TypeDisplayRepresentation: @unchecked Sendable {}
public struct AppAccountEntity: Identifiable, AppEntity {
public var id: String { account.id }
public let account: AppAccount
public static let defaultQuery = DefaultAppAccountEntityQuery()
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "AppAccount"
public var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(account.accountName ?? account.server)")
}
}
public struct DefaultAppAccountEntityQuery: EntityQuery {
public init() {}
public func entities(for identifiers: [AppAccountEntity.ID]) async throws -> [AppAccountEntity] {
return await AppAccountsManager.shared.availableAccounts.filter { account in
identifiers.contains { id in
id == account.id
}
}.map { AppAccountEntity(account: $0) }
}
public func suggestedEntities() async throws -> [AppAccountEntity] {
await AppAccountsManager.shared.availableAccounts.map { .init(account: $0) }
}
public func defaultResult() async -> AppAccountEntity? {
await .init(account: AppAccountsManager.shared.currentAccount)
}
}

View file

@ -0,0 +1,25 @@
import AppIntents
import SwiftUI
@Observable
public class AppIntentService: @unchecked Sendable {
struct HandledIntent: Equatable {
static func == (lhs: AppIntentService.HandledIntent, rhs: AppIntentService.HandledIntent) -> Bool {
lhs.id == rhs.id
}
let id: String
let intent: any AppIntent
init(intent: any AppIntent) {
id = UUID().uuidString
self.intent = intent
}
}
public static let shared = AppIntentService()
var handledIntent: HandledIntent?
private init() {}
}

View file

@ -0,0 +1,42 @@
import AppIntents
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: PostIntent(),
phrases: [
"Post \(\.$content) in \(.applicationName)",
"Post a status on Mastodon with \(.applicationName)",
],
shortTitle: "Compose a post",
systemImageName: "square.and.pencil"
)
AppShortcut(
intent: InlinePostIntent(),
phrases: [
"Write a post with \(.applicationName)",
"Send on post on Mastodon with \(.applicationName)",
],
shortTitle: "Send a post",
systemImageName: "square.and.pencil"
)
AppShortcut(
intent: TabIntent(),
phrases: [
"Open \(\.$tab) in \(.applicationName)",
"Open \(.applicationName)",
],
shortTitle: "Open Ice Cubes",
systemImageName: "cube"
)
AppShortcut(
intent: PostImageIntent(),
phrases: [
"Post images \(\.$images) in \(.applicationName)",
"Send photos \(\.$images) with \(.applicationName)",
],
shortTitle: "Post a status with an image",
systemImageName: "photo"
)
}
}

View file

@ -0,0 +1,61 @@
import AppAccount
import AppIntents
import Env
import Foundation
import Models
import Network
enum PostVisibility: String, AppEnum {
case direct, priv, unlisted, pub
public static var caseDisplayRepresentations: [PostVisibility: DisplayRepresentation] {
[.direct: "Private",
.priv: "Followers Only",
.unlisted: "Quiet Public",
.pub: "Public"]
}
static var typeDisplayName: LocalizedStringResource { "Visibility" }
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Visibility"
var toAppVisibility: Models.Visibility {
switch self {
case .direct:
.direct
case .priv:
.priv
case .unlisted:
.unlisted
case .pub:
.pub
}
}
}
struct InlinePostIntent: AppIntent {
static let title: LocalizedStringResource = "Send post to Mastodon"
static let description: IntentDescription = "Send a text post to Mastodon with Ice Cubes"
static let openAppWhenRun: Bool = false
@Parameter(title: "Account", requestValueDialog: IntentDialog("Account"))
var account: AppAccountEntity
@Parameter(title: "Post visibility", requestValueDialog: IntentDialog("Visibility of your post"))
var visibility: PostVisibility
@Parameter(title: "Post content", requestValueDialog: IntentDialog("Content of the post to be sent to Mastodon"))
var content: String
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
let client = Client(server: account.account.server, version: .v1, oauthToken: account.account.oauthToken)
let status = StatusData(status: content, visibility: visibility.toAppVisibility)
do {
let status: Status = try await client.post(endpoint: Statuses.postStatus(json: status))
return .result(dialog: "\(status.content.asRawText) was posted on Mastodon")
} catch {
return .result(dialog: "An error occured while posting to Mastodon, please try again.")
}
}
}

View file

@ -0,0 +1,55 @@
import AppAccount
import AppIntents
import Env
import Foundation
import Models
import Network
import Timeline
public struct ListEntity: Identifiable, AppEntity {
public var id: String { list.id }
public let list: Models.List
public static let defaultQuery = DefaultListEntityQuery()
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "List"
public var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(list.title)")
}
}
public struct DefaultListEntityQuery: EntityQuery {
public init() {}
@IntentParameterDependency<ListsWidgetConfiguration>(
\.$account
)
var account
public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] {
await fetchLists().map{ .init(list: $0 )}
}
public func suggestedEntities() async throws -> [ListEntity] {
await fetchLists().map{ .init(list: $0 )}
}
public func defaultResult() async -> ListEntity? {
nil
}
private func fetchLists() async -> [Models.List] {
guard let account = account?.account.account else {
return []
}
let client = Client(server: account.server, oauthToken: account.oauthToken)
do {
let lists: [Models.List] = try await client.get(endpoint: Lists.lists)
return lists
} catch {
return []
}
}
}

View file

@ -0,0 +1,19 @@
import AppIntents
import Foundation
struct PostImageIntent: AppIntent {
static let title: LocalizedStringResource = "Post an image to Mastodon"
static let description: IntentDescription = "Use Ice Cubes to compose a post with an image to Mastodon"
static let openAppWhenRun: Bool = true
@Parameter(title: "Image",
description: "Image to post on Mastodon",
supportedTypeIdentifiers: ["public.image"],
inputConnectionBehavior: .connectToPreviousIntentResult)
var images: [IntentFile]?
func perform() async throws -> some IntentResult {
AppIntentService.shared.handledIntent = .init(intent: self)
return .result()
}
}

View file

@ -0,0 +1,16 @@
import AppIntents
import Foundation
struct PostIntent: AppIntent {
static let title: LocalizedStringResource = "Compose a post to Mastodon"
static let description: IntentDescription = "Use Ice Cubes to compose a post for Mastodon"
static let openAppWhenRun: Bool = true
@Parameter(title: "Post content", inputConnectionBehavior: .connectToPreviousIntentResult)
var content: String?
func perform() async throws -> some IntentResult {
AppIntentService.shared.handledIntent = .init(intent: self)
return .result()
}
}

View file

@ -0,0 +1,89 @@
import AppIntents
import Foundation
enum TabEnum: String, AppEnum, Sendable {
case timeline, notifications, mentions, explore, messages, settings
case trending, federated, local
case profile
case bookmarks
case favorites
case post
case followedTags
case lists
case links
static var typeDisplayName: LocalizedStringResource { "Tab" }
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Tab"
nonisolated static var caseDisplayRepresentations: [TabEnum: DisplayRepresentation] {
[.timeline: .init(title: "Home Timeline"),
.trending: .init(title: "Trending Timeline"),
.federated: .init(title: "Federated Timeline"),
.local: .init(title: "Local Timeline"),
.notifications: .init(title: "Notifications"),
.mentions: .init(title: "Mentions"),
.explore: .init(title: "Explore & Trending"),
.messages: .init(title: "Private Messages"),
.settings: .init(title: "Settings"),
.profile: .init(title: "Profile"),
.bookmarks: .init(title: "Bookmarks"),
.favorites: .init(title: "Favorites"),
.followedTags: .init(title: "Followed Tags"),
.lists: .init(title: "Lists"),
.links: .init(title: "Trending Links"),
.post: .init(title: "New post")]
}
var toAppTab: Tab {
switch self {
case .timeline:
.timeline
case .notifications:
.notifications
case .mentions:
.mentions
case .explore:
.explore
case .messages:
.messages
case .settings:
.settings
case .trending:
.trending
case .federated:
.federated
case .local:
.local
case .profile:
.profile
case .bookmarks:
.bookmarks
case .favorites:
.favorites
case .post:
.post
case .followedTags:
.followedTags
case .lists:
.lists
case .links:
.links
}
}
}
struct TabIntent: AppIntent {
static let title: LocalizedStringResource = "Open on a tab"
static let description: IntentDescription = "Open the app on a specific tab"
static let openAppWhenRun: Bool = true
@Parameter(title: "Selected tab")
var tab: TabEnum
@MainActor
func perform() async throws -> some IntentResult {
AppIntentService.shared.handledIntent = .init(intent: self)
return .result()
}
}

View file

@ -0,0 +1,37 @@
import AppAccount
import AppIntents
import Env
import Foundation
import Models
import Network
import Timeline
public struct TimelineFilterEntity: Identifiable, AppEntity {
public var id: String { timeline.id }
public let timeline: TimelineFilter
public static let defaultQuery = DefaultTimelineEntityQuery()
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "TimelineFilter"
public var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(timeline.title)")
}
}
public struct DefaultTimelineEntityQuery: EntityQuery {
public init() {}
public func entities(for _: [TimelineFilter.ID]) async throws -> [TimelineFilterEntity] {
[.home, .trending, .federated, .local].map { .init(timeline: $0) }
}
public func suggestedEntities() async throws -> [TimelineFilterEntity] {
[.home, .trending, .federated, .local].map { .init(timeline: $0) }
}
public func defaultResult() async -> TimelineFilterEntity? {
.init(timeline: .home)
}
}

View file

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0.859",
"green" : "0.267",
"red" : "0.675"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0.949",
"green" : "0.945",
"red" : "0.941"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0.133",
"green" : "0.082",
"red" : "0.067"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,75 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct HashtagPostsWidgetProvider: AppIntentTimelineProvider {
func placeholder(in _: Context) -> PostsWidgetEntry {
.init(date: Date(),
title: "#Mastodon",
statuses: [.placeholder()],
images: [:])
}
func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
if let entry = await timeline(for: configuration, context: context).entries.first {
return entry
}
return .init(date: Date(),
title: "#Mastodon",
statuses: [],
images: [:])
}
func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
await timeline(for: configuration, context: context)
}
private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline<PostsWidgetEntry> {
do {
let timeline: TimelineFilter = .hashtag(tag: configuration.hashgtag, accountId: nil)
let statuses = await loadStatuses(for: timeline,
account: configuration.account,
widgetFamily: context.family)
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
return Timeline(entries: [.init(date: Date(),
title: timeline.title,
statuses: statuses,
images: images)], policy: .atEnd)
} catch {
return Timeline(entries: [.init(date: Date(),
title: "#Mastodon",
statuses: [],
images: [:])],
policy: .atEnd)
}
}
}
struct HashtagPostsWidget: Widget {
let kind: String = "HashtagPostsWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind,
intent: HashtagPostsWidgetConfiguration.self,
provider: HashtagPostsWidgetProvider())
{ entry in
PostsWidgetView(entry: entry)
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
}
.configurationDisplayName("Hashtag timeline")
.description("Show the latest post for the selected hashtag")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}
#Preview(as: .systemMedium) {
HashtagPostsWidget()
} timeline: {
PostsWidgetEntry(date: .now,
title: "#Mastodon",
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
images: [:])
}

View file

@ -0,0 +1,22 @@
import AppIntents
import WidgetKit
struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Configuration"
static let description = IntentDescription("Choose the account and hashtag for this widget")
@Parameter(title: "Account")
var account: AppAccountEntity
@Parameter(title: "Hashtag")
var hashgtag: String
}
extension HashtagPostsWidgetConfiguration {
static var previewAccount: HashtagPostsWidgetConfiguration {
let intent = HashtagPostsWidgetConfiguration()
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
intent.hashgtag = "Mastodon"
return intent
}
}

View file

@ -0,0 +1,12 @@
import SwiftUI
import WidgetKit
@main
struct IceCubesAppWidgetsExtensionBundle: WidgetBundle {
var body: some Widget {
LatestPostsWidget()
HashtagPostsWidget()
ListsPostWidget()
MentionsWidget()
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(BUNDLE_ID_PREFIX).IceCubesApp</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(BUNDLE_ID_PREFIX).IceCubesApp</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View file

@ -2,7 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>DEEPL_SECRET</key>
<string>NICE_TRY_AGAIN</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,93 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct LatestPostsWidgetProvider: AppIntentTimelineProvider {
func placeholder(in _: Context) -> PostsWidgetEntry {
.init(date: Date(),
title: "Home",
statuses: [.placeholder()],
images: [:])
}
func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
if let entry = await timeline(for: configuration, context: context).entries.first {
return entry
}
return .init(date: Date(),
title: configuration.timeline.timeline.title,
statuses: [],
images: [:])
}
func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
await timeline(for: configuration, context: context)
}
private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline<PostsWidgetEntry> {
do {
let statuses = await loadStatuses(for: configuration.timeline.timeline,
account: configuration.account,
widgetFamily: context.family)
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
return Timeline(entries: [.init(date: Date(),
title: configuration.timeline.timeline.title,
statuses: statuses,
images: images)], policy: .atEnd)
} catch {
return Timeline(entries: [.init(date: Date(),
title: configuration.timeline.timeline.title,
statuses: [],
images: [:])],
policy: .atEnd)
}
}
private func loadImages(urls: [URL]) async throws -> [URL: UIImage] {
try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in
for url in urls {
group.addTask {
let response = try await URLSession.shared.data(from: url)
return (url, UIImage(data: response.0))
}
}
var images: [URL: UIImage] = [:]
for try await (url, image) in group {
images[url] = image
}
return images
}
}
}
struct LatestPostsWidget: Widget {
let kind: String = "LatestPostsWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind,
intent: LatestPostsWidgetConfiguration.self,
provider: LatestPostsWidgetProvider())
{ entry in
PostsWidgetView(entry: entry)
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
}
.configurationDisplayName("Latest posts")
.description("Show the latest post for the selected timeline")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}
#Preview(as: .systemMedium) {
LatestPostsWidget()
} timeline: {
PostsWidgetEntry(date: .now,
title: "Mastodon",
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
images: [:])
}

View file

@ -0,0 +1,22 @@
import AppIntents
import WidgetKit
struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Configuration"
static let description = IntentDescription("Choose the account and timeline for this widget")
@Parameter(title: "Account")
var account: AppAccountEntity
@Parameter(title: "Timeline")
var timeline: TimelineFilterEntity
}
extension LatestPostsWidgetConfiguration {
static var previewAccount: LatestPostsWidgetConfiguration {
let intent = LatestPostsWidgetConfiguration()
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
intent.timeline = .init(timeline: .home)
return intent
}
}

View file

@ -0,0 +1,75 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct ListsWidgetProvider: AppIntentTimelineProvider {
func placeholder(in _: Context) -> PostsWidgetEntry {
.init(date: Date(),
title: "List name",
statuses: [.placeholder()],
images: [:])
}
func snapshot(for configuration: ListsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
if let entry = await timeline(for: configuration, context: context).entries.first {
return entry
}
return .init(date: Date(),
title: "List name",
statuses: [],
images: [:])
}
func timeline(for configuration: ListsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
await timeline(for: configuration, context: context)
}
private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async -> Timeline<PostsWidgetEntry> {
do {
let timeline: TimelineFilter = .list(list: configuration.timeline.list)
let statuses = await loadStatuses(for: timeline,
account: configuration.account,
widgetFamily: context.family)
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
return Timeline(entries: [.init(date: Date(),
title: timeline.title,
statuses: statuses,
images: images)], policy: .atEnd)
} catch {
return Timeline(entries: [.init(date: Date(),
title: "List name",
statuses: [],
images: [:])],
policy: .atEnd)
}
}
}
struct ListsPostWidget: Widget {
let kind: String = "ListsPostWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind,
intent: ListsWidgetConfiguration.self,
provider: ListsWidgetProvider())
{ entry in
PostsWidgetView(entry: entry)
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
}
.configurationDisplayName("List timeline")
.description("Show the latest post for the selected list")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}
#Preview(as: .systemMedium) {
ListsPostWidget()
} timeline: {
PostsWidgetEntry(date: .now,
title: "List name",
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
images: [:])
}

View file

@ -0,0 +1,21 @@
import AppIntents
import WidgetKit
struct ListsWidgetConfiguration: WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Configuration"
static let description = IntentDescription("Choose the account and list for this widget")
@Parameter(title: "Account")
var account: AppAccountEntity
@Parameter(title: "List")
var timeline: ListEntity
}
extension ListsWidgetConfiguration {
static var previewAccount: LatestPostsWidgetConfiguration {
let intent = LatestPostsWidgetConfiguration()
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
return intent
}
}

View file

@ -0,0 +1,81 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct MentionsWidgetProvider: AppIntentTimelineProvider {
func placeholder(in _: Context) -> PostsWidgetEntry {
.init(date: Date(),
title: "Mentions",
statuses: [.placeholder()],
images: [:])
}
func snapshot(for configuration: MentionsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
if let entry = await timeline(for: configuration, context: context).entries.first {
return entry
}
return .init(date: Date(),
title: "Mentions",
statuses: [],
images: [:])
}
func timeline(for configuration: MentionsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
await timeline(for: configuration, context: context)
}
private func timeline(for configuration: MentionsWidgetConfiguration, context _: Context) async -> Timeline<PostsWidgetEntry> {
do {
let client = Client(server: configuration.account.account.server,
oauthToken: configuration.account.account.oauthToken)
var excludedTypes = Models.Notification.NotificationType.allCases
excludedTypes.removeAll(where: { $0 == .mention })
let notifications: [Models.Notification] =
try await client.get(endpoint: Notifications.notifications(minId: nil,
maxId: nil,
types: excludedTypes.map(\.rawValue),
limit: 5))
let statuses = notifications.compactMap { $0.status }
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
return Timeline(entries: [.init(date: Date(),
title: "Mentions",
statuses: statuses,
images: images)], policy: .atEnd)
} catch {
return Timeline(entries: [.init(date: Date(),
title: "Mentions",
statuses: [],
images: [:])],
policy: .atEnd)
}
}
}
struct MentionsWidget: Widget {
let kind: String = "MentionsWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind,
intent: MentionsWidgetConfiguration.self,
provider: MentionsWidgetProvider())
{ entry in
PostsWidgetView(entry: entry)
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
}
.configurationDisplayName("Mentions")
.description("Show the latest mentions for the selected account.")
.supportedFamilies([.systemLarge, .systemExtraLarge])
}
}
#Preview(as: .systemMedium) {
MentionsWidget()
} timeline: {
PostsWidgetEntry(date: .now,
title: "Mentions",
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
images: [:])
}

View file

@ -0,0 +1,18 @@
import AppIntents
import WidgetKit
struct MentionsWidgetConfiguration: WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Configuration"
static let description = IntentDescription("Choose the account for this widget")
@Parameter(title: "Account")
var account: AppAccountEntity
}
extension MentionsWidgetConfiguration {
static var previewAccount: MentionsWidgetConfiguration {
let intent = MentionsWidgetConfiguration()
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
return intent
}
}

View file

@ -0,0 +1,96 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct PostsWidgetEntry: TimelineEntry {
let date: Date
let title: String
let statuses: [Status]
let images: [URL: UIImage]
}
struct PostsWidgetView: View {
var entry: LatestPostsWidgetProvider.Entry
@Environment(\.widgetFamily) var family
@Environment(\.redactionReasons) var redacted
var contentLineLimit: Int {
switch family {
case .systemSmall, .systemMedium:
return 5
default:
return 2
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
headerView
ForEach(entry.statuses) { status in
makeStatusView(status)
}
Spacer()
}
.frame(maxWidth: .infinity)
}
private var headerView: some View {
HStack {
Text(entry.title)
Spacer()
Image(systemName: "cube")
}
.font(.subheadline)
.fontWeight(.bold)
.foregroundStyle(Color("AccentColor"))
}
@ViewBuilder
private func makeStatusView(_ status: Status) -> some View {
if let url = URL(string: status.url ?? "") {
Link(destination: url, label: {
VStack(alignment: .leading, spacing: 2) {
makeStatusHeaderView(status)
Text(status.content.asSafeMarkdownAttributedString)
.font(.footnote)
.lineLimit(contentLineLimit)
.fixedSize(horizontal: false, vertical: true)
.padding(.leading, 20)
}
})
}
}
private func makeStatusHeaderView(_ status: Status) -> some View {
HStack(alignment: .center, spacing: 4) {
if let image = entry.images[status.account.avatar] {
Image(uiImage: image)
.resizable()
.frame(width: 16, height: 16)
.clipShape(Circle())
} else {
Circle()
.foregroundStyle(.secondary)
.frame(width: 16, height: 16)
}
HStack(spacing: 0) {
Text(status.account.safeDisplayName)
.foregroundStyle(.primary)
if family != .systemSmall {
Text(" @")
.foregroundStyle(.tertiary)
Text(status.account.username)
.foregroundStyle(.tertiary)
}
Spacer()
}
.font(.footnote)
.fontWeight(.semibold)
.lineLimit(1)
}
}
}

View file

@ -0,0 +1,56 @@
import AppAccount
import Foundation
import Models
import Network
import StatusKit
import Timeline
import UIKit
import WidgetKit
func loadStatuses(for timeline: TimelineFilter,
account: AppAccountEntity,
widgetFamily: WidgetFamily) async -> [Status]
{
let client = Client(server: account.account.server, oauthToken: account.account.oauthToken)
do {
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
maxId: nil,
minId: nil,
offset: nil))
statuses = statuses.filter { $0.reblog == nil && !$0.content.asRawText.isEmpty }
switch widgetFamily {
case .systemSmall, .systemMedium:
if statuses.count >= 1 {
statuses = statuses.prefix(upTo: 1).map { $0 }
}
case .systemLarge, .systemExtraLarge:
if statuses.count >= 5 {
statuses = statuses.prefix(upTo: 5).map { $0 }
}
default:
break
}
return statuses
} catch {
return []
}
}
func loadImages(urls: [URL]) async throws -> [URL: UIImage] {
try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in
for url in urls {
group.addTask {
let response = try await URLSession.shared.data(from: url)
return (url, UIImage(data: response.0))
}
}
var images: [URL: UIImage] = [:]
for try await (url, image) in group {
images[url] = image
}
return images
}
}

View file

@ -9,6 +9,7 @@ public struct AccountDetailContextMenu: View {
@Environment(UserPreferences.self) private var preferences
@Binding var showBlockConfirmation: Bool
@Binding var showTranslateView: Bool
var viewModel: AccountDetailViewModel
@ -136,15 +137,15 @@ public struct AccountDetailContextMenu: View {
#endif
}
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier {
Button {
Task {
await viewModel.translate(userLang: lang)
#if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *) {
Button {
showTranslateView = true
} label: {
Label("status.action.translate", systemImage: "captions.bubble")
}
}
} label: {
Label("status.action.translate", systemImage: "captions.bubble")
}
}
#endif
if viewModel.relationship?.following == true {
Button {

View file

@ -24,6 +24,7 @@ public struct AccountDetailView: View {
@State private var isCurrentUser: Bool = false
@State private var showBlockConfirmation: Bool = false
@State private var isEditingRelationshipNote: Bool = false
@State private var showTranslateView: Bool = false
@State private var displayTitle: Bool = false
@ -285,7 +286,9 @@ public struct AccountDetailView: View {
}
Menu {
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: viewModel)
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
showTranslateView: $showTranslateView,
viewModel: viewModel)
if !viewModel.isCurrentUser {
Button {
@ -327,6 +330,20 @@ public struct AccountDetailView: View {
if let account = viewModel.account {
Divider()
Button {
routerPath.navigate(to: .blockedAccounts)
} label: {
Label("account.blocked", systemImage: "person.crop.circle.badge.xmark")
}
Button {
routerPath.navigate(to: .mutedAccounts)
} label: {
Label("account.muted", systemImage: "person.crop.circle.badge.moon")
}
Divider()
Button {
if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") {
openURL(url)
@ -366,6 +383,9 @@ public struct AccountDetailView: View {
} message: {
Text("account.action.block-user-confirmation")
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account?.note.asRawText ?? "")
#endif
}
}
}

View file

@ -273,22 +273,4 @@ import SwiftUI
func statusDidAppear(status _: Models.Status) {}
func statusDidDisappear(status _: Status) {}
func translate(userLang: String) async {
guard let account else { return }
withAnimation {
isLoadingTranslation = true
}
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
let translation = try? await deeplClient.request(target: userLang, text: account.note.asRawText)
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
}
}

View file

@ -32,6 +32,7 @@ public struct AccountsListRow: View {
@State private var isEditingRelationshipNote: Bool = false
@State private var showBlockConfirmation: Bool = false
@State private var showTranslateView: Bool = false
let isFollowRequest: Bool
let requestUpdated: (() -> Void)?
@ -108,8 +109,13 @@ public struct AccountsListRow: View {
.onTapGesture {
routerPath.navigate(to: .accountDetailWithAccount(account: viewModel.account))
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText)
#endif
.contextMenu {
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: .init(account: viewModel.account))
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
showTranslateView: $showTranslateView,
viewModel: .init(account: viewModel.account))
} preview: {
List {
AccountDetailHeaderView(viewModel: .init(account: viewModel.account),

View file

@ -52,6 +52,9 @@ public struct AccountsListView: View {
searchableList
} else {
standardList
.refreshable {
await viewModel.fetch()
}
}
}
@ -121,16 +124,23 @@ public struct AccountsListView: View {
}
}
Section {
ForEach(accounts) { account in
if let relationship = relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account,
relationShip: relationship))
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
if accounts.isEmpty {
PlaceholderView(iconName: "person.icloud",
title: "No accounts found",
message: "This list of accounts is empty")
.listRowSeparator(.hidden)
} else {
ForEach(accounts) { account in
if let relationship = relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account,
relationShip: relationship))
}
}
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
switch nextPageState {
case .hasNextPage:

View file

@ -8,6 +8,7 @@ public enum AccountsListMode {
case following(accountId: String), followers(accountId: String)
case favoritedBy(statusId: String), rebloggedBy(statusId: String)
case accountsList(accounts: [Account])
case blocked, muted
var title: LocalizedStringKey {
switch self {
@ -21,6 +22,10 @@ public enum AccountsListMode {
"account.boosted-by"
case .accountsList:
""
case .blocked:
"account.blocked"
case .muted:
"account.muted"
}
}
}
@ -84,6 +89,12 @@ public enum AccountsListMode {
case let .accountsList(accounts):
self.accounts = accounts
link = nil
case .blocked:
(accounts, link) = try await client.getWithLink(endpoint: Accounts.blockList)
case .muted:
(accounts, link) = try await client.getWithLink(endpoint: Accounts.muteList)
}
nextPageId = link?.maxId
relationships = try await client.get(endpoint:
@ -114,7 +125,14 @@ public enum AccountsListMode {
case .accountsList:
newAccounts = []
link = nil
case .blocked:
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.blockList)
case .muted:
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.muteList)
}
accounts.append(contentsOf: newAccounts)
let newRelationships: [Relationship] =
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map(\.id)))

View file

@ -9,5 +9,6 @@ public extension CGFloat {
static let statusComponentSpacing: CGFloat = 6
static let secondaryColumnWidth: CGFloat = 400
static let sidebarWidth: CGFloat = 90
static let sidebarWidthExpanded: CGFloat = 220
static let pollBarHeight: CGFloat = 30
}

View file

@ -10,7 +10,7 @@ public struct CloseToolbarItem: ToolbarContent {
Button(action: {
dismiss()
}, label: {
Image(systemName: "xmark.circle")
Image(systemName: "xmark.circle")
})
.keyboardShortcut(.cancelAction)
}

View file

@ -4,9 +4,9 @@ public struct ErrorView: View {
public let title: LocalizedStringKey
public let message: LocalizedStringKey
public let buttonTitle: LocalizedStringKey
public let onButtonPress: (() async -> Void)
public let onButtonPress: () async -> Void
public init(title: LocalizedStringKey, message: LocalizedStringKey, buttonTitle: LocalizedStringKey, onButtonPress: @escaping (() async -> Void) ) {
public init(title: LocalizedStringKey, message: LocalizedStringKey, buttonTitle: LocalizedStringKey, onButtonPress: @escaping (() async -> Void)) {
self.title = title
self.message = message
self.buttonTitle = buttonTitle

View file

@ -34,7 +34,7 @@ import Observation
public var isEditAltTextSupported: Bool {
version >= 4.1
}
public var isNotificationsFilterSupported: Bool {
version >= 4.3
}

View file

@ -23,22 +23,30 @@ public enum DeepLUserAPIHandler {
}
}
public static func readIfAllowed() -> String? {
guard UserPreferences.shared.alwaysUseDeepl else { return nil }
public static func readKeyIfAllowed() -> String? {
guard UserPreferences.shared.preferredTranslationType == .useDeepl else { return nil }
return readValue()
return readKeyInternal()
}
private static func readValue() -> String? {
public static func readKey() -> String {
return readKeyInternal() ?? ""
}
private static func readKeyInternal() -> String? {
keychain.synchronizable = true
return keychain.get(key)
}
public static func deactivateToggleIfNoKey() {
UserPreferences.shared.alwaysUseDeepl = shouldAlwaysUseDeepl
if UserPreferences.shared.preferredTranslationType == .useDeepl {
if readKeyInternal() == nil {
UserPreferences.shared.preferredTranslationType = .useServerIfPossible
}
}
}
public static var shouldAlwaysUseDeepl: Bool {
readIfAllowed() != nil
readKeyIfAllowed() != nil
}
}

View file

@ -0,0 +1,15 @@
import SwiftUI
#if canImport(_Translation_SwiftUI)
import Translation
extension View {
public func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View {
if #available(iOS 17.4, *) {
return self.translationPresentation(isPresented: isPresented, text: text)
} else {
return self
}
}
}
#endif

View file

@ -25,10 +25,13 @@ public enum RouterDestination: Hashable {
case tagsList(tags: [Tag])
case notificationsRequests
case notificationForAccount(accountId: String)
case blockedAccounts
case mutedAccounts
}
public enum WindowDestinationEditor: Hashable, Codable {
case newStatusEditor(visibility: Models.Visibility)
case prefilledStatusEditor(text: String, visibility: Models.Visibility)
case editStatusEditor(status: Status)
case replyToStatusEditor(status: Status)
case quoteStatusEditor(status: Status)
@ -50,6 +53,8 @@ public enum SheetDestination: Identifiable, Hashable {
}
case newStatusEditor(visibility: Models.Visibility)
case prefilledStatusEditor(text: String, visibility: Models.Visibility)
case imageURL(urls: [URL], visibility: Models.Visibility)
case editStatusEditor(status: Status)
case replyToStatusEditor(status: Status)
case quoteStatusEditor(status: Status)
@ -76,7 +81,7 @@ public enum SheetDestination: Identifiable, Hashable {
public var id: String {
switch self {
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
.mentionStatusEditor, .quoteLinkStatusEditor:
.mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor, .imageURL:
"statusEditor"
case .listCreate:
"listCreate"
@ -110,6 +115,18 @@ public enum SheetDestination: Identifiable, Hashable {
}
}
public enum SettingsStartingPoint {
case display
case haptic
case remoteTimelines
case tagGroups
case recentTags
case content
case swipeActions
case tabAndSidebarEntries
case translation
}
@MainActor
@Observable public class RouterPath {
public var client: Client?
@ -118,6 +135,8 @@ public enum SheetDestination: Identifiable, Hashable {
public var path: [RouterDestination] = []
public var presentedSheet: SheetDestination?
public static var settingsStartingPoint: SettingsStartingPoint? = nil
public init() {}
public func navigate(to: RouterDestination) {
@ -186,6 +205,57 @@ public enum SheetDestination: Identifiable, Hashable {
return urlHandler?(url) ?? .systemAction
}
public func handleDeepLink(url: URL) -> OpenURLAction.Result {
guard let client,
client.isAuth,
let id = Int(url.lastPathComponent)
else {
return urlHandler?(url) ?? .systemAction
}
// First check whether we already know that the client's server federates with the server this post is on
if client.hasConnection(with: url) {
navigateToStatus(url: url, id: id)
return .handled
}
Task {
// Client does not currently report a federation relationship, but that doesn't mean none exists
// Ensure client is aware of all peers its server federates with so it can give a meaningful answer to hasConnection(with:)
do {
let connections: [String] = try await client.get(endpoint: Instances.peers)
client.addConnections(connections)
} catch {
handlerOrDefault(url: url)
return
}
guard client.hasConnection(with: url) else {
handlerOrDefault(url: url)
return
}
navigateToStatus(url: url, id: id)
}
return .handled
}
private func navigateToStatus(url: URL, id: Int) {
guard let client else { return }
if url.absoluteString.contains(client.server) {
navigate(to: .statusDetail(id: String(id)))
} else {
navigate(to: .remoteStatusDetail(url: url))
}
}
private func handlerOrDefault(url: URL) {
if let urlHandler {
_ = urlHandler(url)
} else {
UIApplication.shared.open(url)
}
}
public func navigateToAccountFrom(acct: String, url: URL) async {
guard let client else { return }
let results: SearchResults? = try? await client.get(endpoint: Search.search(query: acct,

View file

@ -0,0 +1,18 @@
import SwiftUI
public enum TranslationType: String, CaseIterable {
case useServerIfPossible
case useDeepl
case useApple
public var description: LocalizedStringKey {
switch self {
case .useServerIfPossible:
"Instance"
case .useDeepl:
"DeepL"
case .useApple:
"Apple Translate"
}
}
}

View file

@ -25,7 +25,7 @@ import SwiftUI
@AppStorage("app_require_alt_text") public var appRequireAltText = false
@AppStorage("autoplay_video") public var autoPlayVideo = true
@AppStorage("mute_video") public var muteVideo = true
@AppStorage("always_use_deepl") public var alwaysUseDeepl = false
@AppStorage("preferred_translation_type") public var preferredTranslationType = TranslationType.useServerIfPossible
@AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
@ -61,7 +61,32 @@ import SwiftUI
@AppStorage("show_account_popover") public var showAccountPopover: Bool = true
init() {}
@AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false
init() {
prepareTranslationType()
}
private func prepareTranslationType() {
let sharedDefault = UserDefaults.standard
if let alwaysUseDeepl = (sharedDefault.object(forKey: "always_use_deepl") as? Bool) {
if alwaysUseDeepl {
preferredTranslationType = .useDeepl
}
sharedDefault.removeObject(forKey: "always_use_deepl")
}
#if canImport(_Translation_SwiftUI)
if #unavailable(iOS 17.4),
preferredTranslationType == .useApple
{
preferredTranslationType = .useServerIfPossible
}
#else
if preferredTranslationType == .useApple {
preferredTranslationType = .useServerIfPossible
}
#endif
}
}
public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp")
@ -183,9 +208,9 @@ import SwiftUI
}
}
public var alwaysUseDeepl: Bool {
public var preferredTranslationType: TranslationType {
didSet {
storage.alwaysUseDeepl = alwaysUseDeepl
storage.preferredTranslationType = preferredTranslationType
}
}
@ -327,6 +352,12 @@ import SwiftUI
}
}
public var isSidebarExpanded: Bool {
didSet {
storage.isSidebarExpanded = isSidebarExpanded
}
}
public func getRealMaxIndent() -> UInt {
showReplyIndentation ? maxReplyIndentation : 0
}
@ -474,7 +505,7 @@ import SwiftUI
appDefaultPostsSensitive = storage.appDefaultPostsSensitive
appRequireAltText = storage.appRequireAltText
autoPlayVideo = storage.autoPlayVideo
alwaysUseDeepl = storage.alwaysUseDeepl
preferredTranslationType = storage.preferredTranslationType
userDeeplAPIFree = storage.userDeeplAPIFree
autoDetectPostLanguage = storage.autoDetectPostLanguage
inAppBrowserReaderView = storage.inAppBrowserReaderView
@ -501,6 +532,7 @@ import SwiftUI
showReplyIndentation = storage.showReplyIndentation
showAccountPopover = storage.showAccountPopover
muteVideo = storage.muteVideo
isSidebarExpanded = storage.isSidebarExpanded
}
}

View file

@ -39,6 +39,7 @@ public struct ListCreateView: View {
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.toolbar {
CancelToolbarItem()
ToolbarItem {
Button {
Task {

View file

@ -61,7 +61,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
_ = text.removeLast()
_ = text.removeLast()
}
asRawText = text
asRawText = (try? Entities.unescape(text)) ?? text
if asMarkdown.hasPrefix("\n") {
_ = asMarkdown.removeFirst()
@ -175,7 +175,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
return
} else if node.nodeName() == "#text" {
var txt = node.description
txt = (try? Entities.unescape(txt)) ?? txt
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")

View file

@ -6,7 +6,7 @@ public struct NotificationsPolicy: Codable, Sendable {
public var filterNewAccounts: Bool
public var filterPrivateMentions: Bool
public let summary: Summary
public struct Summary: Codable, Sendable {
public let pendingRequestsCount: String
public let pendingNotificationsCount: String

View file

@ -12,20 +12,8 @@ public struct DeepLClient: Sendable {
"https://api\(deeplUserAPIFree && (deeplUserAPIKey != nil) ? "-free" : "").deepl.com/v2/translate"
}
private var APIKey: String {
if let deeplUserAPIKey {
return deeplUserAPIKey
}
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let secret = NSDictionary(contentsOfFile: path)
return secret?["DEEPL_SECRET"] as? String ?? ""
}
return ""
}
private var authorizationHeaderValue: String {
"DeepL-Auth-Key \(APIKey)"
"DeepL-Auth-Key \(deeplUserAPIKey ?? "")"
}
public struct Response: Decodable {
@ -49,26 +37,22 @@ public struct DeepLClient: Sendable {
}
public func request(target: String, text: String) async throws -> Translation {
do {
var components = URLComponents(string: endpoint)!
var queryItems: [URLQueryItem] = []
queryItems.append(.init(name: "text", value: text))
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let (result, _) = try await URLSession.shared.data(for: request)
let response = try decoder.decode(Response.self, from: result)
if let translation = response.translations.first {
return .init(content: translation.text.removingPercentEncoding ?? "",
detectedSourceLanguage: translation.detectedSourceLanguage,
provider: "DeepL.com")
}
throw DeepLError.notFound
} catch {
throw error
var components = URLComponents(string: endpoint)!
var queryItems: [URLQueryItem] = []
queryItems.append(.init(name: "text", value: text))
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let (result, _) = try await URLSession.shared.data(for: request)
let response = try decoder.decode(Response.self, from: result)
if let translation = response.translations.first {
return .init(content: translation.text.removingPercentEncoding ?? "",
detectedSourceLanguage: translation.detectedSourceLanguage,
provider: "DeepL.com")
}
throw DeepLError.notFound
}
}

View file

@ -32,6 +32,8 @@ public enum Accounts: Endpoint {
case mute(id: String, json: MuteData)
case unmute(id: String)
case relationshipNote(id: String, json: RelationshipNoteData)
case blockList
case muteList
public func path() -> String {
switch self {
@ -81,6 +83,10 @@ public enum Accounts: Endpoint {
"accounts/\(id)/unmute"
case let .relationshipNote(id, _):
"accounts/\(id)/note"
case .blockList:
"blocks"
case .muteList:
"mutes"
}
}

View file

@ -33,7 +33,7 @@ public enum Notifications: Endpoint {
"notifications/clear"
}
}
public var jsonValue: (any Encodable)? {
switch self {
case let .putPolicy(policy):

View file

@ -25,7 +25,7 @@ public struct OpenAIClient {
public let content: String
}
let model = "gpt-3.5-turbo"
let model = "gpt-4o"
let messages: [Message]
let temperature: CGFloat
@ -52,7 +52,7 @@ public struct OpenAIClient {
public let content: [MessageContent]
}
let model = "gpt-4-vision-preview"
let model = "gpt-4o"
let messages: [Message]
let maxTokens = 50
}

View file

@ -1,14 +1,14 @@
import SwiftUI
import Models
import DesignSystem
import Env
import Models
import SwiftUI
struct NotificationsHeaderFilteredView: View {
@Environment(Theme.self) private var theme
@Environment(RouterPath.self) private var routerPath
let filteredNotifications: NotificationsPolicy.Summary
var body: some View {
if let count = Int(filteredNotifications.pendingNotificationsCount), count > 0 {
HStack {

View file

@ -7,14 +7,14 @@ import SwiftUI
@MainActor
public struct NotificationsListView: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(Theme.self) private var theme
@Environment(StreamWatcher.self) private var watcher
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@Environment(CurrentAccount.self) private var account
@Environment(CurrentInstance.self) private var currentInstance
@State private var viewModel = NotificationsViewModel()
@State private var isNotificationsPolicyPresented: Bool = false
@Binding var scrollToTopSignal: Int
@ -24,7 +24,8 @@ public struct NotificationsListView: View {
public init(lockedType: Models.Notification.NotificationType? = nil,
lockedAccountId: String? = nil,
scrollToTopSignal: Binding<Int>) {
scrollToTopSignal: Binding<Int>)
{
self.lockedType = lockedType
self.lockedAccountId = lockedAccountId
_scrollToTopSignal = scrollToTopSignal
@ -113,7 +114,7 @@ public struct NotificationsListView: View {
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.onAppear {
.onAppear {
viewModel.client = client
viewModel.currentAccount = account
if let lockedType {

View file

@ -1,50 +1,50 @@
import SwiftUI
import Network
import DesignSystem
import Models
import Network
import SwiftUI
@MainActor
struct NotificationsPolicyView: View {
@Environment(\.dismiss) private var dismiss
@Environment(Client.self) private var client
@Environment(Theme.self) private var theme
@State private var policy: NotificationsPolicy?
@State private var isUpdating: Bool = false
var body: some View {
NavigationStack {
Form {
Section("notifications.content-filter.title-inline") {
Toggle(isOn: .init(get: { policy?.filterNotFollowing == true },
set: { newValue in
policy?.filterNotFollowing = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleYouDontFollow")
})
policy?.filterNotFollowing = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleYouDontFollow")
})
Toggle(isOn: .init(get: { policy?.filterNotFollowers == true },
set: { newValue in
policy?.filterNotFollowers = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleNotFollowingYou")
})
policy?.filterNotFollowers = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleNotFollowingYou")
})
Toggle(isOn: .init(get: { policy?.filterNewAccounts == true },
set: { newValue in
policy?.filterNewAccounts = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.newAccounts")
})
policy?.filterNewAccounts = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.newAccounts")
})
Toggle(isOn: .init(get: { policy?.filterPrivateMentions == true },
set: { newValue in
policy?.filterPrivateMentions = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.privateMentions")
})
policy?.filterPrivateMentions = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.privateMentions")
})
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
@ -63,7 +63,7 @@ struct NotificationsPolicyView: View {
.presentationDetents([.medium])
.presentationBackground(.thinMaterial)
}
private func getPolicy() async {
defer {
isUpdating = false
@ -75,7 +75,7 @@ struct NotificationsPolicyView: View {
dismiss()
}
}
private func updatePolicy() async {
if let policy {
defer {
@ -84,7 +84,7 @@ struct NotificationsPolicyView: View {
do {
isUpdating = true
self.policy = try await client.put(endpoint: Notifications.putPolicy(policy: policy))
} catch { }
} catch {}
}
}
}

View file

@ -39,7 +39,7 @@ import SwiftUI
private let filterKey = "notification-filter"
var state: State = .loading
var isLockedType: Bool = false
var lockedAccountId: String? = nil
var lockedAccountId: String?
var policy: Models.NotificationsPolicy?
var selectedType: Models.Notification.NotificationType? {
didSet {
@ -156,7 +156,7 @@ import SwiftUI
let newNotifications: [Models.Notification]
if let lockedAccountId {
newNotifications =
try await client.get(endpoint: Notifications.notificationsForAccount(accountId: lockedAccountId, maxId: lastId))
try await client.get(endpoint: Notifications.notificationsForAccount(accountId: lockedAccountId, maxId: lastId))
} else {
newNotifications =
try await client.get(endpoint: Notifications.notifications(minId: nil,
@ -180,7 +180,7 @@ import SwiftUI
} catch {}
}
}
func fetchPolicy() async {
policy = try? await client?.get(endpoint: Notifications.policy)
}

View file

@ -1,30 +1,31 @@
import SwiftUI
import Network
import Models
import DesignSystem
import Models
import Network
import SwiftUI
@MainActor
public struct NotificationsRequestsListView: View {
@Environment(Client.self) private var client
@Environment(Theme.self) private var theme
enum ViewState {
case loading
case error
case requests(_ data: [NotificationsRequest])
}
@State private var viewState: ViewState = .loading
public init() { }
public init() {}
public var body: some View {
List {
switch viewState {
case .loading:
ProgressView()
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
#endif
.listSectionSeparator(.hidden)
case .error:
ErrorView(title: "notifications.error.title",
@ -33,10 +34,10 @@ public struct NotificationsRequestsListView: View {
{
await fetchRequests()
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
.listSectionSeparator(.hidden)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
.listSectionSeparator(.hidden)
case let .requests(data):
ForEach(data) { request in
NotificationsRequestsRowView(request: request)
@ -46,7 +47,7 @@ public struct NotificationsRequestsListView: View {
} label: {
Label("account.follow-request.accept", systemImage: "checkmark")
}
Button {
Task { await dismissRequest(request) }
} label: {
@ -59,32 +60,32 @@ public struct NotificationsRequestsListView: View {
}
.listStyle(.plain)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.navigationTitle("notifications.content-filter.requests.title")
.navigationBarTitleDisplayMode(.inline)
.task {
await fetchRequests()
}
.refreshable {
await fetchRequests()
}
.navigationTitle("notifications.content-filter.requests.title")
.navigationBarTitleDisplayMode(.inline)
.task {
await fetchRequests()
}
.refreshable {
await fetchRequests()
}
}
private func fetchRequests() async {
do {
viewState = .requests(try await client.get(endpoint: Notifications.requests))
viewState = try .requests(await client.get(endpoint: Notifications.requests))
} catch {
viewState = .error
}
}
private func acceptRequest(_ request: NotificationsRequest) async {
_ = try? await client.post(endpoint: Notifications.acceptRequest(id: request.id))
await fetchRequests()
}
private func dismissRequest(_ request: NotificationsRequest) async {
_ = try? await client.post(endpoint: Notifications.dismissRequest(id: request.id))
await fetchRequests()

View file

@ -1,20 +1,20 @@
import SwiftUI
import Models
import DesignSystem
import Env
import Models
import Network
import SwiftUI
struct NotificationsRequestsRowView: View {
@Environment(Theme.self) private var theme
@Environment(RouterPath.self) private var routerPath
@Environment(Client.self) private var client
let request: NotificationsRequest
var body: some View {
HStack(alignment: .center, spacing: 8) {
AvatarView(request.account.avatar, config: .embed)
VStack(alignment: .leading) {
EmojiTextApp(request.account.cachedDisplayName, emojis: request.account.emojis)
.font(.scaledBody)
@ -35,14 +35,14 @@ struct NotificationsRequestsRowView: View {
.padding(8)
.background(.secondary)
.clipShape(Circle())
Image(systemName: "chevron.right")
.foregroundStyle(.secondary)
}
.onTapGesture {
routerPath.navigate(to: .notificationForAccount(accountId: request.account.id))
}
.listRowInsets(.init(top: 12,
.listRowInsets(.init(top: 12,
leading: .layoutPadding,
bottom: 12,
trailing: .layoutPadding))
@ -50,7 +50,7 @@ struct NotificationsRequestsRowView: View {
.listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background))
#else
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
}

View file

@ -161,7 +161,7 @@ extension StatusEditor {
Button {
// all SEVM have the same visibility value
followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility)))
followUpSEVMs.append(ViewModel(mode: .new(text: nil, visibility: focusedSEVM.visibility)))
} label: {
Image(systemName: "arrowshape.turn.up.left.circle.fill")
}

View file

@ -16,8 +16,8 @@ extension StatusEditor {
var body: some View {
NavigationStack {
ScrollView {
ForEach(viewModel.customEmojiContainer) { container in
LazyVGrid(columns: [GridItem(.adaptive(minimum: 40, maximum: 40))], spacing: 9) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 40, maximum: 40))], spacing: 9) {
ForEach(viewModel.customEmojiContainer) { container in
Section {
ForEach(container.emojis) { emoji in
LazyImage(url: emoji.url) { state in
@ -39,15 +39,16 @@ extension StatusEditor {
viewModel.insertStatusText(text: " :\(emoji.shortcode): ")
}
}
.padding(.horizontal, 16)
} header: {
HStack {
Text(container.categoryName)
.font(.scaledFootnote)
Spacer()
}
Text(container.categoryName)
.font(.scaledHeadline)
.bold()
.foregroundStyle(Color.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
}
}
.padding(.horizontal, 8)
}
}
.toolbar {

View file

@ -23,7 +23,7 @@ extension StatusEditor {
@State private var didAppear: Bool = false
@State private var isGeneratingDescription: Bool = false
@State private var showTranslateButton: Bool = false
@State private var showTranslateView: Bool = false
@State private var isTranslating: Bool = false
var body: some View {
@ -34,8 +34,14 @@ extension StatusEditor {
text: $imageDescription,
axis: .vertical)
.focused($isFieldFocused)
generateButton
translateButton
if imageDescription.isEmpty {
generateButton
}
#if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *), !imageDescription.isEmpty {
translateButton
}
#endif
}
.listRowBackground(theme.primaryBackgroundColor)
Section {
@ -111,12 +117,6 @@ extension StatusEditor {
Task {
if let description = await generateDescription(url: url) {
imageDescription = description
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
if lang != nil, lang != "en" {
withAnimation {
showTranslateButton = true
}
}
}
}
} label: {
@ -131,24 +131,18 @@ extension StatusEditor {
@ViewBuilder
private var translateButton: some View {
if showTranslateButton {
Button {
Task {
if let description = await translateDescription() {
imageDescription = description
withAnimation {
showTranslateButton = false
}
}
}
} label: {
if isTranslating {
ProgressView()
} else {
Text("status.action.translate")
}
Button {
showTranslateView = true
} label: {
if isTranslating {
ProgressView()
} else {
Text("status.action.translate")
}
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $showTranslateView, text: imageDescription)
#endif
}
private func generateDescription(url: URL) async -> String? {
@ -158,17 +152,5 @@ extension StatusEditor {
isGeneratingDescription = false
return response?.trimmedText
}
private func translateDescription() async -> String? {
isTranslating = true
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
guard let lang else { return nil }
let translation = try? await deeplClient.request(target: lang, text: imageDescription)
isTranslating = false
return translation?.content.asRawText
}
}
}

View file

@ -41,108 +41,110 @@ public extension StatusEditor {
@Bindable var focusedSEVM = focusedSEVM
NavigationStack {
ScrollView {
VStackLayout(spacing: 0) {
if mainSEVM.isPosting {
ProgressView(value: mainSEVM.postingProgress, total: 100.0)
}
EditorView(
viewModel: mainSEVM,
followUpSEVMs: $followUpSEVMs,
editingMediaContainer: $editingMediaContainer,
editorFocusState: $editorFocusState,
assignedFocusState: .main,
isMain: true
)
.id(mainSEVM.id)
ForEach(followUpSEVMs) { sevm in
@Bindable var sevm: ViewModel = sevm
ZStack(alignment: .top) {
ScrollView {
VStackLayout(spacing: 0) {
EditorView(
viewModel: sevm,
viewModel: mainSEVM,
followUpSEVMs: $followUpSEVMs,
editingMediaContainer: $editingMediaContainer,
editorFocusState: $editorFocusState,
assignedFocusState: .followUp(index: sevm.id),
isMain: false
assignedFocusState: .main,
isMain: true
)
.id(sevm.id)
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $scrollID, anchor: .top)
.animation(.bouncy(duration: 0.3), value: editorFocusState)
.animation(.bouncy(duration: 0.3), value: followUpSEVMs)
#if !os(visionOS)
.background(theme.primaryBackgroundColor)
#endif
.safeAreaInset(edge: .bottom) {
AutoCompleteView(viewModel: focusedSEVM)
}
#if os(visionOS)
.ornament(attachmentAnchor: .scene(.leading)) {
AccessoryView(focusedSEVM: focusedSEVM,
followUpSEVMs: $followUpSEVMs)
}
#else
.safeAreaInset(edge: .bottom) {
if presentationDetent == .large || presentationDetent == .medium {
AccessoryView(focusedSEVM: focusedSEVM,
followUpSEVMs: $followUpSEVMs)
.id(mainSEVM.id)
ForEach(followUpSEVMs) { sevm in
@Bindable var sevm: ViewModel = sevm
EditorView(
viewModel: sevm,
followUpSEVMs: $followUpSEVMs,
editingMediaContainer: $editingMediaContainer,
editorFocusState: $editorFocusState,
assignedFocusState: .followUp(index: sevm.id),
isMain: false
)
.id(sevm.id)
}
}
#endif
.accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views
.navigationTitle(focusedSEVM.mode.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItems(mainSEVM: mainSEVM,
focusedSEVM: focusedSEVM,
followUpSEVMs: followUpSEVMs) }
.toolbarBackground(.visible, for: .navigationBar)
.alert(
"status.error.posting.title",
isPresented: $focusedSEVM.showPostingErrorAlert,
actions: {
Button("OK") {}
}, message: {
Text(mainSEVM.postingError ?? "")
}
)
.interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning)
.onChange(of: appAccounts.currentClient) { _, newValue in
if mainSEVM.mode.isInShareExtension {
currentAccount.setClient(client: newValue)
mainSEVM.client = newValue
for post in followUpSEVMs {
post.client = newValue
.scrollTargetLayout()
}
.scrollPosition(id: $scrollID, anchor: .top)
.animation(.bouncy(duration: 0.3), value: editorFocusState)
.animation(.bouncy(duration: 0.3), value: followUpSEVMs)
#if !os(visionOS)
.background(theme.primaryBackgroundColor)
#endif
.safeAreaInset(edge: .bottom) {
AutoCompleteView(viewModel: focusedSEVM)
}
#if os(visionOS)
.ornament(attachmentAnchor: .scene(.leading)) {
AccessoryView(focusedSEVM: focusedSEVM,
followUpSEVMs: $followUpSEVMs)
}
#else
.safeAreaInset(edge: .bottom) {
if presentationDetent == .large || presentationDetent == .medium {
AccessoryView(focusedSEVM: focusedSEVM,
followUpSEVMs: $followUpSEVMs)
}
}
}
.onDrop(of: [.image, .video, .gif, .mpeg4Movie, .quickTimeMovie, .movie],
delegate: focusedSEVM)
.onChange(of: currentAccount.account?.id) {
mainSEVM.currentAccount = currentAccount.account
for p in followUpSEVMs {
p.currentAccount = mainSEVM.currentAccount
}
}
.onChange(of: mainSEVM.visibility) {
for p in followUpSEVMs {
p.visibility = mainSEVM.visibility
}
}
.onChange(of: followUpSEVMs.count) { oldValue, newValue in
if oldValue < newValue {
Task {
try? await Task.sleep(for: .seconds(0.1))
withAnimation(.bouncy(duration: 0.5)) {
scrollID = followUpSEVMs.last?.id
#endif
.accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views
.navigationTitle(focusedSEVM.mode.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItems(mainSEVM: mainSEVM,
focusedSEVM: focusedSEVM,
followUpSEVMs: followUpSEVMs) }
.toolbarBackground(.visible, for: .navigationBar)
.alert(
"status.error.posting.title",
isPresented: $focusedSEVM.showPostingErrorAlert,
actions: {
Button("OK") {}
}, message: {
Text(mainSEVM.postingError ?? "")
}
)
.interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning)
.onChange(of: appAccounts.currentClient) { _, newValue in
if mainSEVM.mode.isInShareExtension {
currentAccount.setClient(client: newValue)
mainSEVM.client = newValue
for post in followUpSEVMs {
post.client = newValue
}
}
}
}
.onDrop(of: [.image, .video, .gif, .mpeg4Movie, .quickTimeMovie, .movie],
delegate: focusedSEVM)
.onChange(of: currentAccount.account?.id) {
mainSEVM.currentAccount = currentAccount.account
for p in followUpSEVMs {
p.currentAccount = mainSEVM.currentAccount
}
}
.onChange(of: mainSEVM.visibility) {
for p in followUpSEVMs {
p.visibility = mainSEVM.visibility
}
}
.onChange(of: followUpSEVMs.count) { oldValue, newValue in
if oldValue < newValue {
Task {
try? await Task.sleep(for: .seconds(0.1))
withAnimation(.bouncy(duration: 0.5)) {
scrollID = followUpSEVMs.last?.id
}
}
}
}
if mainSEVM.isPosting {
ProgressView(value: mainSEVM.postingProgress, total: 100.0)
}
}
}
.sheet(item: $editingMediaContainer) { container in
StatusEditor.MediaEditView(viewModel: focusedSEVM, container: container)

View file

@ -5,12 +5,13 @@ import UIKit
public extension StatusEditor.ViewModel {
enum Mode {
case replyTo(status: Status)
case new(visibility: Models.Visibility)
case new(text: String?, visibility: Models.Visibility)
case edit(status: Status)
case quote(status: Status)
case quoteLink(link: URL)
case mention(account: Account, visibility: Models.Visibility)
case shareExtension(items: [NSItemProvider])
case imageURL(urls: [URL], visibility: Models.Visibility)
var isInShareExtension: Bool {
switch self {
@ -41,7 +42,7 @@ public extension StatusEditor.ViewModel {
var title: LocalizedStringKey {
switch self {
case .new, .mention, .shareExtension, .quoteLink:
case .new, .mention, .shareExtension, .quoteLink, .imageURL:
"status.editor.mode.new"
case .edit:
"status.editor.mode.edit"

View file

@ -234,7 +234,7 @@ public extension StatusEditor {
language: selectedLanguage,
mediaAttributes: mediaAttributes)
switch mode {
case .new, .replyTo, .quote, .mention, .shareExtension, .quoteLink:
case .new, .replyTo, .quote, .mention, .shareExtension, .quoteLink, .imageURL:
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))
if let postStatus {
StreamWatcher.shared.emmitPostEvent(for: postStatus)
@ -301,12 +301,23 @@ public extension StatusEditor {
func prepareStatusText() {
switch mode {
case let .new(visibility):
case let .new(text, visibility):
if let text {
statusText = .init(string: text)
selectedRange = .init(location: text.utf16.count, length: 0)
}
self.visibility = visibility
case let .shareExtension(items):
itemsProvider = items
visibility = .pub
processItemsProvider(items: items)
case let .imageURL(urls, visibility):
Task {
for container in await Self.makeImageContainer(from: urls) {
prepareToPost(for: container)
}
}
self.visibility = visibility
case let .replyTo(status):
var mentionString = ""
if (status.reblog?.account.acct ?? status.account.acct) != currentAccount?.acct {
@ -557,7 +568,7 @@ public extension StatusEditor {
!statusText.string.contains(url.absoluteString)
{
embeddedStatus = nil
mode = .new(visibility: visibility)
mode = .new(text: nil, visibility: visibility)
}
}
@ -736,6 +747,31 @@ public extension StatusEditor {
)
}
private static func makeImageContainer(from urls: [URL]) async -> [MediaContainer] {
var containers: [MediaContainer] = []
for url in urls {
let compressor = Compressor()
_ = url.startAccessingSecurityScopedResource()
if let compressedData = await compressor.compressImageFrom(url: url),
let image = UIImage(data: compressedData)
{
containers.append(MediaContainer(
id: UUID().uuidString,
image: image,
movieTransferable: nil,
gifTransferable: nil,
mediaAttachment: nil,
error: nil
))
}
url.stopAccessingSecurityScopedResource()
}
return containers
}
func upload(container: MediaContainer) async {
if let index = indexOf(container: container) {
let originalContainer = mediaContainers[index]

View file

@ -15,6 +15,7 @@ public struct StatusRowView: View {
@Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled
@Environment(\.isStatusFocused) private var isFocused
@Environment(\.indentationLevel) private var indentationLevel
@Environment(RouterPath.self) private var routerPath: RouterPath
@Environment(QuickLook.self) private var quickLook
@Environment(Theme.self) private var theme
@ -219,6 +220,23 @@ public struct StatusRowView: View {
StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus,
client: viewModel.client)
)
.alert("DeepL couldn't be reached!\nIs the API Key correct?", isPresented: $viewModel.deeplTranslationError) {
Button("alert.button.ok", role: .cancel) {}
Button("settings.general.translate") {
RouterPath.settingsStartingPoint = .translation
routerPath.presentedSheet = .settings
}
}
.alert("The Translation Service of your Instance couldn't be reached!", isPresented: $viewModel.instanceTranslationError) {
Button("alert.button.ok", role: .cancel) {}
Button("settings.general.translate") {
RouterPath.settingsStartingPoint = .translation
routerPath.presentedSheet = .settings
}
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText)
#endif
}
@ViewBuilder

View file

@ -31,6 +31,18 @@ import SwiftUI
var translation: Translation?
var isLoadingTranslation: Bool = false
var showDeleteAlert: Bool = false
var showAppleTranslation = false
var preferredTranslationType = TranslationType.useServerIfPossible {
didSet {
if oldValue != preferredTranslationType {
translation = nil
showAppleTranslation = false
}
}
}
var deeplTranslationError = false
var instanceTranslationError = false
private(set) var actionsAccountsFetched: Bool = false
var favoriters: [Account] = []
@ -297,25 +309,43 @@ import SwiftUI
}
func translate(userLang: String) async {
withAnimation {
isLoadingTranslation = true
}
if !alwaysTranslateWithDeepl {
do {
// We first use instance translation API if available.
let translation: Translation = try await client.post(endpoint: Statuses.translate(id: finalStatus.id,
lang: userLang))
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
return
} catch {}
updatePreferredTranslation()
if preferredTranslationType == .useApple {
showAppleTranslation = true
return
}
// If not or fail we use Ice Cubes own DeepL client.
await translateWithDeepL(userLang: userLang)
if preferredTranslationType != .useDeepl {
await translateWithInstance(userLang: userLang)
if translation == nil {
await translateWithDeepL(userLang: userLang)
}
} else {
await translateWithDeepL(userLang: userLang)
if translation == nil {
await translateWithInstance(userLang: userLang)
}
}
var hasShown = false
#if canImport(_Translation_SwiftUI)
if translation == nil,
#available(iOS 17.4, *) {
showAppleTranslation = true
hasShown = true
}
#endif
if !hasShown,
translation == nil {
if preferredTranslationType == .useDeepl {
deeplTranslationError = true
} else {
instanceTranslationError = true
}
}
}
func translateWithDeepL(userLang: String) async {
@ -331,6 +361,20 @@ import SwiftUI
isLoadingTranslation = false
}
}
func translateWithInstance(userLang: String) async {
withAnimation {
isLoadingTranslation = true
}
let translation: Translation? = try? await client.post(endpoint: Statuses.translate(id: finalStatus.id,
lang: userLang))
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
}
private func getDeepLClient() -> DeepLClient {
let userAPIfree = UserPreferences.shared.userDeeplAPIFree
@ -339,11 +383,17 @@ import SwiftUI
}
private var userAPIKey: String? {
DeepLUserAPIHandler.readIfAllowed()
DeepLUserAPIHandler.readKey()
}
var alwaysTranslateWithDeepl: Bool {
DeepLUserAPIHandler.shouldAlwaysUseDeepl
func updatePreferredTranslation() {
if DeepLUserAPIHandler.shouldAlwaysUseDeepl {
preferredTranslationType = .useDeepl
} else if UserPreferences.shared.preferredTranslationType == .useApple {
preferredTranslationType = .useApple
} else {
preferredTranslationType = .useServerIfPossible
}
}
func fetchRemoteStatus() async -> Bool {

View file

@ -31,7 +31,7 @@ struct StatusRowContextMenu: View {
}
if statusDataController.isReblogged {
return Label("status.action.unboost", image: "Rocket.fill")
return Label("status.action.unboost", image: "Rocket.Fill")
}
return Label("status.action.boost", image: "Rocket")
}

View file

@ -258,6 +258,7 @@ struct AltTextButton: View {
@Environment(Theme.self) private var theme
@State private var isDisplayingAlert = false
@State private var isDisplayingTranslation = false
var body: some View {
if !isInCaptureMode,
@ -278,6 +279,9 @@ struct AltTextButton: View {
.buttonStyle(.borderless)
.padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7))
.background(.thinMaterial)
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $isDisplayingTranslation, text: text)
#endif
#if os(visionOS)
.clipShape(Capsule())
#endif
@ -288,6 +292,12 @@ struct AltTextButton: View {
isPresented: $isDisplayingAlert
) {
Button("alert.button.ok", action: {})
Button("status.action.copy-text", action: { UIPasteboard.general.string = text })
#if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *) {
Button("status.action.translate", action: { isDisplayingTranslation = true })
}
#endif
} message: {
Text(text)
}

View file

@ -35,7 +35,8 @@ struct StatusRowTranslateView: View {
}
}
var body: some View {
@ViewBuilder
var translateButton: some View {
if !isInCaptureMode,
!isCompact,
let userLang = preferences.serverPreferences?.postLanguage,
@ -54,8 +55,22 @@ struct StatusRowTranslateView: View {
}
.buttonStyle(.borderless)
}
}
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
@ViewBuilder
var generalTranslateButton: some View {
translateButton
}
var body: some View {
generalTranslateButton
.onChange(of: preferences.preferredTranslationType) { _, _ in
withAnimation {
_ = viewModel.updatePreferredTranslation()
}
}
if let translation = viewModel.translation, !viewModel.isLoadingTranslation, preferences.preferredTranslationType != .useApple {
GroupBox {
VStack(alignment: .leading, spacing: 4) {
Text(translation.content.asSafeMarkdownAttributedString)

View file

@ -3,7 +3,7 @@ import Models
import Network
import SwiftUI
public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable, Sendable {
case local, federated, trending
public func localizedTitle() -> LocalizedStringKey {
@ -29,7 +29,7 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
}
}
public enum TimelineFilter: Hashable, Equatable, Identifiable {
public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable {
case home, local, federated, trending
case hashtag(tag: String, accountId: String?)
case tagGroup(title: String, tags: [String], symbolName: String?)