Compare commits

...

121 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
Thomas Ricouard 5c32c24ae5 Add supports for notifications filter API 2024-03-26 15:49:43 +01:00
Roddy Munro bb56047ee2
Add SwiftPolyglot to validate localizations (#2011)
* Add step to validate translations on PRs without errors

* Move workflow

* Get specific release of SwiftPolyglot
2024-03-25 08:48:44 +01:00
sh95014 924ada6606
Update zh_Hant loc (#2013)
* checkpoint

* checkpoint

* plurals and a couple of minor fixes

* Update Localizable.strings

* Update Localizable.strings

* Update zh-Hant localizations

* improve translation of "by" in "filtered by"

* update zh-Hant localization

* update Hant localization

* Update zh_Hant

* improve status.poll.closes-in

* Update zh_Hant

* Custom layout for App Store links

* update zh-Hant

* update zh_Hant

* update zh_Hant

* update zh_Hant

* update zh_Hant

* update zh_Hant

* update zh_Hant
2024-03-25 08:47:21 +01:00
Jerry Zhang e6f96d1899
Update Simplified Chinese localization (#2005) 2024-03-14 09:45:11 +01:00
Thomas Ricouard 058500f91e Swiftformat . 2024-03-11 09:05:52 +01:00
Thomas Ricouard fd3d9fc2bc Add missing localization 2024-03-11 09:02:32 +01:00
Thomas Ricouard 9a7e6b7cb0 Various fixes for Xcode 15.3 2024-03-11 08:59:29 +01:00
Max von Webel bc2a09891a
Added a "Moved To" Button to accounts that moved to other instances (#2001)
* added moved information to Account model

* Added "Moved To" button to account details for accounts that have moved
2024-03-11 08:57:35 +01:00
Ahnaf Mahmud 7c343eb4e9
Update visionOS availability (#1999) 2024-03-11 08:56:50 +01:00
Thai D. V 15d7d1dabd
handle edge cases for StatusRowCardView (#1985) 2024-02-26 11:50:10 +01:00
Xabi f4ec69a37f
Update EU localisation (#1986) 2024-02-26 11:49:32 +01:00
Thomas Ricouard 732a253c7a More EN locale fix 2024-02-26 09:17:59 +01:00
Thomas Ricouard 9c67af8451 Fix EN locale 2024-02-26 09:16:16 +01:00
Thomas Ricouard b56da94a7c Add more sheets to shared + link to filters in timeline top filters 2024-02-21 09:45:29 +01:00
Alessio Mason e612fbdf7c
Italian localization fix (#1982)
* General Italian localization overhaul

* Quick IT fix

* Another quick IT fix

* Italian localization fix
2024-02-17 19:26:14 +01:00
Cthulhux f46a0cee17
de: two more missing strings (#1975)
While using the app, I found that I had not seen two strings. Probably they were marked "translated" too early. Sorry! Here they are.
2024-02-16 08:50:41 +01:00
Andrzej Rózga 4a90d979e3
Polish localization update (#1976)
* Polish localization update

* pl: two more missing strings
2024-02-16 08:50:34 +01:00
Jerry Zhang 9e4323f317
Update Simplified Chinese localization (#1974) 2024-02-14 13:34:19 +01:00
Thomas Ricouard 24ce872849 Add previews + refactor placeholder view 2024-02-14 13:34:06 +01:00
Thomas Ricouard 1f858414d8 format . 2024-02-14 12:48:14 +01:00
Thomas Ricouard 2d988d48c1 Remove some button from status row 2024-02-14 10:48:17 +01:00
Thomas Ricouard 21d9fd7b59 Bump version to 1.10.33 2024-02-14 07:37:15 +01:00
Thomas Ricouard cca6472a32 Update to Nuke 12.4.0 2024-02-13 18:51:00 +01:00
Thomas Ricouard c769e80bb6 Add preview for status row 2024-02-13 18:50:51 +01:00
Thomas Ricouard 2986d2b177 Fix env 2024-02-13 17:21:33 +01:00
sh95014 29312d1be2
select a contrasting color for label of "show sensitive content" button (#1965)
* Custom layout for App Store links

* select a contrasting color for label of "show sensitive content" button

fix https://github.com/Dimillian/IceCubesApp/issues/1932

* move contrasting color to Theme and cache computed var
2024-02-13 11:33:59 +01:00
Cthulhux 9ddf0e65fc
Update Localizable.xcstrings (#1969) 2024-02-13 07:50:05 +01:00
Thomas Ricouard bc74a50a6a Fix some English strings 2024-02-12 07:22:28 +01:00
sh95014 d55d6a0371
Use horizontal link preview card for Apple Podcasts as well (#1966)
* Custom layout for App Store links

* generalize the logic to include links known to be associated with square icons

- such as Apple Music and Spotify

* show Apple Podcasts in horizontal link preview
2024-02-12 07:18:18 +01:00
sh95014 773fdc318b
Plurals for various strings (#1968)
fix https://github.com/Dimillian/IceCubesApp/issues/1936
2024-02-12 07:18:08 +01:00
Thomas Ricouard 7423aba92a Fix crash on visionOS in AboutView 2024-02-11 18:59:34 +01:00
Thomas Ricouard 77aa50ef19 Fix #1873 2024-02-11 18:52:58 +01:00
Thomas Ricouard dfc213a19a Remove spacer 2024-02-11 18:45:38 +01:00
Andrzej Rózga 20900f573f
Polish localization update (#1958) 2024-02-11 11:14:44 +01:00
Ege Sucu 046a41e8ef
TR Localizations are Updated & some are formatted (#1957) 2024-02-11 11:14:36 +01:00
Thomas Ricouard fcd56ab7a0 Fix #1960 2024-02-11 11:13:03 +01:00
Thomas Ricouard 923927cddd Cleanup 2024-02-11 11:12:34 +01:00
Thomas Ricouard 219703ecc7 Refactor to NextPageView + handle next page loading failure 2024-02-11 10:58:51 +01:00
Thomas Ricouard 0739264005 Fix background 2024-02-10 12:16:32 +01:00
Thomas Ricouard 6f8bec4737 Add localization 2024-02-10 11:34:49 +01:00
Thomas Ricouard d8e6e6cfb1 Share Sheet: set cursor before shared content 2024-02-10 11:32:58 +01:00
Thomas Ricouard e7bc857231 News trending links experience 2024-02-10 11:26:22 +01:00
Thomas Ricouard 35d249f7c9 Bump to 1.10.32 2024-02-10 09:57:56 +01:00
220 changed files with 10875 additions and 2989 deletions

View file

@ -0,0 +1,35 @@
name: Validate Translations
on:
pull_request:
types: [synchronize, opened, reopened, labeled, unlabeled, edited]
jobs:
main:
name: Validate Translations
runs-on: macOS-latest
steps:
- name: git checkout
uses: actions/checkout@v3
- name: ruby versions
run: |
ruby --version
gem --version
bundler --version
- name: ruby setup
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
bundler-cache: true
# additional steps here, if needed
- name: Clone SwiftPolyglot
run: git clone https://github.com/appdecostudio/SwiftPolyglot.git --branch 0.2.0
- name: Build and Run SwiftPolyglot
run: |
swift build --package-path ./SwiftPolyglot --configuration release
swift run --package-path ./SwiftPolyglot swiftpolyglot "en,eu,be,ca,zh-Hans,zh-Hant,nl,en-GB,fr,de,it,ja,ko,nb,pl,pt-BR,es,tr,uk"

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.31;
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.31;
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.31;
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.31;
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.31;
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.31;
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.31;
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.31;
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

@ -68,17 +68,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke",
"state" : {
"revision" : "15fde63470d782c897816a74bdd516a907e33147",
"version" : "12.3.0"
"revision" : "8ecbfc886da39bccb01c34abef5f2ff4073ad633",
"version" : "12.4.0"
}
},
{
"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,6 +8,7 @@ import LinkPresentation
import Lists
import MediaUI
import Models
import Notifications
import StatusKit
import SwiftUI
import Timeline
@ -60,9 +61,19 @@ extension View {
scrollToTopSignal: .constant(0),
canFilterTimeline: false)
case let .trendingLinks(cards):
CardsListView(cards: cards)
TrendingLinksListView(cards: cards)
case let .tagsList(tags):
TagsListView(tags: tags)
case .notificationsRequests:
NotificationsRequestsListView()
case let .notificationForAccount(accountId):
NotificationsListView(lockedType: nil,
lockedAccountId: accountId,
scrollToTopSignal: .constant(0))
case .blockedAccounts:
AccountsListView(mode: .blocked)
case .mutedAccounts:
AccountsListView(mode: .muted)
}
}
}
@ -74,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))
@ -82,6 +99,9 @@ extension View {
case let .quoteStatusEditor(status):
StatusEditor.MainView(mode: .quote(status: status))
.withEnvironments()
case let .quoteLinkStatusEditor(link):
StatusEditor.MainView(mode: .quoteLink(link: link))
.withEnvironments()
case let .mentionStatusEditor(account, visibility):
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
.withEnvironments()
@ -137,6 +157,12 @@ extension View {
.presentationDetents([.medium])
.presentationBackground(.thinMaterial)
.withEnvironments()
case .accountEditInfo:
EditAccountView()
.withEnvironments()
case .accountFiltersList:
FiltersListView()
.withEnvironments()
}
}
}

View file

@ -17,29 +17,29 @@ struct AppView: View {
@Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme
@Environment(StreamWatcher.self) private var watcher
@Environment(\.openWindow) var openWindow
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Binding var selectedTab: Tab
@Binding var appRouterPath: RouterPath
@State var popToRootTab: Tab = .other
@State var iosTabs = iOSTabs.shared
@State var sidebarTabs = SidebarTabs.shared
var body: some View {
#if os(visionOS)
tabBarView
#else
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
sidebarView
} else {
tabBarView
}
#else
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
sidebarView
} else {
tabBarView
}
#endif
}
var availableTabs: [Tab] {
guard appAccountsManager.currentClient.isAuth else {
return Tab.loggedOutTab()
@ -49,7 +49,7 @@ struct AppView: View {
} else if UIDevice.current.userInterfaceIdiom == .vision {
return Tab.visionOSTab()
}
return sidebarTabs.tabs.map{ $0.tab }
return sidebarTabs.tabs.map { $0.tab }
}
var tabBarView: some View {
@ -58,9 +58,9 @@ struct AppView: View {
}, set: { newTab in
if newTab == .post {
#if os(visionOS)
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
#else
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
#endif
return
}
@ -104,40 +104,40 @@ struct AppView: View {
}
return 0
}
#if !os(visionOS)
var sidebarView: some View {
SideBarView(selectedTab: $selectedTab,
popToRootTab: $popToRootTab,
tabs: availableTabs)
{
HStack(spacing: 0) {
TabView(selection: $selectedTab) {
ForEach(availableTabs) { tab in
tab
.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
.tabItem {
tab.label
}
.tag(tab)
var sidebarView: some View {
SideBarView(selectedTab: $selectedTab,
popToRootTab: $popToRootTab,
tabs: availableTabs)
{
HStack(spacing: 0) {
TabView(selection: $selectedTab) {
ForEach(availableTabs) { tab in
tab
.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
.tabItem {
tab.label
}
.tag(tab)
}
}
.introspect(.tabView, on: .iOS(.v17)) { (tabview: UITabBarController) in
tabview.tabBar.isHidden = horizontalSizeClass == .regular
tabview.customizableViewControllers = []
tabview.moreNavigationController.isNavigationBarHidden = true
}
if horizontalSizeClass == .regular,
appAccountsManager.currentClient.isAuth,
userPreferences.showiPadSecondaryColumn
{
Divider().edgesIgnoringSafeArea(.all)
notificationsSecondaryColumn
}
}
.introspect(.tabView, on: .iOS(.v17)) { (tabview: UITabBarController) in
tabview.tabBar.isHidden = horizontalSizeClass == .regular
tabview.customizableViewControllers = []
tabview.moreNavigationController.isNavigationBarHidden = true
}
if horizontalSizeClass == .regular,
appAccountsManager.currentClient.isAuth,
userPreferences.showiPadSecondaryColumn
{
Divider().edgesIgnoringSafeArea(.all)
notificationsSecondaryColumn
}
}
.environment(appRouterPath)
}
.environment(appRouterPath)
}
#endif
var notificationsSecondaryColumn: some View {

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):
@ -83,11 +93,14 @@ extension IceCubesApp {
StatusEditor.MainView(mode: .replyTo(status: status))
case let .mentionStatusEditor(account, visibility):
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
case let .quoteLinkStatusEditor(link):
StatusEditor.MainView(mode: .quoteLink(link: link))
case .none:
EmptyView()
}
}
.withEnvironments()
.environment(RouterPath())
.withModelContainer()
.applyTheme(theme)
.frame(minWidth: 300, minHeight: 400)
@ -113,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,13 +23,14 @@ 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
@State var selectedTab: Tab = .timeline
@State var appRouterPath = RouterPath()
@State var isSupporter: Bool = false
var body: some Scene {
@ -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

@ -36,37 +36,37 @@ public struct ReportView: View {
.navigationTitle("report.title")
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
#endif
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSendingReport = true
Task {
do {
let _: ReportSent =
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
statusId: status.id,
comment: commentText))
dismiss()
isSendingReport = false
} catch {
isSendingReport = false
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSendingReport = true
Task {
do {
let _: ReportSent =
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
statusId: status.id,
comment: commentText))
dismiss()
isSendingReport = false
} catch {
isSendingReport = false
}
}
} label: {
if isSendingReport {
ProgressView()
} else {
Text("report.action.send")
}
}
} label: {
if isSendingReport {
ProgressView()
} else {
Text("report.action.send")
}
}
}
CancelToolbarItem()
}
CancelToolbarItem()
}
}
}
}

View file

@ -13,25 +13,28 @@ 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
#if !os(visionOS)
@State private var safariManager = InAppSafariManager()
@State private var safariManager = InAppSafariManager()
#endif
func body(content: Content) -> some View {
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
@ -52,78 +55,78 @@ private struct SafariRouter: ViewModifier {
return .systemAction
}
#if os(visionOS)
return .systemAction
return .systemAction
#else
return safariManager.open(url)
return safariManager.open(url)
#endif
#else
return .systemAction
#endif
}
}
#if !os(visionOS)
#if !os(visionOS)
.background {
WindowReader { window in
safariManager.windowScene = window.windowScene
}
}
#endif
#endif
}
}
#if !os(visionOS)
@MainActor
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
var windowScene: UIWindowScene?
let viewController: UIViewController = .init()
var window: UIWindow?
@MainActor
func open(_ url: URL) -> OpenURLAction.Result {
guard let windowScene else { return .systemAction }
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
var windowScene: UIWindowScene?
let viewController: UIViewController = .init()
var window: UIWindow?
window = setupWindow(windowScene: windowScene)
@MainActor
func open(_ url: URL) -> OpenURLAction.Result {
guard let windowScene else { return .systemAction }
let configuration = SFSafariViewController.Configuration()
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
window = setupWindow(windowScene: windowScene)
let safari = SFSafariViewController(url: url, configuration: configuration)
safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor)
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
safari.delegate = self
let configuration = SFSafariViewController.Configuration()
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
DispatchQueue.main.async { [weak self] in
self?.viewController.present(safari, animated: true)
let safari = SFSafariViewController(url: url, configuration: configuration)
safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor)
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
safari.delegate = self
DispatchQueue.main.async { [weak self] in
self?.viewController.present(safari, animated: true)
}
return .handled
}
return .handled
}
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
let window = window ?? UIWindow(windowScene: windowScene)
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
let window = window ?? UIWindow(windowScene: windowScene)
window.rootViewController = viewController
window.makeKeyAndVisible()
window.rootViewController = viewController
window.makeKeyAndVisible()
switch Theme.shared.selectedScheme {
case .dark:
window.overrideUserInterfaceStyle = .dark
case .light:
window.overrideUserInterfaceStyle = .light
}
switch Theme.shared.selectedScheme {
case .dark:
window.overrideUserInterfaceStyle = .dark
case .light:
window.overrideUserInterfaceStyle = .light
self.window = window
return window
}
self.window = window
return window
}
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
Task { @MainActor in
window?.resignKey()
window?.isHidden = false
window = nil
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
Task { @MainActor in
window?.resignKey()
window?.isHidden = false
window = nil
}
}
}
}
#endif
private struct WindowReader: UIViewRepresentable {

View file

@ -17,12 +17,12 @@ struct SideBarView<Content: View>: View {
@Environment(StreamWatcher.self) private var watcher
@Environment(UserPreferences.self) private var userPreferences
@Environment(RouterPath.self) private var routerPath
@Binding var selectedTab: Tab
@Binding var popToRootTab: Tab
var tabs: [Tab]
@ViewBuilder var content: () -> Content
@State private var sidebarTabs = SidebarTabs.shared
private func badgeFor(tab: Tab) -> Int {
@ -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,18 +185,25 @@ 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)
Divider().edgesIgnoringSafeArea(.all)
}
content()
}
@ -196,6 +233,7 @@ private struct SideBarIcon: View {
self.isHovered = isHovered
}
}
.frame(width: 50, height: 40)
}
}

View file

@ -1,29 +1,23 @@
import SwiftUI
import Env
import AppAccount
import DesignSystem
import Env
import SwiftUI
@MainActor
struct NavigationSheet<Content: View>: View {
@Environment(\.dismiss) private var dismiss
var content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
NavigationStack {
content()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle")
}
}
CloseToolbarItem()
}
}
}

View file

@ -1,27 +1,27 @@
import SwiftUI
import Env
import AppAccount
import DesignSystem
import Env
import Network
import SwiftUI
@MainActor
struct NavigationTab<Content: View>: View {
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
@Environment(AppAccountsManager.self) private var appAccount
@Environment(CurrentAccount.self) private var currentAccount
@Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme
@Environment(Client.self) private var client
var content: () -> Content
@State private var routerPath = RouterPath()
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
NavigationStack(path: $routerPath.path) {
content()

View file

@ -21,7 +21,7 @@ struct NotificationsTab: View {
@Environment(PushNotificationsService.self) private var pushNotificationsService
@State private var routerPath = RouterPath()
@State private var scrollToTopSignal: Int = 0
@Binding var selectedTab: Tab
@Binding var popToRootTab: Tab
@ -60,9 +60,9 @@ struct NotificationsTab: View {
}
}
}
.onChange(of: selectedTab, { _, newValue in
.onChange(of: selectedTab) { _, _ in
clearNotifications()
})
}
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
if let newValue, let type = newValue.notification.supportedType {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {

View file

@ -27,27 +27,27 @@ struct AboutView: View {
var body: some View {
List {
Section {
#if !targetEnvironment(macCatalyst)
HStack {
Spacer()
Image(uiImage: .init(named: "AppIconAlternate0")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate4")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate17")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate23")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Spacer()
}
#if !targetEnvironment(macCatalyst) && !os(visionOS)
HStack {
Spacer()
Image(uiImage: .init(named: "AppIconAlternate0")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate4")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate17")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate23")!)
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Spacer()
}
#endif
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
Label("settings.support.privacy-policy", systemImage: "lock")
@ -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)
@ -107,14 +107,14 @@ struct AboutView: View {
}
.listStyle(.insetGrouped)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.navigationTitle(Text("settings.about.title"))
.navigationBarTitleDisplayMode(.large)
.environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url)
})
.navigationTitle(Text("settings.about.title"))
.navigationBarTitleDisplayMode(.large)
.environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url)
})
}
@ViewBuilder

View file

@ -17,9 +17,8 @@ struct AccountSettingsView: View {
@Environment(Theme.self) private var theme
@Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@State private var isEditingAccount: Bool = false
@State private var isEditingFilters: Bool = false
@State private var cachedPostsCount: Int = 0
@State private var timelineCache = TimelineCache()
@ -30,7 +29,7 @@ struct AccountSettingsView: View {
Form {
Section {
Button {
isEditingAccount = true
routerPath.presentedSheet = .accountEditInfo
} label: {
Label("account.action.edit-info", systemImage: "pencil")
.frame(maxWidth: .infinity, alignment: .leading)
@ -40,7 +39,7 @@ struct AccountSettingsView: View {
if currentInstance.isFiltersSupported {
Button {
isEditingFilters = true
routerPath.presentedSheet = .accountFiltersList
} label: {
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
.frame(maxWidth: .infinity, alignment: .leading)
@ -96,12 +95,6 @@ struct AccountSettingsView: View {
}
.listRowBackground(theme.primaryBackgroundColor)
}
.sheet(isPresented: $isEditingAccount, content: {
EditAccountView()
})
.sheet(isPresented: $isEditingFilters, content: {
FiltersListView()
})
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
@ -116,8 +109,8 @@ struct AccountSettingsView: View {
}
.navigationTitle(account.safeDisplayName)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
}

View file

@ -82,75 +82,75 @@ struct AddAccountView: View {
.navigationTitle("account.add.navigation-title")
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
#endif
.toolbar {
if !appAccountsManager.availableAccounts.isEmpty {
CancelToolbarItem()
}
}
.onAppear {
isInstanceURLFieldFocused = true
Task {
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
.toolbar {
if !appAccountsManager.availableAccounts.isEmpty {
CancelToolbarItem()
}
}
isSigninIn = false
}
.onChange(of: instanceName) {
searchingTask.cancel()
searchingTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
}
}
getInstanceDetailTask.cancel()
getInstanceDetailTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
do {
// bare bones preflight for domain validity
let instanceDetailClient = Client(server: sanitizedName)
if
instanceDetailClient.server.contains("."),
instanceDetailClient.server.last != "."
{
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
withAnimation {
self.instance = instance
instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
}
instanceFetchError = nil
} else {
instance = nil
instanceFetchError = nil
.onAppear {
isInstanceURLFieldFocused = true
Task {
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
}
}
isSigninIn = false
}
.onChange(of: instanceName) {
searchingTask.cancel()
searchingTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
}
}
getInstanceDetailTask.cancel()
getInstanceDetailTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
do {
// bare bones preflight for domain validity
let instanceDetailClient = Client(server: sanitizedName)
if
instanceDetailClient.server.contains("."),
instanceDetailClient.server.last != "."
{
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
withAnimation {
self.instance = instance
instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
}
instanceFetchError = nil
} else {
instance = nil
instanceFetchError = nil
}
} catch _ as DecodingError {
instance = nil
instanceFetchError = "account.add.error.instance-not-supported"
} catch {
instance = nil
}
} catch _ as DecodingError {
instance = nil
instanceFetchError = "account.add.error.instance-not-supported"
} catch {
instance = nil
}
}
}
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
isSigninIn = false
default:
break
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
isSigninIn = false
default:
break
}
}
}
}
}
@ -214,9 +214,9 @@ struct AddAccountView: View {
.foregroundColor(.primary)
Spacer()
(Text("instance.list.users-\(formatAsNumber(instance.users))")
+ Text("")
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
.foregroundStyle(theme.tintColor)
+ Text("")
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
.foregroundStyle(theme.tintColor)
}
.padding(.bottom, 5)
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
@ -263,7 +263,7 @@ struct AddAccountView: View {
.redacted(reason: .placeholder)
.allowsHitTesting(false)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
@ -302,13 +302,3 @@ struct AddAccountView: View {
}
}
}
struct SafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context _: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
SFSafariViewController(url: url)
}
func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext<SafariView>) {}
}

View file

@ -5,14 +5,14 @@ import Models
import Network
import NukeUI
import SwiftUI
import UserNotifications
import Timeline
import UserNotifications
@MainActor
struct ContentSettingsView: View {
@Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme
@State private var contentFilter = TimelineContentFilter.shared
var body: some View {
@ -41,7 +41,7 @@ struct ContentSettingsView: View {
}
}
}
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
@ -113,7 +113,7 @@ struct ContentSettingsView: View {
Text("settings.content.default-sensitive")
}
.disabled(userPreferences.useInstanceContentSettings)
Toggle(isOn: $userPreferences.appRequireAltText) {
Text("settings.content.require-alt-text")
}
@ -121,7 +121,7 @@ struct ContentSettingsView: View {
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
Section("timeline.content-filter.title") {
Toggle(isOn: $contentFilter.showBoosts) {
Label("timeline.filter.show-boosts", image: "Rocket")
@ -142,8 +142,8 @@ struct ContentSettingsView: View {
}
.navigationTitle("settings.content.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
}

View file

@ -7,6 +7,7 @@ import Observation
import StatusKit
import SwiftUI
@MainActor
@Observable class DisplaySettingsLocalValues {
var tintColor = Theme.shared.tintColor
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
@ -36,11 +37,11 @@ struct DisplaySettingsView: View {
ZStack(alignment: .top) {
Form {
#if !os(visionOS)
StatusRowView(viewModel: previewStatusViewModel)
.allowsHitTesting(false)
.opacity(0)
.hidden()
themeSection
StatusRowView(viewModel: previewStatusViewModel)
.allowsHitTesting(false)
.opacity(0)
.hidden()
themeSection
#endif
fontSection
layoutSection
@ -49,35 +50,35 @@ struct DisplaySettingsView: View {
}
.navigationTitle("settings.display.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.task(id: localValues.tintColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.tintColor = localValues.tintColor
}
.task(id: localValues.primaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.primaryBackgroundColor = localValues.primaryBackgroundColor
}
.task(id: localValues.secondaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
}
.task(id: localValues.labelColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.labelColor = localValues.labelColor
}
.task(id: localValues.lineSpacing) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.lineSpacing = localValues.lineSpacing
}
.task(id: localValues.fontSizeScale) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.fontSizeScale = localValues.fontSizeScale
}
.task(id: localValues.tintColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.tintColor = localValues.tintColor
}
.task(id: localValues.primaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.primaryBackgroundColor = localValues.primaryBackgroundColor
}
.task(id: localValues.secondaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
}
.task(id: localValues.labelColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.labelColor = localValues.labelColor
}
.task(id: localValues.lineSpacing) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.lineSpacing = localValues.lineSpacing
}
.task(id: localValues.fontSizeScale) {
do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.fontSizeScale = localValues.fontSizeScale
}
#if !os(visionOS)
examplePost
examplePost
#endif
}
}

View file

@ -23,8 +23,8 @@ struct HapticSettingsView: View {
}
.navigationTitle("settings.haptic.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
}

View file

@ -14,8 +14,8 @@ struct InstanceInfoView: View {
}
.navigationTitle("instance.info.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
}

View file

@ -111,12 +111,12 @@ struct PushNotificationsView: View {
}
.navigationTitle("settings.push.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.task {
await subscription.fetchSubscription()
}
.task {
await subscription.fetchSubscription()
}
}
private func updateSubscription() {

View file

@ -1,17 +1,17 @@
import SwiftUI
import SwiftData
import Models
import Env
import DesignSystem
import Env
import Models
import SwiftData
import SwiftUI
struct RecenTagsSettingView: View {
@Environment(\.modelContext) private var context
@Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme
@Query(sort: \RecentTag.lastUse, order: .reverse) var tags: [RecentTag]
var body: some View {
Form {
ForEach(tags) { tag in
@ -35,10 +35,10 @@ struct RecenTagsSettingView: View {
.navigationTitle("settings.general.recent-tags")
.scrollContentBackground(.hidden)
#if !os(visionOS)
.background(theme.secondaryBackgroundColor)
.background(theme.secondaryBackgroundColor)
#endif
.toolbar {
EditButton()
}
.toolbar {
EditButton()
}
}
}

View file

@ -1,17 +1,17 @@
import SwiftUI
import SwiftData
import Models
import Env
import DesignSystem
import Env
import Models
import SwiftData
import SwiftUI
struct RemoteTimelinesSettingView: View {
@Environment(\.modelContext) private var context
@Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
var body: some View {
Form {
ForEach(localTimelines) { timeline in
@ -36,10 +36,10 @@ struct RemoteTimelinesSettingView: View {
.navigationTitle("settings.general.remote-timelines")
.scrollContentBackground(.hidden)
#if !os(visionOS)
.background(theme.secondaryBackgroundColor)
.background(theme.secondaryBackgroundColor)
#endif
.toolbar {
EditButton()
}
.toolbar {
EditButton()
}
}
}

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 {
@ -43,27 +45,53 @@ struct SettingsTabs: View {
}
.scrollContentBackground(.hidden)
#if !os(visionOS)
.background(theme.secondaryBackgroundColor)
.background(theme.secondaryBackgroundColor)
#endif
.navigationTitle(Text("settings.title"))
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.toolbar {
if isModal {
ToolbarItem {
Button {
dismiss()
} label: {
Text("action.done").bold()
.navigationTitle(Text("settings.title"))
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.toolbar {
if isModal {
ToolbarItem {
Button {
dismiss()
} label: {
Text("action.done").bold()
}
}
}
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
SecondaryColumnToolbarItem()
}
}
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
SecondaryColumnToolbarItem()
.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()
}
}
}
.withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
}
.onAppear {
routerPath.client = client

View file

@ -2,10 +2,11 @@ import DesignSystem
import Env
import SwiftUI
@MainActor
struct SidebarEntriesSettingsView: View {
@Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var userPreferences
@State private var sidebarTabs = SidebarTabs.shared
var body: some View {
@ -28,11 +29,11 @@ struct SidebarEntriesSettingsView: View {
.environment(\.editMode, .constant(.active))
.navigationTitle("settings.general.sidebarEntries")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
func move(from source: IndexSet, to destination: Int) {
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
}

View file

@ -69,24 +69,24 @@ struct SupportAppView: View {
}
.navigationTitle("settings.support.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
}, message: {
Text("settings.support.alert.message")
})
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
}, message: {
Text("settings.support.alert.error.message")
})
.onAppear {
loadingProducts = true
fetchStoreProducts()
refreshUserInfo()
}
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
}, message: {
Text("settings.support.alert.message")
})
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
}, message: {
Text("settings.support.alert.error.message")
})
.onAppear {
loadingProducts = true
fetchStoreProducts()
refreshUserInfo()
}
}
private func purchase(product: StoreProduct) async {

View file

@ -49,7 +49,7 @@ struct SwipeActionsSettingsView: View {
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
Section {
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in
@ -70,8 +70,8 @@ struct SwipeActionsSettingsView: View {
}
.navigationTitle("settings.swipeactions.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}

View file

@ -6,7 +6,7 @@ import SwiftUI
struct TabbarEntriesSettingsView: View {
@Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var userPreferences
@State private var tabs = iOSTabs.shared
var body: some View {
@ -42,7 +42,7 @@ struct TabbarEntriesSettingsView: View {
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
Section {
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
}
@ -52,8 +52,8 @@ struct TabbarEntriesSettingsView: View {
}
.navigationTitle("settings.general.tabbarEntries")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
}

View file

@ -1,17 +1,17 @@
import SwiftUI
import SwiftData
import Models
import Env
import DesignSystem
import Env
import Models
import SwiftData
import SwiftUI
struct TagsGroupSettingView: View {
@Environment(\.modelContext) private var context
@Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
var body: some View {
Form {
ForEach(tagGroups) { group in
@ -41,10 +41,10 @@ struct TagsGroupSettingView: View {
.navigationTitle("timeline.filter.tag-groups")
.scrollContentBackground(.hidden)
#if !os(visionOS)
.background(theme.secondaryBackgroundColor)
.background(theme.secondaryBackgroundColor)
#endif
.toolbar {
EditButton()
}
.toolbar {
EditButton()
}
}
}

View file

@ -11,20 +11,17 @@ 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
if apiKey.isEmpty {
Section {
Link(destination: URL(string: "https://www.deepl.com/pro-api")!) {
@ -37,30 +34,51 @@ struct TranslationSettingsView: View {
#endif
}
}
backgroundAPIKey
autoDetectSection
}
.navigationTitle("settings.translation.navigation-title")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.onChange(of: apiKey) {
writeNewValue()
}
.onAppear(perform: updatePrefs)
.onChange(of: apiKey) {
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
@ -15,6 +16,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
case post
case followedTags
case lists
case links
nonisolated var id: Int {
rawValue
@ -23,7 +25,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
static func loggedOutTab() -> [Tab] {
[.timeline, .settings]
}
static func visionOSTab() -> [Tab] {
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
}
@ -67,8 +69,10 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
NavigationTab {
ListsListView()
}
case .links:
NavigationTab { TrendingLinksListView(cards: []) }
case .post:
VStack { }
VStack {}
case .other:
EmptyView()
}
@ -76,40 +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:
"explore.section.trending.links"
case .other:
EmptyView()
""
}
}
@ -145,19 +156,22 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
"tag"
case .lists:
"list.bullet"
case .links:
"newspaper"
case .other:
""
}
}
}
@MainActor
@Observable
class SidebarTabs {
struct SidedebarTab: Hashable, Codable {
let tab: Tab
var enabled: Bool
}
class Storage {
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [
.init(tab: .timeline, enabled: true),
@ -172,36 +186,37 @@ class SidebarTabs {
.init(tab: .favorites, enabled: true),
.init(tab: .followedTags, enabled: true),
.init(tab: .lists, enabled: true),
.init(tab: .settings, enabled: true),
.init(tab: .profile, enabled: true),
]
}
private let storage = Storage()
public static let shared = SidebarTabs()
var tabs: [SidedebarTab] {
didSet {
storage.tabs = tabs
}
}
func isEnabled(_ tab: Tab) -> Bool {
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
}
private init() {
tabs = storage.tabs
}
}
@MainActor
@Observable
class iOSTabs {
enum TabEntries: String {
case first, second, third, fourth, fifth
}
class Storage {
@AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline
@AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications
@ -209,44 +224,44 @@ class iOSTabs {
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile
}
private let storage = Storage()
public static let shared = iOSTabs()
var tabs: [Tab] {
[firstTab, secondTab, thirdTab, fourthTab, fifthTab]
}
var firstTab: Tab {
didSet {
storage.firstTab = firstTab
}
}
var secondTab: Tab {
didSet {
storage.secondTab = secondTab
}
}
var thirdTab: Tab {
didSet {
storage.thirdTab = thirdTab
}
}
var fourthTab: Tab {
didSet {
storage.fourthTab = fourthTab
}
}
var fifthTab: Tab {
didSet {
storage.fifthTab = fifthTab
}
}
private init() {
firstTab = storage.firstTab
secondTab = storage.secondTab

View file

@ -61,20 +61,20 @@ struct EditTagGroupView: View {
)
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.interactively)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.interactively)
#endif
.toolbar {
CancelToolbarItem()
ToolbarItem(placement: .navigationBarTrailing) {
Button("action.save", action: { save() })
.disabled(!tagGroup.isValid)
.toolbar {
CancelToolbarItem()
ToolbarItem(placement: .navigationBarTrailing) {
Button("action.save", action: { save() })
.disabled(!tagGroup.isValid)
}
}
.onAppear {
focusedField = .title
}
}
.onAppear {
focusedField = .title
}
}
}

View file

@ -52,29 +52,29 @@ struct AddRemoteTimelineView: View {
.navigationTitle("timeline.add-remote.title")
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
#endif
.toolbar {
CancelToolbarItem()
}
.onChange(of: instanceName) { _, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
Task {
let client = Client(server: newValue)
instance = try? await client.get(endpoint: Instances.instance)
.toolbar {
CancelToolbarItem()
}
}
.onAppear {
isInstanceURLFieldFocused = true
let client = InstanceSocialClient()
Task {
instances = await client.fetchInstances(keyword: instanceName)
.onChange(of: instanceName) { _, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
Task {
let client = Client(server: newValue)
instance = try? await client.get(endpoint: Instances.instance)
}
}
.onAppear {
isInstanceURLFieldFocused = true
let client = InstanceSocialClient()
Task {
instances = await client.fetchInstances(keyword: instanceName)
}
}
}
}
}

View file

@ -95,7 +95,7 @@ struct TimelineTab: View {
}
switch newValue {
case let .tagGroup(title, _, _):
if let group = tagGroups.first(where: { $0.title == title}) {
if let group = tagGroups.first(where: { $0.title == title }) {
selectedTagGroup = group
}
default:
@ -185,7 +185,7 @@ struct TimelineTab: View {
}
}
}
@ViewBuilder
private var headerGroup: some View {
ControlGroup {
@ -209,10 +209,10 @@ struct TimelineTab: View {
pinButton
}
}
@ViewBuilder
private var pinButton: some View {
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id})
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id })
Button {
withAnimation {
if let index {
@ -222,14 +222,14 @@ struct TimelineTab: View {
}
}
} label: {
if index != nil {
if index != nil {
Label("status.action.unpin", systemImage: "pin.slash")
} else {
Label("status.action.pin", systemImage: "pin")
}
}
}
private var timelineFiltersButtons: some View {
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
Button {
@ -239,7 +239,7 @@ struct TimelineTab: View {
}
}
}
@ViewBuilder
private var listsFiltersButons: some View {
Menu("timeline.filter.lists") {
@ -257,7 +257,7 @@ struct TimelineTab: View {
}
}
}
@ViewBuilder
private var tagsFiltersButtons: some View {
if !currentAccount.tags.isEmpty {
@ -272,7 +272,7 @@ struct TimelineTab: View {
}
}
}
private var localTimelinesFiltersButtons: some View {
Menu("timeline.filter.local") {
ForEach(localTimelines) { remoteLocal in
@ -291,7 +291,7 @@ struct TimelineTab: View {
}
}
}
private var tagGroupsFiltersButtons: some View {
Menu("timeline.filter.tag-groups") {
ForEach(tagGroups) { group in
@ -312,7 +312,7 @@ struct TimelineTab: View {
}
}
}
private var contentFilterButton: some View {
Button(action: {
routerPath.presentedSheet = .timelineContentFilter

View file

@ -1,23 +1,39 @@
import SwiftUI
import Env
import AppAccount
import DesignSystem
import Env
import SwiftUI
@MainActor
struct ToolbarTab: ToolbarContent {
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(UserPreferences.self) private var userPreferences
@Binding var routerPath: RouterPath
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 ||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) {
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
{
ToolbarItem(placement: .navigationBarLeading) {
AppAccountsSelectorView(routerPath: routerPath)
}

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

@ -24,7 +24,7 @@ extension NotificationService {
var _plaintext: Data?
do {
_plaintext = try AES.GCM.open(sealedBox, using: key)
} catch { }
} catch {}
guard let plaintext = _plaintext else {
return nil
}

View file

@ -2,11 +2,11 @@ import Account
import AppAccount
import DesignSystem
import Env
import Models
import Network
import StatusKit
import SwiftUI
import UIKit
import Models
class ShareViewController: UIViewController {
override func viewDidLoad() {

View file

@ -31,7 +31,7 @@ let package = Package(
.product(name: "Models", package: "Models"),
.product(name: "StatusKit", package: "StatusKit"),
.product(name: "Env", package: "Env"),
.product(name: "ButtonKit", package: "ButtonKit")
.product(name: "ButtonKit", package: "ButtonKit"),
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),

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
@ -28,16 +29,16 @@ public struct AccountDetailContextMenu: View {
Label("account.action.message", systemImage: "tray.full")
}
#if !targetEnvironment(macCatalyst)
#if !targetEnvironment(macCatalyst)
Divider()
#endif
#endif
if viewModel.relationship?.blocking == true {
Button {
Task {
do {
viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id))
} catch { }
} catch {}
}
} label: {
Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark")
@ -55,7 +56,7 @@ public struct AccountDetailContextMenu: View {
Task {
do {
viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id))
} catch { }
} catch {}
}
} label: {
Label("account.action.unmute", systemImage: "speaker")
@ -67,7 +68,7 @@ public struct AccountDetailContextMenu: View {
Task {
do {
viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue)))
} catch { }
} catch {}
}
}
}
@ -86,7 +87,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: false,
reblogs: relationship.showingReblogs))
} catch { }
} catch {}
}
} label: {
Label("account.action.notify-disable", systemImage: "bell.fill")
@ -98,7 +99,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: true,
reblogs: relationship.showingReblogs))
} catch { }
} catch {}
}
} label: {
Label("account.action.notify-enable", systemImage: "bell")
@ -111,7 +112,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: relationship.notifying,
reblogs: false))
} catch { }
} catch {}
}
} label: {
Label("account.action.reboosts-hide", image: "Rocket.Fill")
@ -123,7 +124,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: relationship.notifying,
reblogs: true))
} catch { }
} catch {}
}
} label: {
Label("account.action.reboosts-show", image: "Rocket")
@ -131,20 +132,20 @@ public struct AccountDetailContextMenu: View {
}
}
#if !targetEnvironment(macCatalyst)
#if !targetEnvironment(macCatalyst)
Divider()
#endif
#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 {
@ -164,7 +165,7 @@ public struct AccountDetailContextMenu: View {
}
#if !targetEnvironment(macCatalyst)
Divider()
Divider()
#endif
}
}

View file

@ -207,6 +207,7 @@ struct AccountDetailHeaderView: View {
.foregroundStyle(.secondary)
.textSelection(.enabled)
.accessibilityRespondsToUserInteraction(false)
movedToView
joinedAtView
}
.accessibilityElement(children: .contain)
@ -311,6 +312,17 @@ struct AccountDetailHeaderView: View {
}
}
@ViewBuilder
private var movedToView: some View {
if let movedTo = viewModel.account?.moved {
Button("account.movedto.redirect-\("@\(movedTo.acct)")") {
routerPath.navigate(to: .accountDetailWithAccount(account: movedTo))
}
.font(.scaledCallout)
.foregroundColor(.accentColor)
}
}
@ViewBuilder
private func makeNoteView(_ note: String) -> some View {
VStack(alignment: .leading, spacing: 4) {
@ -372,15 +384,15 @@ struct AccountDetailHeaderView: View {
.accessibilityElement(children: .contain)
.accessibilityLabel("accessibility.tabs.profile.fields.container.label")
#if os(visionOS)
.background(Material.thick)
.background(Material.thick)
#else
.background(theme.secondaryBackgroundColor)
.background(theme.secondaryBackgroundColor)
#endif
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(.gray.opacity(0.35), lineWidth: 1)
)
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(.gray.opacity(0.35), lineWidth: 1)
)
}
}
}

View file

@ -23,11 +23,9 @@ public struct AccountDetailView: View {
@State private var viewModel: AccountDetailViewModel
@State private var isCurrentUser: Bool = false
@State private var showBlockConfirmation: Bool = false
@State private var isEditingAccount: Bool = false
@State private var isEditingFilters: Bool = false
@State private var isEditingRelationshipNote: Bool = false
@State private var showTranslateView: Bool = false
@State private var displayTitle: Bool = false
@Binding var scrollToTopSignal: Int
@ -88,14 +86,14 @@ public struct AccountDetailView: View {
.environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.onChange(of: scrollToTopSignal) {
withAnimation {
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
.onChange(of: scrollToTopSignal) {
withAnimation {
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
}
}
}
}
.onAppear {
guard reasons != .placeholder else { return }
@ -136,20 +134,14 @@ public struct AccountDetailView: View {
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
}
}
.onChange(of: isEditingAccount) { _, newValue in
if !newValue {
.onChange(of: routerPath.presentedSheet) { oldValue, newValue in
if oldValue == .accountEditInfo || newValue == .accountEditInfo {
Task {
await viewModel.fetchAccount()
await preferences.refreshServerPreferences()
}
}
}
.sheet(isPresented: $isEditingAccount, content: {
EditAccountView()
})
.sheet(isPresented: $isEditingFilters, content: {
FiltersListView()
})
.sheet(isPresented: $isEditingRelationshipNote, content: {
EditRelationshipNoteView(accountDetailViewModel: viewModel)
})
@ -220,7 +212,6 @@ public struct AccountDetailView: View {
AvatarView(account.avatar, config: .badge)
.padding(.leading, -4)
.accessibilityLabel(account.safeDisplayName)
}
.accessibilityAddTraits(.isImage)
.buttonStyle(.plain)
@ -247,18 +238,18 @@ public struct AccountDetailView: View {
bottom: 0,
trailing: .layoutPadding))
.listRowSeparator(.hidden)
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
#endif
ForEach(viewModel.pinned) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
}
Rectangle()
#if os(visionOS)
#if os(visionOS)
.fill(Color.clear)
#else
#else
.fill(theme.secondaryBackgroundColor)
#endif
#endif
.frame(height: 12)
.listRowInsets(.init())
.listRowSeparator(.hidden)
@ -288,7 +279,6 @@ public struct AccountDetailView: View {
routerPath.presentedSheet = .mentionStatusEditor(account: account,
visibility: preferences.postVisibility)
#endif
}
} label: {
Image(systemName: "arrowshape.turn.up.left")
@ -296,7 +286,9 @@ public struct AccountDetailView: View {
}
Menu {
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: viewModel)
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
showTranslateView: $showTranslateView,
viewModel: viewModel)
if !viewModel.isCurrentUser {
Button {
@ -308,7 +300,7 @@ public struct AccountDetailView: View {
if isCurrentUser {
Button {
isEditingAccount = true
routerPath.presentedSheet = .accountEditInfo
} label: {
Label("account.action.edit-info", systemImage: "pencil")
}
@ -323,7 +315,7 @@ public struct AccountDetailView: View {
if currentInstance.isFiltersSupported {
Button {
isEditingFilters = true
routerPath.presentedSheet = .accountFiltersList
} label: {
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
}
@ -338,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)
@ -370,24 +376,28 @@ public struct AccountDetailView: View {
Task {
do {
viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id))
} catch { }
} catch {}
}
}
}
} message: {
Text("account.action.block-user-confirmation")
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account?.note.asRawText ?? "")
#endif
}
}
}
extension View {
@MainActor
func applyAccountDetailsRowStyle(theme: Theme) -> some View {
listRowInsets(.init())
.listRowSeparator(.hidden)
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
#endif
}
}

View file

@ -84,7 +84,7 @@ import SwiftUI
private var tabTask: Task<Void, Never>?
private(set) var statuses: [Status] = []
var boosts: [Status] = []
/// When coming from a URL like a mention tap in a status.
@ -151,7 +151,7 @@ import SwiftUI
self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
}
func fetchNewestStatuses(pullToRefresh: Bool) async {
func fetchNewestStatuses(pullToRefresh _: Bool) async {
guard let client else { return }
do {
statusesState = .loading
@ -166,7 +166,7 @@ import SwiftUI
pinned: nil))
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
if selectedTab == .boosts {
boosts = statuses.filter{ $0.reblog != nil }
boosts = statuses.filter { $0.reblog != nil }
}
if selectedTab == .statuses {
pinned =
@ -191,55 +191,46 @@ import SwiftUI
}
}
func fetchNextPage() async {
func fetchNextPage() async throws {
guard let client else { return }
do {
switch selectedTab {
case .statuses, .replies, .boosts, .media:
guard let lastId = statuses.last?.id else { return }
if selectedTab == .boosts {
statusesState = .display(statuses: boosts, nextPageState: .loadingNextPage)
} else {
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
}
let newStatuses: [Status] =
try await client.get(endpoint: Accounts.statuses(id: accountId,
sinceId: lastId,
tag: nil,
onlyMedia: selectedTab == .media,
excludeReplies: selectedTab != .replies,
excludeReblogs: selectedTab != .boosts,
pinned: nil))
statuses.append(contentsOf: newStatuses)
if selectedTab == .boosts {
let newBoosts = statuses.filter{ $0.reblog != nil }
self.boosts.append(contentsOf: newBoosts)
}
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
if selectedTab == .boosts {
statusesState = .display(statuses: boosts,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
} else {
statusesState = .display(statuses: statuses,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
}
case .favorites:
guard let nextPageId = favoritesNextPage?.maxId else { return }
let newFavorites: [Status]
(newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId))
favorites.append(contentsOf: newFavorites)
StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client)
statusesState = .display(statuses: favorites, nextPageState: .hasNextPage)
case .bookmarks:
guard let nextPageId = bookmarksNextPage?.maxId else { return }
let newBookmarks: [Status]
(newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId))
StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client)
bookmarks.append(contentsOf: newBookmarks)
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
switch selectedTab {
case .statuses, .replies, .boosts, .media:
guard let lastId = statuses.last?.id else { return }
let newStatuses: [Status] =
try await client.get(endpoint: Accounts.statuses(id: accountId,
sinceId: lastId,
tag: nil,
onlyMedia: selectedTab == .media,
excludeReplies: selectedTab != .replies,
excludeReblogs: selectedTab != .boosts,
pinned: nil))
statuses.append(contentsOf: newStatuses)
if selectedTab == .boosts {
let newBoosts = statuses.filter { $0.reblog != nil }
boosts.append(contentsOf: newBoosts)
}
} catch {
statusesState = .error(error: error)
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
if selectedTab == .boosts {
statusesState = .display(statuses: boosts,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
} else {
statusesState = .display(statuses: statuses,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
}
case .favorites:
guard let nextPageId = favoritesNextPage?.maxId else { return }
let newFavorites: [Status]
(newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId))
favorites.append(contentsOf: newFavorites)
StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client)
statusesState = .display(statuses: favorites, nextPageState: .hasNextPage)
case .bookmarks:
guard let nextPageId = bookmarksNextPage?.maxId else { return }
let newBookmarks: [Status]
(newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId))
StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client)
bookmarks.append(contentsOf: newBookmarks)
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
}
}
@ -260,9 +251,13 @@ import SwiftUI
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
if let event = event as? StreamEventUpdate {
if event.status.account.id == currentAccount.account?.id, selectedTab == .statuses {
statuses.insert(event.status, at: 0)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
if event.status.account.id == currentAccount.account?.id {
if (event.status.inReplyToId == nil && selectedTab == .statuses) ||
(event.status.inReplyToId != nil && selectedTab == .replies)
{
statuses.insert(event.status, at: 0)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
}
}
} else if let event = event as? StreamEventDelete {
statuses.removeAll(where: { $0.id == event.status })
@ -278,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

@ -45,16 +45,19 @@ public struct AccountsListView: View {
await viewModel.fetch()
}
}
@ViewBuilder
private var listView: some View {
if currentAccount.account?.id == viewModel.accountId {
searchableList
} else {
standardList
.refreshable {
await viewModel.fetch()
}
}
}
private var searchableList: some View {
List {
listContent
@ -74,13 +77,13 @@ public struct AccountsListView: View {
}
}
}
private var standardList: some View {
List {
listContent
}
}
@ViewBuilder
private var listContent: some View {
switch viewModel.state {
@ -89,9 +92,9 @@ public struct AccountsListView: View {
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
.redacted(reason: .placeholder)
.allowsHitTesting(false)
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
#endif
}
case let .display(accounts, relationships, nextPageState):
if case .followers = viewModel.mode,
@ -121,51 +124,52 @@ 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:
loadingRow
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
.onAppear {
Task {
await viewModel.fetchNextPage()
}
}
NextPageView {
try await viewModel.fetchNextPage()
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
case .loadingNextPage:
loadingRow
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
case .none:
EmptyView()
}
case let .error(error):
Text(error.localizedDescription)
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
}
private var loadingRow: some View {
HStack {
Spacer()
ProgressView()
Spacer()
#endif
}
}
}
#Preview {
List {
AccountsListRow(viewModel: .init(account: .placeholder(),
relationShip: .placeholder()))
}
.listStyle(.plain)
.withPreviewsEnv()
.environment(Theme.shared)
}

View file

@ -1,13 +1,14 @@
import Models
import Network
import Observation
import SwiftUI
import OSLog
import SwiftUI
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"
}
}
}
@ -33,7 +38,7 @@ public enum AccountsListMode {
public enum State {
public enum PagingState {
case hasNextPage, loadingNextPage, none
case hasNextPage, none
}
case loading
@ -49,7 +54,7 @@ public enum AccountsListMode {
var state = State.loading
var totalCount: Int?
var accountId: String?
var searchQuery: String = ""
private var nextPageId: String?
@ -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:
@ -94,44 +105,45 @@ public enum AccountsListMode {
} catch {}
}
func fetchNextPage() async {
func fetchNextPage() async throws {
guard let client, let nextPageId else { return }
do {
state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage)
let newAccounts: [Account]
let link: LinkHandler?
switch mode {
case let .followers(accountId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
let newAccounts: [Account]
let link: LinkHandler?
switch mode {
case let .followers(accountId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
maxId: nextPageId))
case let .following(accountId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
maxId: nextPageId))
case let .rebloggedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
maxId: nextPageId))
case let .following(accountId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
case let .favoritedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
maxId: nextPageId))
case let .rebloggedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
maxId: nextPageId))
case let .favoritedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
maxId: nextPageId))
case .accountsList:
newAccounts = []
link = nil
}
accounts.append(contentsOf: newAccounts)
let newRelationships: [Relationship] =
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map(\.id)))
case .accountsList:
newAccounts = []
link = nil
relationships.append(contentsOf: newRelationships)
self.nextPageId = link?.maxId
state = .display(accounts: accounts,
relationships: relationships,
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
} catch {
let logger = Logger(subsystem: "com.icecubesapp", category: "UI")
logger.log(level: .info, "\(error.localizedDescription)")
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)))
relationships.append(contentsOf: newRelationships)
self.nextPageId = link?.maxId
state = .display(accounts: accounts,
relationships: relationships,
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
}
func search() async {
guard let client, !searchQuery.isEmpty else { return }
do {
@ -150,8 +162,6 @@ public enum AccountsListMode {
relationships: relationships,
nextPageState: .none)
}
} catch {
}
} catch {}
}
}

View file

@ -2,8 +2,8 @@ import DesignSystem
import Env
import Models
import Network
import SwiftUI
import NukeUI
import SwiftUI
@MainActor
public struct EditAccountView: View {
@ -13,8 +13,8 @@ public struct EditAccountView: View {
@Environment(UserPreferences.self) private var userPrefs
@State private var viewModel = EditAccountViewModel()
public init() { }
public init() {}
public var body: some View {
NavigationStack {
@ -31,24 +31,24 @@ public struct EditAccountView: View {
}
.environment(\.editMode, .constant(.active))
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
#endif
.navigationTitle("account.edit.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
toolbarContent
}
.alert("account.edit.error.save.title",
isPresented: $viewModel.saveError,
actions: {
Button("alert.button.ok", action: {})
}, message: { Text("account.edit.error.save.message") })
.task {
viewModel.client = client
await viewModel.fetchAccount()
}
.navigationTitle("account.edit.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
toolbarContent
}
.alert("account.edit.error.save.title",
isPresented: $viewModel.saveError,
actions: {
Button("alert.button.ok", action: {})
}, message: { Text("account.edit.error.save.message") })
.task {
viewModel.client = client
await viewModel.fetchAccount()
}
}
}
@ -64,7 +64,7 @@ public struct EditAccountView: View {
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
private var imagesSection: some View {
Section {
ZStack(alignment: .center) {

View file

@ -1,9 +1,9 @@
import Models
import Network
import Observation
import SwiftUI
import PhotosUI
import StatusKit
import SwiftUI
@MainActor
@Observable class EditAccountViewModel {
@ -30,22 +30,23 @@ import StatusKit
var fields: [FieldEditViewModel] = []
var avatar: URL?
var header: URL?
var isPhotoPickerPresented: Bool = false {
didSet {
if !isPhotoPickerPresented && mediaPickers.isEmpty {
if !isPhotoPickerPresented, mediaPickers.isEmpty {
isChangingAvatar = false
isChangingHeader = false
}
}
}
var isChangingAvatar: Bool = false
var isChangingHeader: Bool = false
var isLoading: Bool = true
var isSaving: Bool = false
var saveError: Bool = false
var mediaPickers: [PhotosPickerItem] = [] {
didSet {
if let item = mediaPickers.first {
@ -108,47 +109,47 @@ import StatusKit
saveError = true
}
}
private func uploadHeader(data: Data) async -> Bool {
guard let client else { return false }
do {
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
version: .v1,
method: "PATCH",
mimeType: "image/jpeg",
filename: "header",
data: data)
version: .v1,
method: "PATCH",
mimeType: "image/jpeg",
filename: "header",
data: data)
return response?.statusCode == 200
} catch {
return false
}
}
private func uploadAvatar(data: Data) async -> Bool {
guard let client else { return false }
do {
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
version: .v1,
method: "PATCH",
mimeType: "image/jpeg",
filename: "avatar",
data: data)
version: .v1,
method: "PATCH",
mimeType: "image/jpeg",
filename: "avatar",
data: data)
return response?.statusCode == 200
} catch {
return false
}
}
private func getItemImageData(item: PhotosPickerItem) async -> Data? {
guard let imageFile = try? await item.loadTransferable(type: StatusEditor.ImageFileTranseferable.self) else { return nil }
let compressor = StatusEditor.Compressor()
guard let compressedData = await compressor.compressImageFrom(url: imageFile.url),
let image = UIImage(data: compressedData),
let uploadData = try? await compressor.compressImageForUpload(image)
let image = UIImage(data: compressedData),
let uploadData = try? await compressor.compressImageForUpload(image)
else { return nil }
return uploadData
}
}

View file

@ -71,20 +71,20 @@ struct EditFilterView: View {
.navigationTitle(filter?.title ?? NSLocalizedString("filter.new", comment: ""))
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.scrollDismissesKeyboard(.interactively)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.scrollDismissesKeyboard(.interactively)
.background(theme.secondaryBackgroundColor)
#endif
.onAppear {
if filter == nil {
focusedField = .title
.onAppear {
if filter == nil {
focusedField = .title
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
saveButton
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
saveButton
}
}
}
}
private var expirySection: some View {

View file

@ -74,18 +74,18 @@ public struct FiltersListView: View {
.navigationTitle("filter.filters")
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.task {
do {
isLoading = true
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
isLoading = false
} catch {
isLoading = false
.task {
do {
isLoading = true
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
isLoading = false
} catch {
isLoading = false
}
}
}
}
}

View file

@ -4,8 +4,8 @@ import Foundation
import Models
import Network
import Observation
import SwiftUI
import OSLog
import SwiftUI
@MainActor
@Observable public class FollowButtonViewModel {

View file

@ -1,14 +1,14 @@
import DesignSystem
import Env
import Models
import SwiftUI
import Env
public struct ListsListView: View {
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Theme.self) private var theme
public init() {}
public var body: some View {
List {
ForEach(currentAccount.lists) { list in
@ -43,4 +43,3 @@ public struct ListsListView: View {
.navigationBarTitleDisplayMode(.inline)
}
}

View file

@ -1,50 +1,50 @@
import StatusKit
import Network
import SwiftUI
import DesignSystem
import Env
import Models
import DesignSystem
import Network
import StatusKit
import SwiftUI
@MainActor
public struct AccountStatusesListView: View {
@Environment(Theme.self) private var theme
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@State private var viewModel: AccountStatusesListViewModel
@State private var isLoaded = false
public init(mode: AccountStatusesListViewModel.Mode) {
_viewModel = .init(initialValue: .init(mode: mode))
}
public var body: some View {
List {
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
}
.listStyle(.plain)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline)
.refreshable {
await viewModel.fetchNewestStatuses(pullToRefresh: true)
}
.task {
guard !isLoaded else { return }
viewModel.client = client
await viewModel.fetchNewestStatuses(pullToRefresh: false)
isLoaded = true
}
.onChange(of: client.id) { _, _ in
isLoaded = false
viewModel.client = client
Task {
.navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline)
.refreshable {
await viewModel.fetchNewestStatuses(pullToRefresh: true)
}
.task {
guard !isLoaded else { return }
viewModel.client = client
await viewModel.fetchNewestStatuses(pullToRefresh: false)
isLoaded = true
}
}
.onChange(of: client.id) { _, _ in
isLoaded = false
viewModel.client = client
Task {
await viewModel.fetchNewestStatuses(pullToRefresh: false)
isLoaded = true
}
}
}
}

View file

@ -1,15 +1,15 @@
import SwiftUI
import Models
import StatusKit
import Network
import Env
import Models
import Network
import StatusKit
import SwiftUI
@MainActor
@Observable
public class AccountStatusesListViewModel: StatusesFetcher {
public enum Mode {
public enum Mode {
case bookmarks, favorites
var title: LocalizedStringKey {
switch self {
case .bookmarks:
@ -18,7 +18,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
"accessibility.tabs.profile.picker.favorites"
}
}
func endpoint(sinceId: String?) -> Endpoint {
switch self {
case .bookmarks:
@ -28,19 +28,19 @@ public class AccountStatusesListViewModel: StatusesFetcher {
}
}
}
let mode: Mode
public var statusesState: StatusesState = .loading
var statuses: [Status] = []
var nextPage: LinkHandler?
var client: Client?
init(mode: Mode) {
self.mode = mode
}
public func fetchNewestStatuses(pullToRefresh: Bool) async {
public func fetchNewestStatuses(pullToRefresh _: Bool) async {
guard let client else { return }
statusesState = .loading
do {
@ -52,26 +52,18 @@ public class AccountStatusesListViewModel: StatusesFetcher {
statusesState = .error(error: error)
}
}
public func fetchNextPage() async {
public func fetchNextPage() async throws {
guard let client, let nextId = nextPage?.maxId else { return }
var newStatuses: [Status] = []
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
statuses.append(contentsOf: newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
statusesState = .display(statuses: statuses,
nextPageState: .loadingNextPage)
do {
var newStatuses: [Status] = []
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
statuses.append(contentsOf: newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
statusesState = .display(statuses: statuses,
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
} catch { }
}
public func statusDidAppear(status: Status) {
}
public func statusDidDisappear(status: Status) {
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
}
public func statusDidAppear(status _: Status) {}
public func statusDidDisappear(status _: Status) {}
}

View file

@ -1,20 +1,20 @@
import DesignSystem
import Env
import Models
import SwiftUI
import Env
public struct FollowedTagsListView: View {
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Theme.self) private var theme
public init() {}
public var body: some View {
List(currentAccount.tags) { tag in
TagRowView(tag: tag)
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
#endif
.padding(.vertical, 4)
}
.task {
@ -32,4 +32,3 @@ public struct FollowedTagsListView: View {
.navigationBarTitleDisplayMode(.inline)
}
}

View file

@ -13,7 +13,7 @@ public struct AppAccountView: View {
@State var viewModel: AppAccountViewModel
@Binding var isParentPresented: Bool
public init(viewModel: AppAccountViewModel, isParentPresented: Binding<Bool>) {
self.viewModel = viewModel
_isParentPresented = isParentPresented

View file

@ -33,11 +33,11 @@ public struct AppAccountsSelectorView: View {
public init(routerPath: RouterPath,
accountCreationEnabled: Bool = true,
avatarConfig: AvatarView.FrameConfig = .badge)
avatarConfig: AvatarView.FrameConfig? = nil)
{
self.routerPath = routerPath
self.accountCreationEnabled = accountCreationEnabled
self.avatarConfig = avatarConfig
self.avatarConfig = avatarConfig ?? .badge
}
public var body: some View {
@ -96,9 +96,12 @@ public struct AppAccountsSelectorView: View {
AppAccountView(viewModel: viewModel, isParentPresented: $isPresented)
}
addAccountButton
#if os(visionOS)
.foregroundStyle(theme.labelColor)
#endif
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor)
#endif
if accountCreationEnabled {
@ -107,8 +110,10 @@ public struct AppAccountsSelectorView: View {
aboutButton
supportButton
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#if os(visionOS)
.foregroundStyle(theme.labelColor)
#else
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
}
@ -129,7 +134,7 @@ public struct AppAccountsSelectorView: View {
.environment(routerPath)
}
}
private var addAccountButton: some View {
Button {
isPresented = false
@ -153,7 +158,7 @@ public struct AppAccountsSelectorView: View {
Label("tab.settings", systemImage: "gear")
}
}
private var supportButton: some View {
Button {
isPresented = false
@ -165,7 +170,7 @@ public struct AppAccountsSelectorView: View {
Label("settings.app.support", systemImage: "wand.and.stars")
}
}
private var aboutButton: some View {
Button {
isPresented = false

View file

@ -71,35 +71,35 @@ public struct ConversationDetailView: View {
}
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.toolbar {
ToolbarItem(placement: .principal) {
if viewModel.conversation.accounts.count == 1,
let account = viewModel.conversation.accounts.first
{
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
.font(.scaledHeadline)
.foregroundColor(theme.labelColor)
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
} else {
Text("Direct message with \(viewModel.conversation.accounts.count) people")
.font(.scaledHeadline)
}
}
}
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent {
viewModel.handleEvent(event: latestEvent)
DispatchQueue.main.async {
withAnimation {
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
.toolbar {
ToolbarItem(placement: .principal) {
if viewModel.conversation.accounts.count == 1,
let account = viewModel.conversation.accounts.first
{
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
.font(.scaledHeadline)
.foregroundColor(theme.labelColor)
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
} else {
Text("Direct message with \(viewModel.conversation.accounts.count) people")
.font(.scaledHeadline)
}
}
}
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent {
viewModel.handleEvent(event: latestEvent)
DispatchQueue.main.async {
withAnimation {
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
}
}
}
}
}
}
private var loadingView: some View {

View file

@ -205,12 +205,12 @@ struct ConversationMessageView: View {
.frame(height: 200)
.contentShape(Rectangle())
.onTapGesture {
#if targetEnvironment(macCatalyst) || os(visionOS)
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
selectedAttachment: attachement))
#else
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
#endif
#if targetEnvironment(macCatalyst) || os(visionOS)
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
selectedAttachment: attachement))
#else
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
#endif
}
}

View file

@ -7,7 +7,7 @@ import SwiftUI
@MainActor
struct ConversationsListRow: View {
@Environment(\.openWindow) private var openWindow
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme

View file

@ -48,17 +48,15 @@ public struct ConversationsListView: View {
Divider()
}
} else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError {
EmptyView(iconName: "tray",
title: "conversations.empty.title",
message: "conversations.empty.message")
PlaceholderView(iconName: "tray",
title: "conversations.empty.title",
message: "conversations.empty.message")
} else if viewModel.isError {
ErrorView(title: "conversations.error.title",
message: "conversations.error.message",
buttonTitle: "conversations.error.button")
{
Task {
await viewModel.fetchConversations()
}
await viewModel.fetchConversations()
}
}

View file

@ -19,7 +19,7 @@ let package = Package(
dependencies: [
.package(name: "Models", path: "../Models"),
.package(name: "Env", path: "../Env"),
.package(url: "https://github.com/kean/Nuke", from: "12.0.0"),
.package(url: "https://github.com/kean/Nuke", from: "12.4.0"),
.package(url: "https://github.com/divadretlaw/EmojiText", from: "4.0.0"),
],
targets: [

View file

@ -201,4 +201,3 @@ public struct ThreadsLight: ColorSet {
public init() {}
}

View file

@ -1,11 +1,11 @@
import SwiftUI
public extension View {
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}

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

@ -2,14 +2,14 @@ import Combine
import UIKit
@Observable
public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
@MainActor public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
public var window: UIWindow?
#if os(visionOS)
public private(set) var windowWidth: CGFloat = 0
public private(set) var windowHeight: CGFloat = 0
public private(set) var windowWidth: CGFloat = 0
public private(set) var windowHeight: CGFloat = 0
#else
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
#endif
public func scene(_ scene: UIScene,
@ -30,11 +30,11 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
override public init() {
super.init()
#if os(visionOS)
windowWidth = window?.bounds.size.width ?? 0
windowHeight = window?.bounds.size.height ?? 0
windowWidth = window?.bounds.size.width ?? 0
windowHeight = window?.bounds.size.height ?? 0
#else
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
#endif
Self.observedSceneDelegate.insert(self)
_ = Self.observer // just for activating the lazy static property
@ -47,30 +47,29 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
}
private static var observedSceneDelegate: Set<SceneDelegate> = []
private static let observer = Task {
private static let observer = Task { @MainActor in
while true {
try? await Task.sleep(for: .seconds(0.1))
for delegate in observedSceneDelegate {
#if os(visionOS)
let newWidth = delegate.window?.bounds.size.width ?? 0
if delegate.windowWidth != newWidth {
delegate.windowWidth = newWidth
}
let newHeight = delegate.window?.bounds.size.height ?? 0
if delegate.windowHeight != newHeight {
delegate.windowHeight = newHeight
}
let newWidth = delegate.window?.bounds.size.width ?? 0
if delegate.windowWidth != newWidth {
delegate.windowWidth = newWidth
}
let newHeight = delegate.window?.bounds.size.height ?? 0
if delegate.windowHeight != newHeight {
delegate.windowHeight = newHeight
}
#else
let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width
if delegate.windowWidth != newWidth {
delegate.windowWidth = newWidth
}
let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height
if delegate.windowHeight != newHeight {
delegate.windowHeight = newHeight
}
let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width
if delegate.windowWidth != newWidth {
delegate.windowWidth = newWidth
}
let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height
if delegate.windowHeight != newHeight {
delegate.windowHeight = newHeight
}
#endif
}
}
}

View file

@ -1,8 +1,10 @@
import Combine
import SwiftUI
@Observable public class Theme {
class ThemeStorage {
@MainActor
@Observable
public final class Theme {
final class ThemeStorage {
enum ThemeKey: String {
case colorScheme, tint, label, primaryBackground, secondaryBackground
case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle
@ -69,10 +71,10 @@ import SwiftUI
}
}
}
public enum StatusActionSecondary: String, CaseIterable {
case share, bookmark
public var description: LocalizedStringKey {
switch self {
case .share:
@ -167,12 +169,14 @@ import SwiftUI
public var tintColor: Color {
didSet {
themeStorage.tintColor = tintColor
computeContrastingTintColor()
}
}
public var primaryBackgroundColor: Color {
didSet {
themeStorage.primaryBackgroundColor = primaryBackgroundColor
computeContrastingTintColor()
}
}
@ -185,6 +189,31 @@ import SwiftUI
public var labelColor: Color {
didSet {
themeStorage.labelColor = labelColor
computeContrastingTintColor()
}
}
public private(set) var contrastingTintColor: Color
// set contrastingTintColor to either labelColor or primaryBackgroundColor, whichever contrasts
// better against the tintColor
private func computeContrastingTintColor() {
func luminance(_ color: Color.Resolved) -> Float {
return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue
}
let resolvedTintColor = tintColor.resolve(in: .init())
let resolvedLabelColor = labelColor.resolve(in: .init())
let resolvedPrimaryBackgroundColor = primaryBackgroundColor.resolve(in: .init())
let tintLuminance = luminance(resolvedTintColor)
let labelLuminance = luminance(resolvedLabelColor)
let primaryBackgroundLuminance = luminance(resolvedPrimaryBackgroundColor)
if abs(tintLuminance - labelLuminance) > abs(tintLuminance - primaryBackgroundLuminance) {
contrastingTintColor = labelColor
} else {
contrastingTintColor = primaryBackgroundColor
}
}
@ -217,7 +246,7 @@ import SwiftUI
themeStorage.statusDisplayStyle = statusDisplayStyle
}
}
public var statusActionSecondary: StatusActionSecondary {
didSet {
themeStorage.statusActionSecondary = statusActionSecondary
@ -273,7 +302,7 @@ import SwiftUI
chosenFontData = nil
statusActionSecondary = .share
}
private init() {
isThemePreviouslySet = themeStorage.isThemePreviouslySet
selectedScheme = themeStorage.selectedScheme
@ -281,6 +310,7 @@ import SwiftUI
primaryBackgroundColor = themeStorage.primaryBackgroundColor
secondaryBackgroundColor = themeStorage.secondaryBackgroundColor
labelColor = themeStorage.labelColor
contrastingTintColor = .red // real work done in computeContrastingTintColor()
avatarPosition = themeStorage.avatarPosition
avatarShape = themeStorage.avatarShape
storedSet = themeStorage.storedSet
@ -293,6 +323,8 @@ import SwiftUI
chosenFontData = themeStorage.chosenFontData
statusActionSecondary = themeStorage.statusActionSecondary
selectedSet = storedSet
computeContrastingTintColor()
}
public static var allColorSet: [ColorSet] {
@ -310,7 +342,7 @@ import SwiftUI
ConstellationLight(),
ConstellationDark(),
ThreadsLight(),
ThreadsDark()
ThreadsDark(),
]
}

View file

@ -4,7 +4,7 @@ import SwiftUI
#endif
public extension View {
func applyTheme(_ theme: Theme) -> some View {
@MainActor func applyTheme(_ theme: Theme) -> some View {
modifier(ThemeApplier(theme: theme))
}
}
@ -77,17 +77,15 @@ struct ThemeApplier: ViewModifier {
}
private func setWindowUserInterfaceStyle(_ userInterfaceStyle: UIUserInterfaceStyle) {
allWindows()
.forEach {
$0.overrideUserInterfaceStyle = userInterfaceStyle
}
for window in allWindows() {
window.overrideUserInterfaceStyle = userInterfaceStyle
}
}
private func setWindowTint(_ color: Color) {
allWindows()
.forEach {
$0.tintColor = UIColor(color)
}
for window in allWindows() {
window.tintColor = UIColor(color)
}
}
private func setBarsColor(_ color: Color) {

View file

@ -2,9 +2,9 @@ import SwiftUI
public struct CancelToolbarItem: ToolbarContent {
@Environment(\.dismiss) private var dismiss
public init() { }
public init() {}
public var body: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
Button("action.cancel", role: .cancel, action: { dismiss() })

View file

@ -0,0 +1,18 @@
import SwiftUI
public struct CloseToolbarItem: ToolbarContent {
@Environment(\.dismiss) private var dismiss
public init() {}
public var body: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}, label: {
Image(systemName: "xmark.circle")
})
.keyboardShortcut(.cancelAction)
}
}
}

View file

@ -4,6 +4,7 @@ import Nuke
import NukeUI
import SwiftUI
@MainActor
struct AccountPopoverView: View {
let account: Account
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview

View file

@ -33,7 +33,8 @@ public struct AvatarView: View {
self.config = config
}
public struct FrameConfig: Equatable {
@MainActor
public struct FrameConfig: Equatable, Sendable {
public let size: CGSize
public var width: CGFloat { size.width }
public var height: CGFloat { size.height }

Some files were not shown because too many files have changed in this diff Show more