Ice Cubes 2.0 + iOS 26 supports (#2280)

* iOS 26 compiles

* New compose accessory view

* Better GlassEffectContainer transition

* Fixes

* Drop iOS 17 + Timeline filters at bottom

* Glass status observer

* Fixes

* More fixes

* Disable tabbar collapse

* Refacor timeline checkpoint

* Tyding code

* Set version to 2.0

* Remove fast timeline setting

* Perf fixes

* Remove custom Sidebar

* Perf boost + remove content gradient

* Fixes

* Remove steaming

* Fix TL

* Use Tab

* Fix separators

* Better sheet

* Editor enhance

* Leading status row actions

* More iOS 26 fixes

* Fix context menu tint color

* Fix app account sheet

* Fix extension send button

* New Icons

* AppIcon

* Add back VisionOS Icon

* Working icon

* Update Claude.MD

* WIP Playground

* Replace OpenAI with FoundationModels

* Stream response

* Prewarm Assistant on editor open

* Add more LLM tools

* Fixes

* Remove contentWarning prompt

* Update packages to Swift 6.2

* Various iOS 26 fixes

* Prepare birictional gap

* Add sections for iPad sidebar

* Fix profile status update

* Make StatusesState Equatable

* Disable LLM on macCatalyst for now

* Add Tags and Lists to sideabar

* Add local timeline and tags group in the sidebar

* Account: Refine header view

* Show non LLM tag

* Support V2 notifications group

* Refactor NotificationsViewModel

* Fix initial gap

* Notifications list: Add glassEffect

* Set editor glass effect to interactive

* Refactor TimelineView

* Notifications: Merge new group

* Notifications: Refactor VM

* Notifications: Cleanup code

* Liquid glass DM View

* Add direct access to DM in notifications top menu

* Editor: Fix sizing on mention/tags

* Better DM detail view

* DM: More refactor view

* DM: Even better views

* DM: Rework media attachements

* DM: Interactive textfield

* Notifications: Refactor and remove ViewModel

* DM: Refactor and remove ViewModel

* DM Detail: Refactor without ViewModel

* Remove sub.club support

* DM Detail: Fix loading

* Icons cleanup

* Tweak icons

* Account Detail: Refactor sub tabs

* More icons

* Account Detail: Remove ViewModel

* Account Detail: Refactor to smaller views

* Explore: Refactor to components + inline state

* Accounts Statuses: Refactor + remove VM

* Edit note: Remove VM

* Trim AccountDetail

* Account: Set button to primary color

* Fixes

* Improve UI consistency and context menus in media and notifications

Refactored MediaView to use context menus instead of Menu for media items and updated alt/discard marker buttons for iOS 26 compatibility. Added .tint(.label) to notification filter buttons for consistent appearance. Minor code style and import order improvements in AppView.

* Add .glassProminent

* Fix warnings

* Refactor compressVideo to use async/await

Updated the compressVideo function to use Swift's async/await syntax instead of withCheckedContinuation, simplifying the code and improving readability.

* Rename SPM Network -> NetworkClient

* Fix build

* Client -> MastodonClient

* Rename file

* Refactor media container to use state-based model

Replaces the previous MediaContainer property model with a state-based enum to better represent the lifecycle of media (pending, uploading, uploaded, failed). Updates MediaView and ViewModel to use the new state model, improving clarity and error handling for media uploads, progress, and failures. Adds convenience initializers and factory methods for creating containers in various states, and updates UI logic to match the new structure.

* Add media upload progress tracking and UI updates

Introduces progress tracking for media uploads in MastodonClient by adding new upload methods with progress handlers and a URLSession delegate. Updates StatusEditor ViewModel to pass progress handlers and update media container state during uploads. Enhances MediaView to display both circular and linear progress indicators with animation.

* Refactor message view to use glass effect on iOS 26+

Introduces a conditional to use .glassEffect for message backgrounds on iOS 26 and above, while maintaining the previous background logic for earlier versions. Extracts the message text rendering into a reusable textView property for cleaner code.

* Test removing legacy app icon

* Revert "Test removing legacy app icon"

This reverts commit 27c552d3cb.

* Refactor timeline pills and tab bar for iOS 26 compatibility

Moved TimelineQuickAccessPills logic from AppView to TimelineView and updated implementation to use new iOS 26 APIs where available. Simplified tab bar view in AppView and improved conditional logic for iPad and Mac layouts.

* Update glassEffect to use .regular.interactive()

Replaces the default glassEffect modifier with .regular.interactive() for improved visual consistency and interaction feedback on FollowButton and related controls.

* Improve progress indicator logic and remove iOS version check

Refines the display logic for progress indicators in MediaView, showing a linear progress bar only when progress is between 0 and 1, and otherwise showing a circular indicator. Also removes an unnecessary iOS version check in TimelineView for toolbar background visibility.

* Update screenshots

* Update toolbar tint and improve account fields UI

Added `.tint(.label)` to several toolbar items for consistent icon coloring. Refactored AccountFieldsView to support iOS 26+ visual effects and improved accessibility and background handling. Removed redundant `.tint(.label)` from NotificationsListView.

* Add zoom transition for MediaUI

* Remove Old AppIcon

* Remove alternate icons assets

* Update sign-in button style and layout in AddAccountView

Refactors the sign-in button to use a new 'signinButton' view, applying .glassProminent style on iOS 26+ and .borderedProminent otherwise. Adjusts button layout for full width, sets a fixed height, and updates row insets and background for improved appearance.

* Remove iPhone tab label preference setting

Eliminated the 'showiPhoneTabLabel' toggle from TabbarEntriesSettingsView and related property from UserPreferences. This streamlines settings by removing an unused or deprecated option.

* Fix text replacement in StatusEditor ViewModel

Updated the async stream handling in StatusEditor to use the 'content' property of the streamed object when replacing text, ensuring correct text updates.

* Update UI for iOS 26 and improve avatar effects

Refactored ToolbarTab and AppAccountsSelectorView to use .embed avatarConfig and apply glassEffect on iOS 26+. Updated contentShape to .circle for better interaction. Added theme-based tint to notification list buttons for improved visual consistency.

* Add optional caption to Mastodon post intent (#2292)

* Add app shortcut and intent for inline image posting on Mastodon (#2293)

* Add optional alt text to image upload intents (#2294)

* Adjust toolbar label offset for iOS 26 (#2296)

* Improve background handling in Explore and Editor views

Updated ExploreView to use edgesIgnoringSafeArea for the background color and made minor formatting improvements. Refactored MainView in StatusEditor to use a computed backgroundColor view for better background management based on presentationDetent.

* Remove macCatalyst conditional compilation for AI features

Eliminated #if !targetEnvironment(macCatalyst) checks from AI-related components in the status editor, making AI features available on all platforms where iOS 26+ is supported. Also added new localization keys related to image posting and descriptions.

* Add ToolbarSpacer and improve toolbar styling

Introduces ToolbarSpacer with .topBarTrailing placement for iOS 26.0+ in NotificationTab, TimelineTab, and TimelineView to improve toolbar layout. Updates toolbar item placements and adds theme-based foreground styling to toolbar icons. Removes unnecessary line limit in NotificationRowContentView and adjusts line limit logic in StatusRowTextView for better text display. Refactors some pattern matching for clarity.
This commit is contained in:
Thomas Ricouard 2025-08-27 07:37:20 +02:00 committed by GitHub
parent a3f041c741
commit 24cccfad1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
417 changed files with 9203 additions and 7294 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View file

@ -0,0 +1,163 @@
{
"fill-specializations" : [
{
"value" : {
"linear-gradient" : [
"display-p3:0.36471,0.36863,0.94510,1.00000",
"srgb:0.57919,0.12801,0.57269,1.00000"
]
}
},
{
"appearance" : "dark",
"value" : "system-dark"
}
],
"groups" : [
{
"blend-mode-specializations" : [
{
"appearance" : "tinted",
"value" : "normal"
}
],
"blur-material" : null,
"layers" : [
{
"blend-mode" : "screen",
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : "automatic"
},
{
"appearance" : "tinted",
"value" : {
"linear-gradient" : [
"display-p3:0.90000,0.90000,0.90000,0.83000",
"srgb:1.00000,1.00000,1.00000,0.41987"
]
}
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 1.24,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 0.59
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.87
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : false,
"value" : 0.84
}
}
]
},
{
"blur-material-specializations" : [
{
"value" : 1
},
{
"appearance" : "tinted",
"value" : 1
}
],
"layers" : [
{
"blend-mode-specializations" : [
{
"value" : "screen"
},
{
"appearance" : "dark",
"value" : "lighten"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:1.00000,0.25279,1.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"solid" : "display-p3:0.82510,0.25332,1.00000,1.00000"
}
},
{
"appearance" : "tinted",
"value" : "automatic"
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 1.13,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 1
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.3
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : false,
"value" : 0.1
}
}
]
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<path id="Oval" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 50 25 C 50 11.192883 38.807117 0 25 0 C 11.192882 0 0 11.192883 0 25 C 0 38.807117 11.192882 50 25 50 C 38.807117 50 50 38.807117 50 25 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<path id="Oval-copy" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 50 25 C 50 11.192883 38.807117 0 25 0 C 11.192882 0 0 11.192883 0 25 C 0 38.807117 11.192882 50 25 50 C 38.807117 50 50 38.807117 50 25 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="563" height="116" viewBox="0 0 563 116" xmlns="http://www.w3.org/2000/svg">
<path id="Oval" fill="#9368ef" fill-rule="evenodd" stroke="none" d="M 563 58 C 563 25.967484 436.96817 0 281.5 0 C 126.031845 0 0 25.967484 0 58 C 0 90.032516 126.031845 116 281.5 116 C 436.96817 116 563 90.032516 563 58 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="507" height="394" viewBox="0 0 507 394" xmlns="http://www.w3.org/2000/svg">
<path id="Path" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 483 2 C 483 2 512.988586 13.092346 497 41 C 482.658203 66.033203 438.230896 111.900574 459 139 C 473.848694 158.374512 473.556641 143.664063 497 154 C 501.080017 155.798828 511.591797 163.125 503 179 C 494.266113 195.137512 476.132813 205.480469 472 237 C 467.867188 268.519531 497 250 497 250 L 489 320 C 489 320 481.453125 293.089844 402 348 C 322.546875 402.910156 262.179321 377.661072 251 375 C 175.819977 357.104492 166.122375 354.23291 160 362 C 128.982666 401.349884 64.947479 401.921997 3 376 C 1.566711 375.400238 1.434143 370.590149 1 365 C -1.16745 337.091187 142.118988 331.994934 222 250 C 318.242188 151.210938 280.920471 115.197754 306 90 C 321.328125 74.599609 316.399841 51.071167 414 57 C 425.937134 57.725159 444.693237 28.917725 455 15 C 470.195007 -5.518616 483 2 483 2 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="320" height="44" viewBox="0 0 320 44" xmlns="http://www.w3.org/2000/svg">
<path id="Path" fill="#9368ef" fill-opacity="0.397493" fill-rule="evenodd" stroke="none" d="M 15 20 C 12.371399 19.847656 0.616394 39.961365 1 40 C 61.304993 46.071472 224.469727 45.789246 319 15 C 321.072937 14.324829 314.229187 13.876343 311 9 C 309.598389 6.883423 312.004028 0.24054 309 1 C 224.475891 22.368103 81.76416 23.870117 15 20 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="487" height="349" viewBox="0 0 487 349" xmlns="http://www.w3.org/2000/svg">
<path id="Path-copy-2" fill="#4c2cb2" fill-rule="evenodd" stroke="none" d="M 456 260 C 456.742737 256.67865 487 7 487 7 C 487 7 479.453125 -19.910156 400 35 C 320.546875 89.910156 260.179321 64.661072 249 62 C 173.819977 44.104492 164.122375 41.23291 158 49 C 126.982666 88.349884 62.947479 88.921997 1 63 C -0.433289 62.400238 19.681335 249.13916 20 250 C 45.552124 319.030579 96.464447 340.619385 228 348 C 335.737 354.045258 442.191467 321.749542 456 260 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="510" height="406" viewBox="0 0 510 406" xmlns="http://www.w3.org/2000/svg">
<path id="Path-copy" fill="#5737b5" fill-opacity="0.18601" fill-rule="evenodd" stroke="none" d="M 272 35 C 211.596466 34.858887 72.501373 32.551025 1 2 C 0.651367 1.851013 35.419159 312.718201 36 315 C 50.539032 372.116516 111.354187 404.779663 233 405 C 256.668945 405.042847 304 405 304 405 C 304 405 461.608398 395.958496 472 322 C 480.747559 259.742432 509.830811 0.628601 509 1 C 446.60675 28.893738 322.028076 35.116882 272 35 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="564" height="503" viewBox="0 0 564 503" xmlns="http://www.w3.org/2000/svg">
<path id="Path" fill="#e3d9fa" fill-opacity="0.764026" fill-rule="evenodd" stroke="none" d="M 1 2 C 6.978516 13.994141 27 25 27 25 C 27 25 61.419159 335.718201 62 338 C 76.539032 395.116516 137.354187 427.779663 259 428 C 282.668945 428.042847 330 428 330 428 C 330 428 487.608398 418.958496 498 345 C 506.747559 282.742432 536 24 536 24 C 536 24 555.78833 10.634644 563 1 C 566.011047 -3.022705 515 416 515 416 C 515 416 520.245361 450.830627 472 472 C 424.911926 492.66156 304.897827 514.033081 174 495 C 25.14917 473.356506 46.727173 419.739075 43 388 C 30.527893 281.792633 -4.978516 -9.994141 1 2 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="248" height="245" viewBox="0 0 248 245" xmlns="http://www.w3.org/2000/svg">
<path id="Rounded-Rectangle-copy-2" fill="#ffffff" fill-rule="evenodd" stroke="none" d="M 6.537902 110.999268 C -4.468609 126.213013 -1.057972 147.468735 14.155773 158.47525 L 123.515793 237.592667 C 138.729538 248.599167 159.985245 245.188538 170.99176 229.974792 L 241.043701 133.145523 C 252.050217 117.931778 248.639572 96.676056 233.425842 85.669556 L 124.065819 6.552124 C 108.852074 -4.454376 87.596359 -1.043747 76.589844 14.169998 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 627 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="248" height="250" viewBox="0 0 248 250" xmlns="http://www.w3.org/2000/svg">
<path id="Rounded-Rectangle-copy" fill="#ffffff" fill-rule="evenodd" stroke="none" d="M 11.10811 90.739502 C -2.717603 103.445831 -3.625039 124.954315 9.081296 138.780029 L 100.417458 238.162537 C 113.123787 251.988251 134.632263 252.895691 148.457977 240.189362 L 236.453003 159.318726 C 250.278717 146.612396 251.186142 125.103912 238.479813 111.278198 L 147.143661 11.895691 C 134.437317 -1.930023 112.928841 -2.837463 99.103127 9.868866 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 628 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="248" height="245" viewBox="0 0 248 245" xmlns="http://www.w3.org/2000/svg">
<path id="Rounded-Rectangle" fill="#ffffff" fill-rule="evenodd" stroke="none" d="M 75.916382 229.732666 C 86.797638 245.03624 108.024643 248.621246 123.328224 237.73999 L 233.334015 159.522949 C 248.637589 148.641693 252.222595 127.414696 241.341339 112.111115 L 172.086609 14.710068 C 161.205353 -0.593521 139.978348 -4.178528 124.674767 6.702728 L 14.668981 84.919769 C -0.634602 95.801025 -4.219606 117.028038 6.661647 132.331604 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.18 -->
<svg width="509" height="73" viewBox="0 0 509 73" xmlns="http://www.w3.org/2000/svg">
<path id="Shape" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 509 36.5 C 509 16.341614 380.556458 0 240 0 C 99.443542 0 0 16.341614 0 36.5 C 0 53.433044 80.39856 67.672974 189.434479 71.796265 C 210.203217 72.581665 232.010956 73 254.5 73 C 395.056458 73 509 56.658386 509 36.5 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View file

@ -0,0 +1,217 @@
{
"fill" : {
"linear-gradient" : [
"display-p3:0.49338,0.09624,0.75589,1.00000",
"display-p3:0.10553,0.00332,0.25763,1.00000"
]
},
"groups" : [
{
"layers" : [
{
"image-name" : "Path 2.svg",
"name" : "Path 2",
"position" : {
"scale" : 1,
"translation-in-points" : [
78,
-211
]
}
},
{
"glass" : false,
"image-name" : "Path copy.svg",
"name" : "Path copy",
"position" : {
"scale" : 1,
"translation-in-points" : [
0,
-23
]
}
},
{
"blend-mode-specializations" : [
{
"appearance" : "dark",
"value" : "normal"
}
],
"fill-specializations" : [
{
"appearance" : "dark",
"value" : "none"
}
],
"image-name" : "Rounded Rectangle copy 2.svg",
"name" : "Rounded Rectangle copy 2",
"opacity-specializations" : [
{
"value" : 0.9
},
{
"appearance" : "dark",
"value" : 0.8
}
],
"position" : {
"scale" : 1,
"translation-in-points" : [
39,
-186.1796875
]
}
},
{
"fill-specializations" : [
{
"appearance" : "dark",
"value" : "none"
}
],
"image-name" : "Rounded Rectangle.svg",
"name" : "Rounded Rectangle",
"opacity-specializations" : [
{
"value" : 0.9
},
{
"appearance" : "dark",
"value" : 0.8
}
],
"position" : {
"scale" : 1,
"translation-in-points" : [
-111,
-35
]
}
},
{
"fill-specializations" : [
{
"appearance" : "dark",
"value" : "none"
}
],
"image-name" : "Rounded Rectangle copy.svg",
"name" : "Rounded Rectangle copy",
"opacity-specializations" : [
{
"value" : 0.9
},
{
"appearance" : "dark",
"value" : 0.8
}
],
"position" : {
"scale" : 1,
"translation-in-points" : [
96.51953125,
48
]
}
},
{
"image-name" : "Oval 1.svg",
"name" : "Oval 1",
"position" : {
"scale" : 1,
"translation-in-points" : [
-47.87109375,
-342.03125
]
}
},
{
"image-name" : "Oval copy.svg",
"name" : "Oval copy",
"opacity" : 1,
"position" : {
"scale" : 1,
"translation-in-points" : [
278.50390625,
-375.5703125
]
}
},
{
"image-name" : "Path.svg",
"name" : "Path"
},
{
"image-name" : "Path 1.svg",
"name" : "Path 1",
"position" : {
"scale" : 1,
"translation-in-points" : [
10,
-277
]
}
},
{
"glass" : false,
"hidden" : false,
"image-name" : "Shape.svg",
"name" : "Shape",
"position" : {
"scale" : 1,
"translation-in-points" : [
1.216796875,
-248
]
}
},
{
"hidden" : false,
"image-name" : "Oval.svg",
"name" : "Oval",
"position" : {
"scale" : 1,
"translation-in-points" : [
-0.62109375,
-247.6171875
]
}
},
{
"glass" : false,
"image-name" : "Path copy 2.svg",
"name" : "Path copy 2",
"position" : {
"scale" : 1.1,
"translation-in-points" : [
2,
10
]
}
}
],
"position" : {
"scale" : 1,
"translation-in-points" : [
0,
97.5
]
},
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View file

@ -0,0 +1,309 @@
{
"fill-specializations" : [
{
"value" : {
"linear-gradient" : [
"display-p3:0.67059,0.26667,0.85098,1.00000",
"display-p3:0.11373,0.12941,0.22353,1.00000"
]
}
},
{
"appearance" : "dark",
"value" : "system-dark"
}
],
"groups" : [
{
"blend-mode-specializations" : [
{
"appearance" : "tinted",
"value" : "normal"
}
],
"blur-material" : 1,
"layers" : [
{
"blend-mode" : "screen",
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : "automatic"
},
{
"appearance" : "tinted",
"value" : {
"linear-gradient" : [
"display-p3:0.90000,0.90000,0.90000,0.83000",
"srgb:1.00000,1.00000,1.00000,0.41987"
]
}
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 1.24,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 1
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.2
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : true,
"value" : 0.1
}
}
]
},
{
"blur-material-specializations" : [
{
"value" : 1
},
{
"appearance" : "tinted",
"value" : 1
}
],
"layers" : [
{
"blend-mode-specializations" : [
{
"value" : "screen"
},
{
"appearance" : "dark",
"value" : "lighten"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:1.00000,0.25279,1.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"solid" : "display-p3:0.82510,0.25332,1.00000,1.00000"
}
},
{
"appearance" : "tinted",
"value" : "automatic"
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 1.13,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 1
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.3
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : true,
"value" : 0.3
}
}
]
},
{
"blur-material-specializations" : [
{
"value" : 1
},
{
"appearance" : "tinted",
"value" : 1
}
],
"layers" : [
{
"blend-mode-specializations" : [
{
"value" : "screen"
},
{
"appearance" : "dark",
"value" : "lighten"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "display-p3:0.96921,0.39989,0.04269,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"solid" : "display-p3:1.00000,0.61199,0.29857,1.00000"
}
},
{
"appearance" : "tinted",
"value" : "automatic"
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 1.02,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 1
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.3
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : true,
"value" : 0.5
}
}
]
},
{
"blur-material-specializations" : [
{
"value" : 1
},
{
"appearance" : "tinted",
"value" : 1
}
],
"layers" : [
{
"blend-mode-specializations" : [
{
"value" : "screen"
},
{
"appearance" : "dark",
"value" : "lighten"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "display-p3:0.96921,0.94842,0.00650,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"solid" : "display-p3:1.00000,0.89360,0.20100,1.00000"
}
},
{
"appearance" : "tinted",
"value" : "automatic"
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 0.92,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 1
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.3
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : true,
"value" : 0.7
}
}
]
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

149
AppIcon.icon/icon.json Normal file
View file

@ -0,0 +1,149 @@
{
"fill-specializations" : [
{
"value" : "system-light"
},
{
"appearance" : "dark",
"value" : "system-dark"
}
],
"groups" : [
{
"blend-mode-specializations" : [
{
"appearance" : "tinted",
"value" : "normal"
}
],
"blur-material" : null,
"layers" : [
{
"fill-specializations" : [
{
"appearance" : "dark",
"value" : "automatic"
},
{
"appearance" : "tinted",
"value" : {
"linear-gradient" : [
"display-p3:0.90000,0.90000,0.90000,0.83000",
"srgb:1.00000,1.00000,1.00000,0.41987"
]
}
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 1.24,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 0.5
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.84
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : false,
"value" : 0.84
}
}
]
},
{
"blur-material-specializations" : [
{
"value" : 1
},
{
"appearance" : "tinted",
"value" : 1
}
],
"layers" : [
{
"blend-mode-specializations" : [
{
"appearance" : "dark",
"value" : "lighten"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:1.00000,0.25279,1.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"solid" : "display-p3:0.82510,0.25332,1.00000,1.00000"
}
},
{
"appearance" : "tinted",
"value" : "automatic"
}
],
"glass" : true,
"hidden" : false,
"image-name" : "puple_cube.png",
"name" : "puple_cube",
"position" : {
"scale" : 1.13,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "layer-color",
"opacity" : 0.5
},
"specular" : true,
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.1
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : false,
"value" : 0.1
}
}
]
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

311
CLAUDE.md
View file

@ -8,19 +8,12 @@ IceCubesApp is a multiplatform Mastodon client built entirely in SwiftUI. It's a
## Build Commands ## Build Commands
### Initial Setup ### Building for iOS Simulator
1. Clone the repository To build IceCubesApp for iPhone 16 Pro simulator:
2. Create your configuration file: ```bash
```bash mcp__XcodeBuildMCP__build_sim_name_proj projectPath: "/Users/thomas/Documents/Dev/Open Source/IceCubesApp/IceCubesApp.xcodeproj" scheme: "IceCubesApp" simulatorName: "iPhone 16 Pro"
cp IceCubesApp.xcconfig.template IceCubesApp.xcconfig ```
```
3. Edit `IceCubesApp.xcconfig` and add:
- `DEVELOPMENT_TEAM` = Your Apple Developer Team ID
- `BUNDLE_ID_PREFIX` = Your bundle identifier prefix (e.g., com.yourcompany)
### Building
- **Xcode GUI**: Open `IceCubesApp.xcodeproj` and build
- **Command Line**: `xcodebuild -scheme IceCubesApp build`
### Running Tests ### Running Tests
- **All tests**: Run through Xcode's Test navigator - **All tests**: Run through Xcode's Test navigator
@ -80,51 +73,90 @@ The codebase contains legacy MVVM patterns, but **new features should NOT use Vi
## Modern SwiftUI Architecture Guidelines (2025) ## Modern SwiftUI Architecture Guidelines (2025)
### No ViewModels - Use Native SwiftUI Data Flow ### Core Philosophy
**New features MUST follow these patterns:**
1. **Views as Pure State Expressions** - SwiftUI is the default UI paradigm - embrace its declarative nature
```swift - Avoid legacy UIKit patterns and unnecessary abstractions
struct MyView: View { - Focus on simplicity, clarity, and native data flow
@Environment(MyService.self) private var service - Let SwiftUI handle the complexity - don't fight the framework
@State private var viewState: ViewState = .loading - **No ViewModels** - Use native SwiftUI data flow patterns
enum ViewState {
case loading
case loaded(data: [Item])
case error(String)
}
var body: some View {
// View is just a representation of its state
}
}
```
2. **Use Environment Appropriately** ### Architecture Principles
- **App-wide services**: Router, Theme, CurrentAccount, Client, etc. - use `@Environment`
- **Feature-specific services**: Timeline services, single-view logic - use `let` properties with `@Observable`
- Rule: Environment for cross-app/cross-feature dependencies, let properties for single-feature services
- Access app-wide via `@Environment(ServiceType.self)`
- Feature services: `private let myService = MyObservableService()`
3. **Local State Management** #### 1. Native State Management
- Use `@State` for view-specific state
- Use `enum` for view states (loading, loaded, error)
- Use `.task(id:)` and `.onChange(of:)` for side effects
- Pass state between views using `@Binding`
4. **No ViewModels Required** Use SwiftUI's built-in property wrappers appropriately:
- Views should be lightweight and disposable - `@State` - Local, ephemeral view state
- Business logic belongs in services/clients - `@Binding` - Two-way data flow between views
- Test services independently, not views - `@Observable` - Shared state (preferred for new code)
- Use SwiftUI previews for visual testing - `@Environment` - Dependency injection for app-wide concerns
5. **When Views Get Complex** #### 2. State Ownership
- Split into smaller subviews
- Use compound views that compose smaller views - Views own their local state unless sharing is required
- Pass state via bindings between views - State flows down, actions flow up
- Never reach for a ViewModel as the solution - Keep state as close to where it's used as possible
- Extract shared state only when multiple views need it
Example:
```swift
struct TimelineView: View {
@Environment(Client.self) private var client
@State private var viewState: ViewState = .loading
enum ViewState {
case loading
case loaded(statuses: [Status])
case error(Error)
}
var body: some View {
Group {
switch viewState {
case .loading:
ProgressView()
case .loaded(let statuses):
StatusList(statuses: statuses)
case .error(let error):
ErrorView(error: error)
}
}
.task {
await loadTimeline()
}
}
private func loadTimeline() async {
do {
let statuses = try await client.getHomeTimeline()
viewState = .loaded(statuses: statuses)
} catch {
viewState = .error(error)
}
}
}
```
#### 3. Modern Async Patterns
- Use `async/await` as the default for asynchronous operations
- Leverage `.task` modifier for lifecycle-aware async work
- Handle errors gracefully with try/catch
- Avoid Combine unless absolutely necessary
#### 4. View Composition
- Build UI with small, focused views
- Extract reusable components naturally
- Use view modifiers to encapsulate common styling
- Prefer composition over inheritance
#### 5. Code Organization
- Organize by feature (e.g., Timeline/, Account/, Settings/)
- Keep related code together in the same file when appropriate
- Use extensions to organize large files
- Follow Swift naming conventions consistently
### Build Verification Process ### Build Verification Process
**IMPORTANT**: When editing code, you MUST: **IMPORTANT**: When editing code, you MUST:
@ -132,6 +164,7 @@ The codebase contains legacy MVVM patterns, but **new features should NOT use Vi
1. Build the project after making changes using XcodeBuildMCP commands 1. Build the project after making changes using XcodeBuildMCP commands
2. Fix any compilation errors before proceeding 2. Fix any compilation errors before proceeding
3. Run relevant tests if modifying existing functionality 3. Run relevant tests if modifying existing functionality
4. Ensure code follows modern SwiftUI patterns
Example workflow: Example workflow:
```bash ```bash
@ -142,15 +175,183 @@ mcp__XcodeBuildMCP__build_mac_proj projectPath: "/path/to/IceCubesApp.xcodeproj"
mcp__XcodeBuildMCP__build_ios_sim_name_proj projectPath: "/path/to/IceCubesApp.xcodeproj" scheme: "IceCubesApp" simulatorName: "iPhone 16" mcp__XcodeBuildMCP__build_ios_sim_name_proj projectPath: "/path/to/IceCubesApp.xcodeproj" scheme: "IceCubesApp" simulatorName: "iPhone 16"
``` ```
### Implementation Examples
#### Shared State with @Observable
```swift
@Observable
class AppAccountsManager {
var currentAccount: Account?
var availableAccounts: [Account] = []
func switchAccount(_ account: Account) {
currentAccount = account
// Handle account switching
}
}
// In App file
struct IceCubesApp: App {
@State private var accountManager = AppAccountsManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(accountManager)
}
}
}
```
#### Modern Async Data Loading
```swift
struct NotificationsView: View {
@Environment(Client.self) private var client
@State private var notifications: [Notification] = []
@State private var isLoading = false
@State private var error: Error?
var body: some View {
List(notifications) { notification in
NotificationRow(notification: notification)
}
.overlay {
if isLoading {
ProgressView()
}
}
.task {
await loadNotifications()
}
.refreshable {
await loadNotifications()
}
}
private func loadNotifications() async {
isLoading = true
defer { isLoading = false }
do {
notifications = try await client.getNotifications()
} catch {
self.error = error
}
}
}
```
### Best Practices
#### DO:
- Write self-contained views when possible
- Use property wrappers as intended by Apple
- Test logic in isolation, preview UI visually
- Handle loading and error states explicitly
- Keep views focused on presentation
- Use Swift's type system for safety
- Trust SwiftUI's update mechanism
#### DON'T:
- Create ViewModels for every view
- Move state out of views unnecessarily
- Add abstraction layers without clear benefit
- Use Combine for simple async operations
- Fight SwiftUI's update mechanism
- Overcomplicate simple features
- **Nest @Observable objects within other @Observable objects** - This breaks SwiftUI's observation system. Initialize services at the view level instead.
### Testing Strategy
- Unit test business logic in services/clients
- Use SwiftUI Previews for visual testing
- Test @Observable classes independently
- Keep tests simple and focused
- Don't sacrifice code clarity for testability
### Code Style When Editing ### Code Style When Editing
- Maintain existing patterns in legacy code - Maintain existing patterns in legacy code
- New features use modern patterns exclusively - New features use modern patterns exclusively
- Prefer composition over inheritance - Prefer composition over inheritance
- Keep views focused and single-purpose - Keep views focused and single-purpose
- Use descriptive names for state enums - Use descriptive names for state enums
- Write SwiftUI code that looks and feels like SwiftUI
## Development Requirements ## Development Requirements
- Minimum Swift 6.0 - Minimum Swift 6.0
- Minimum deployment: iOS 17.0, visionOS 1.0 - iOS 26 SDK (June 2025)
- Xcode 15.0 or later - Minimum deployment: iOS 18.0, visionOS 1.0
- Apple Developer account for device testing - Xcode 16.0 or later with iOS 26 SDK
- Apple Developer account for device testing
## iOS 26 SDK Integration
**IMPORTANT**: The project now supports iOS 26 SDK (June 2025) while maintaining iOS 18 as the minimum deployment target. Use `#available` checks when adopting iOS 26+ APIs.
### Available iOS 26 SwiftUI APIs
#### Liquid Glass Effects
- `glassEffect(_:in:isEnabled:)` - Apply Liquid Glass effects to views
- `buttonStyle(.glass)` - Apply Liquid Glass styling to buttons
- `ToolbarSpacer` - Create visual breaks in toolbars with Liquid Glass
Example:
```swift
Button("Post", action: postStatus)
.buttonStyle(.glass)
.glassEffect(.thin, in: .rect(cornerRadius: 12))
```
#### Enhanced Scrolling
- `scrollEdgeEffectStyle(_:for:)` - Configure scroll edge effects
- `backgroundExtensionEffect()` - Duplicate, mirror, and blur views around edges
#### Tab Bar Enhancements
- `tabBarMinimizeBehavior(_:)` - Control tab bar minimization behavior
- Search role for tabs with search field replacing tab bar
- `TabViewBottomAccessoryPlacement` - Adjust accessory view content based on placement
#### Web Integration
- `WebView` and `WebPage` - Full control over browsing experience
#### Drag and Drop
- `draggable(_:_:)` - Drag multiple items
- `dragContainer(for:id:in:selection:_:)` - Container for draggable views
#### Animation
- `@Animatable` macro - SwiftUI synthesizes custom animatable data properties
#### UI Components
- `Slider` with automatic tick marks when using step parameter
- `windowResizeAnchor(_:)` - Set window anchor point for resizing
#### Text Enhancements
- `TextEditor` now supports `AttributedString`
- `AttributedTextSelection` - Handle text selection with attributed text
- `AttributedTextFormattingDefinition` - Define text styling in specific contexts
- `FindContext` - Create find navigator in text editing views
#### Accessibility
- `AssistiveAccess` - Support Assistive Access in iOS/iPadOS scenes
#### HDR Support
- `Color.ResolvedHDR` - RGBA values with HDR headroom information
#### UIKit Integration
- `UIHostingSceneDelegate` - Host and present SwiftUI scenes in UIKit
- `NSHostingSceneRepresentation` - Host SwiftUI scenes in AppKit
- `NSGestureRecognizerRepresentable` - Incorporate gesture recognizers from AppKit
#### Immersive Spaces (visionOS)
- `manipulable(coordinateSpace:operations:inertia:isEnabled:onChanged:)` - Hand gesture manipulation
- `SurfaceSnappingInfo` - Snap volumes and windows to surfaces
- `RemoteImmersiveSpace` - Render stereo content from Mac to Apple Vision Pro
- `SpatialContainer` - 3D layout container
- Depth-based modifiers: `aspectRatio3D(_:contentMode:)`, `rotation3DLayout(_:)`, `depthAlignment(_:)`
### Usage Guidelines
- Use `#available(iOS 26, *)` for iOS 26-only features
- Replace legacy implementations with iOS 26 APIs where appropriate
- Leverage Liquid Glass effects for modern UI aesthetics in timeline and status views
- Use enhanced text capabilities for the status composer
- Apply new drag-and-drop APIs for media and status interactions

View file

@ -7,7 +7,7 @@
import MobileCoreServices import MobileCoreServices
import Models import Models
import Network import NetworkClient
import UIKit import UIKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
@ -52,7 +52,7 @@ extension URL {
guard let host = host() else { guard let host = host() else {
throw ActionRequestHandler.Error.noHost throw ActionRequestHandler.Error.noHost
} }
let _: Instance = try await Client(server: host).get(endpoint: Instances.instance) let _: Instance = try await MastodonClient(server: host).get(endpoint: Instances.instance)
return true return true
} catch { } catch {
return false return false

View file

@ -29,7 +29,7 @@
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DF2BE6543D004E6BEF /* AppAccount */; }; 9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DF2BE6543D004E6BEF /* AppAccount */; };
9F7788E22BE6543D004E6BEF /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E12BE6543D004E6BEF /* Env */; }; 9F7788E22BE6543D004E6BEF /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E12BE6543D004E6BEF /* Env */; };
9F7788E42BE6543D004E6BEF /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E32BE6543D004E6BEF /* Models */; }; 9F7788E42BE6543D004E6BEF /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E32BE6543D004E6BEF /* Models */; };
9F7788E62BE6543D004E6BEF /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E52BE6543D004E6BEF /* Network */; }; 9F7788E62BE6543D004E6BEF /* NetworkClient in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E52BE6543D004E6BEF /* NetworkClient */; };
9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788EF2BE78E77004E6BEF /* Timeline */; }; 9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788EF2BE78E77004E6BEF /* Timeline */; };
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; }; 9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
9F9191592C6DDF20001C89E7 /* WishKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9F9191582C6DDF20001C89E7 /* WishKit */; }; 9F9191592C6DDF20001C89E7 /* WishKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9F9191582C6DDF20001C89E7 /* WishKit */; };
@ -42,7 +42,7 @@
9FAD85A2297456A400496AB1 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A1297456A400496AB1 /* Env */; }; 9FAD85A2297456A400496AB1 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A1297456A400496AB1 /* Env */; };
9FAD85A4297456A800496AB1 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A3297456A800496AB1 /* DesignSystem */; }; 9FAD85A4297456A800496AB1 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A3297456A800496AB1 /* DesignSystem */; };
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; }; 9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; };
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; }; 9FBFE64E292A72BD00C250E9 /* NetworkClient in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* NetworkClient */; };
9FC2A38B2B49D19A00DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38A2B49D19A00DFD1C1 /* StatusKit */; }; 9FC2A38B2B49D19A00DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38A2B49D19A00DFD1C1 /* StatusKit */; };
9FC2A38D2B49D1A200DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38C2B49D1A200DFD1C1 /* StatusKit */; }; 9FC2A38D2B49D1A200DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38C2B49D1A200DFD1C1 /* StatusKit */; };
9FC2A38F2B49D1AA00DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38E2B49D1AA00DFD1C1 /* StatusKit */; }; 9FC2A38F2B49D1AA00DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38E2B49D1AA00DFD1C1 /* StatusKit */; };
@ -53,7 +53,8 @@
9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; }; 9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; };
DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */; }; DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */; };
E92817FA298443D600875FD1 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = E92817F9298443D600875FD1 /* Models */; }; E92817FA298443D600875FD1 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = E92817F9298443D600875FD1 /* Models */; };
E92817FC298443D600875FD1 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = E92817FB298443D600875FD1 /* Network */; }; E92817FC298443D600875FD1 /* NetworkClient in Frameworks */ = {isa = PBXBuildFile; productRef = E92817FB298443D600875FD1 /* NetworkClient */; };
E9DB78982E004E4C004EAC2E /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = E9DB78972E004E4C004EAC2E /* AppIcon.icon */; };
E9DF41FC29830FEC0003AAD2 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */; }; E9DF41FC29830FEC0003AAD2 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */; };
E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -107,7 +108,7 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = "<group>"; }; 9F29553D292B67B600E0E81B /* NetworkClient */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NetworkClient; path = Packages/NetworkClient; sourceTree = "<group>"; };
9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = "<group>"; }; 9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = "<group>"; };
9F2A5404296995FB009B2D7C /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; }; 9F2A5404296995FB009B2D7C /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; };
9F2A540D2969A0B0009B2D7C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 9F2A540D2969A0B0009B2D7C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
@ -132,6 +133,7 @@
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; }; 9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; }; 9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IceCubesApp.xcconfig; sourceTree = "<group>"; }; DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IceCubesApp.xcconfig; sourceTree = "<group>"; };
E9DB78972E004E4C004EAC2E /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -214,6 +216,7 @@
9FF305672CCA8515007B6B8F /* IceCubesActionExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF3056D2CCA8515007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesActionExtension; sourceTree = "<group>"; }; 9FF305672CCA8515007B6B8F /* IceCubesActionExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF3056D2CCA8515007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesActionExtension; sourceTree = "<group>"; };
9FF305712CCA8528007B6B8F /* IceCubesShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF305732CCA8528007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesShareExtension; sourceTree = "<group>"; }; 9FF305712CCA8528007B6B8F /* IceCubesShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF305732CCA8528007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesShareExtension; sourceTree = "<group>"; };
9FF305C02CCA8569007B6B8F /* IceCubesApp */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF305FB2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 9FF305FC2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 9FF305FD2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesApp; sourceTree = "<group>"; }; 9FF305C02CCA8569007B6B8F /* IceCubesApp */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF305FB2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 9FF305FC2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 9FF305FD2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesApp; sourceTree = "<group>"; };
E9FB0ACA2E09B13C002D232F /* AlternateIcons */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AlternateIcons; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -237,7 +240,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9F7788E42BE6543D004E6BEF /* Models in Frameworks */, 9F7788E42BE6543D004E6BEF /* Models in Frameworks */,
9F7788E62BE6543D004E6BEF /* Network in Frameworks */, 9F7788E62BE6543D004E6BEF /* NetworkClient in Frameworks */,
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */, 9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */,
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */, 9F7788DE2BE6543D004E6BEF /* Account in Frameworks */,
9F7788E22BE6543D004E6BEF /* Env in Frameworks */, 9F7788E22BE6543D004E6BEF /* Env in Frameworks */,
@ -273,7 +276,7 @@
9F7335EA2966B3F800AFF0BA /* Conversations in Frameworks */, 9F7335EA2966B3F800AFF0BA /* Conversations in Frameworks */,
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */, 9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */,
9F398AA92935FFDB00A889F2 /* Account in Frameworks */, 9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */, 9FBFE64E292A72BD00C250E9 /* NetworkClient in Frameworks */,
9FD542E72962D2FF0045321A /* Lists in Frameworks */, 9FD542E72962D2FF0045321A /* Lists in Frameworks */,
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */, 9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
9F5E581929545BE700A53960 /* Env in Frameworks */, 9F5E581929545BE700A53960 /* Env in Frameworks */,
@ -290,7 +293,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E92817FC298443D600875FD1 /* Network in Frameworks */, E92817FC298443D600875FD1 /* NetworkClient in Frameworks */,
E92817FA298443D600875FD1 /* Models in Frameworks */, E92817FA298443D600875FD1 /* Models in Frameworks */,
E9DF41FC29830FEC0003AAD2 /* UniformTypeIdentifiers.framework in Frameworks */, E9DF41FC29830FEC0003AAD2 /* UniformTypeIdentifiers.framework in Frameworks */,
); );
@ -302,6 +305,8 @@
9FBFE630292A715500C250E9 = { 9FBFE630292A715500C250E9 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E9FB0ACA2E09B13C002D232F /* AlternateIcons */,
E9DB78972E004E4C004EAC2E /* AppIcon.icon */,
DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */, DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */,
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */, 9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */,
9FF305C02CCA8569007B6B8F /* IceCubesApp */, 9FF305C02CCA8569007B6B8F /* IceCubesApp */,
@ -321,7 +326,7 @@
9F398AA32935F90100A889F2 /* Models */, 9F398AA32935F90100A889F2 /* Models */,
9FC2A3892B49D10000DFD1C1 /* StatusKit */, 9FC2A3892B49D10000DFD1C1 /* StatusKit */,
9FE0346A2ADD59AC00529EA8 /* MediaUI */, 9FE0346A2ADD59AC00529EA8 /* MediaUI */,
9F29553D292B67B600E0E81B /* Network */, 9F29553D292B67B600E0E81B /* NetworkClient */,
9FD542E52962D2CE0045321A /* Lists */, 9FD542E52962D2CE0045321A /* Lists */,
9F35DB4829506F7F00B3281A /* Notifications */, 9F35DB4829506F7F00B3281A /* Notifications */,
9F29553E292B6AF600E0E81B /* Timeline */, 9F29553E292B6AF600E0E81B /* Timeline */,
@ -405,7 +410,7 @@
9F7788DF2BE6543D004E6BEF /* AppAccount */, 9F7788DF2BE6543D004E6BEF /* AppAccount */,
9F7788E12BE6543D004E6BEF /* Env */, 9F7788E12BE6543D004E6BEF /* Env */,
9F7788E32BE6543D004E6BEF /* Models */, 9F7788E32BE6543D004E6BEF /* Models */,
9F7788E52BE6543D004E6BEF /* Network */, 9F7788E52BE6543D004E6BEF /* NetworkClient */,
9F7788EF2BE78E77004E6BEF /* Timeline */, 9F7788EF2BE78E77004E6BEF /* Timeline */,
); );
productName = IceCubesAppWidgetsExtensionExtension; productName = IceCubesAppWidgetsExtensionExtension;
@ -458,10 +463,11 @@
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
9FF304F52CCA8440007B6B8F /* IceCubesAppIntents */, 9FF304F52CCA8440007B6B8F /* IceCubesAppIntents */,
9FF305C02CCA8569007B6B8F /* IceCubesApp */, 9FF305C02CCA8569007B6B8F /* IceCubesApp */,
E9FB0ACA2E09B13C002D232F /* AlternateIcons */,
); );
name = IceCubesApp; name = IceCubesApp;
packageProductDependencies = ( packageProductDependencies = (
9FBFE64D292A72BD00C250E9 /* Network */, 9FBFE64D292A72BD00C250E9 /* NetworkClient */,
9F29553F292B6C3400E0E81B /* Timeline */, 9F29553F292B6C3400E0E81B /* Timeline */,
9F398AA82935FFDB00A889F2 /* Account */, 9F398AA82935FFDB00A889F2 /* Account */,
9F398AAA2935FFDB00A889F2 /* Models */, 9F398AAA2935FFDB00A889F2 /* Models */,
@ -499,7 +505,7 @@
name = IceCubesActionExtension; name = IceCubesActionExtension;
packageProductDependencies = ( packageProductDependencies = (
E92817F9298443D600875FD1 /* Models */, E92817F9298443D600875FD1 /* Models */,
E92817FB298443D600875FD1 /* Network */, E92817FB298443D600875FD1 /* NetworkClient */,
); );
productName = IceCubesActionExtension; productName = IceCubesActionExtension;
productReference = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */; productReference = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */;
@ -605,6 +611,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E9DB78982E004E4C004EAC2E /* AppIcon.icon in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -706,13 +713,13 @@
INFOPLIST_FILE = IceCubesNotifications/Info.plist; INFOPLIST_FILE = IceCubesNotifications/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications; INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -742,13 +749,13 @@
INFOPLIST_FILE = IceCubesNotifications/Info.plist; INFOPLIST_FILE = IceCubesNotifications/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications; INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -781,13 +788,13 @@
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -817,13 +824,13 @@
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -850,13 +857,13 @@
INFOPLIST_FILE = IceCubesShareExtension/Info.plist; INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes"; INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -884,13 +891,13 @@
INFOPLIST_FILE = IceCubesShareExtension/Info.plist; INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes"; INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -943,6 +950,7 @@
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = Z6P74P6T99;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -1010,6 +1018,7 @@
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = Z6P74P6T99;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -1071,14 +1080,15 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes"; PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES; SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@ -1139,14 +1149,15 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes"; PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES; SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@ -1183,13 +1194,13 @@
INFOPLIST_FILE = IceCubesActionExtension/Info.plist; INFOPLIST_FILE = IceCubesActionExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube"; INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1218,13 +1229,13 @@
INFOPLIST_FILE = IceCubesActionExtension/Info.plist; INFOPLIST_FILE = IceCubesActionExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube"; INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.12.0; MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1304,7 +1315,7 @@
repositoryURL = "https://github.com/RevenueCat/purchases-ios-spm"; repositoryURL = "https://github.com/RevenueCat/purchases-ios-spm";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 5.21.0; minimumVersion = 5.28.0;
}; };
}; };
9F9191572C6DDF20001C89E7 /* XCRemoteSwiftPackageReference "wishkit-ios" */ = { 9F9191572C6DDF20001C89E7 /* XCRemoteSwiftPackageReference "wishkit-ios" */ = {
@ -1396,9 +1407,9 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Models; productName = Models;
}; };
9F7788E52BE6543D004E6BEF /* Network */ = { 9F7788E52BE6543D004E6BEF /* NetworkClient */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Network; productName = NetworkClient;
}; };
9F7788EF2BE78E77004E6BEF /* Timeline */ = { 9F7788EF2BE78E77004E6BEF /* Timeline */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
@ -1443,9 +1454,9 @@
package = 9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */; package = 9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */;
productName = KeychainSwift; productName = KeychainSwift;
}; };
9FBFE64D292A72BD00C250E9 /* Network */ = { 9FBFE64D292A72BD00C250E9 /* NetworkClient */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Network; productName = NetworkClient;
}; };
9FC2A38A2B49D19A00DFD1C1 /* StatusKit */ = { 9FC2A38A2B49D19A00DFD1C1 /* StatusKit */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
@ -1488,9 +1499,9 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Models; productName = Models;
}; };
E92817FB298443D600875FD1 /* Network */ = { E92817FB298443D600875FD1 /* NetworkClient */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Network; productName = NetworkClient;
}; };
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };

View file

@ -1,5 +1,5 @@
{ {
"originHash" : "9d42c0a5696b2ea2df5db9f4f0bcf2ed56558636f36278a222b188cc2df705a7", "originHash" : "f4a38cf3b71adee5c987dcb603362633b86bbbd38c6dc4c897085f53a0ea9d44",
"pins" : [ "pins" : [
{ {
"identity" : "bodega", "identity" : "bodega",
@ -22,10 +22,10 @@
{ {
"identity" : "emojitext", "identity" : "emojitext",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/divadretlaw/EmojiText", "location" : "https://github.com/Dimillian/EmojiText",
"state" : { "state" : {
"revision" : "3b0417959e23307d38fd0f72ba0110daa2122e3f", "branch" : "fix-ios26",
"version" : "4.2.0" "revision" : "0305cf9c0ecfe661bbab2b834167e261b89b3d7a"
} }
}, },
{ {
@ -60,8 +60,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/RevenueCat/purchases-ios-spm", "location" : "https://github.com/RevenueCat/purchases-ios-spm",
"state" : { "state" : {
"revision" : "adde1aa9c5fdb8cb9178ae1eed0981adcff07dd3", "revision" : "2f43b88b893880848983af9b885185e20850a1df",
"version" : "5.21.0" "version" : "5.28.0"
} }
}, },
{ {
@ -94,7 +94,7 @@
{ {
"identity" : "swift-markdown", "identity" : "swift-markdown",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-markdown", "location" : "https://github.com/swiftlang/swift-markdown",
"state" : { "state" : {
"revision" : "ea79e83c8744d2b50b0dc2d5bbd1e857e1253bf9", "revision" : "ea79e83c8744d2b50b0dc2d5bbd1e857e1253bf9",
"version" : "0.6.0" "version" : "0.6.0"

View file

@ -5,54 +5,70 @@ import DesignSystem
import Env import Env
import KeychainSwift import KeychainSwift
import MediaUI import MediaUI
import Network import Models
import NetworkClient
import RevenueCat import RevenueCat
import StatusKit import StatusKit
import SwiftData
import SwiftUI import SwiftUI
import Timeline import Timeline
@MainActor @MainActor
struct AppView: View { struct AppView: View {
@Environment(\.modelContext) private var context
@Environment(\.openWindow) var openWindow
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(AppAccountsManager.self) private var appAccountsManager @Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(StreamWatcher.self) private var watcher @Environment(StreamWatcher.self) private var watcher
@Environment(CurrentAccount.self) private var currentAccount
@Environment(\.openWindow) var openWindow
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Binding var selectedTab: AppTab @Binding var selectedTab: AppTab
@Binding var appRouterPath: RouterPath @Binding var appRouterPath: RouterPath
@State var iosTabs = iOSTabs.shared @State var iosTabs = iOSTabs.shared
@State var sidebarTabs = SidebarTabs.shared
@State var selectedTabScrollToTop: Int = -1 @State var selectedTabScrollToTop: Int = -1
@State var timeline: TimelineFilter = .home
@AppStorage("timeline_pinned_filters") private var pinnedFilters: [TimelineFilter] = []
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
var body: some View { var body: some View {
switch UIDevice.current.userInterfaceIdiom { HStack(spacing: 0) {
case .vision:
tabBarView
case .pad, .mac:
#if !os(visionOS)
sidebarView
#else
tabBarView
#endif
default:
tabBarView tabBarView
.tabViewStyle(.sidebarAdaptable)
if (horizontalSizeClass == .regular && (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac)),
appAccountsManager.currentClient.isAuth,
userPreferences.showiPadSecondaryColumn
{
Divider().edgesIgnoringSafeArea(.all)
notificationsSecondaryColumn
}
} }
} }
var availableTabs: [AppTab] { var availableSections: [SidebarSections] {
guard appAccountsManager.currentClient.isAuth else { guard appAccountsManager.currentClient.isAuth else {
return AppTab.loggedOutTab() return [SidebarSections.loggedOutTabs]
} }
if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact { if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact {
return iosTabs.tabs return [SidebarSections.iosTabs]
} else if UIDevice.current.userInterfaceIdiom == .vision { } else if UIDevice.current.userInterfaceIdiom == .vision {
return AppTab.visionOSTab() return [SidebarSections.visionOSTabs]
} }
return sidebarTabs.tabs.map { $0.tab } var sections = SidebarSections.macOrIpadOSSections
if !localTimelines.isEmpty {
sections.append(.localTimeline)
}
if !tagGroups.isEmpty {
sections.append(.tagGroup)
}
sections.append(.app)
return sections
} }
@ViewBuilder @ViewBuilder
@ -66,19 +82,49 @@ struct AppView: View {
updateTab(with: newTab) updateTab(with: newTab)
}) })
) { ) {
ForEach(availableTabs) { tab in ForEach(availableSections) { section in
tab.makeContentView(selectedTab: $selectedTab) TabSection(section.title) {
.tabItem { if section == .localTimeline {
if userPreferences.showiPhoneTabLabel { ForEach(localTimelines) { timeline in
tab.label let tab = AppTab.anyTimelineFilter(
.environment(\.symbolVariants, tab == selectedTab ? .fill : .none) filter: .remoteLocal(server: timeline.instance, filter: .local))
} else { Tab(value: tab) {
Image(systemName: tab.iconName) tab.makeContentView(
homeTimeline: $timeline, selectedTab: $selectedTab, pinnedFilters: $pinnedFilters)
} label: {
tab.label.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
}
.tabPlacement(tab.tabPlacement)
}
} else if section == .tagGroup {
ForEach(tagGroups) { tagGroup in
let tab = AppTab.anyTimelineFilter(
filter: TimelineFilter.tagGroup(
title: tagGroup.title,
tags: tagGroup.tags,
symbolName: tagGroup.symbolName))
Tab(value: tab) {
tab.makeContentView(
homeTimeline: $timeline, selectedTab: $selectedTab, pinnedFilters: $pinnedFilters)
} label: {
tab.label.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
}
.tabPlacement(tab.tabPlacement)
}
} else {
ForEach(section.tabs) { tab in
Tab(value: tab, role: tab == .explore ? .search : .none) {
tab.makeContentView(
homeTimeline: $timeline, selectedTab: $selectedTab, pinnedFilters: $pinnedFilters)
} label: {
tab.label.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
}
.tabPlacement(tab.tabPlacement)
.badge(badgeFor(tab: tab))
} }
} }
.tag(tab) }
.badge(badgeFor(tab: tab)) .tabPlacement(.sidebarOnly)
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .tabBar)
} }
} }
.id(appAccountsManager.currentClient.id) .id(appAccountsManager.currentClient.id)
@ -102,7 +148,7 @@ struct AppView: View {
SoundEffectManager.shared.playSound(.tabSelection) SoundEffectManager.shared.playSound(.tabSelection)
if selectedTab == newTab { if selectedTab == newTab {
selectedTabScrollToTop = newTab.rawValue selectedTabScrollToTop = newTab.id
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
selectedTabScrollToTop = -1 selectedTabScrollToTop = -1
} }
@ -122,66 +168,6 @@ struct AppView: View {
return 0 return 0
} }
#if !os(visionOS)
var sidebarView: some View {
SideBarView(
selectedTab: .init(
get: {
selectedTab
},
set: { newTab in
updateTab(with: newTab)
}), tabs: availableTabs
) {
HStack(spacing: 0) {
if #available(iOS 18.0, *) {
baseTabView
#if targetEnvironment(macCatalyst)
.tabViewStyle(.sidebarAdaptable)
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
tabview.sidebar.isHidden = true
}
#else
.tabViewStyle(.tabBarOnly)
#endif
} else {
baseTabView
}
if horizontalSizeClass == .regular,
appAccountsManager.currentClient.isAuth,
userPreferences.showiPadSecondaryColumn
{
Divider().edgesIgnoringSafeArea(.all)
notificationsSecondaryColumn
}
}
}
.environment(appRouterPath)
.environment(\.selectedTabScrollToTop, selectedTabScrollToTop)
}
#endif
private var baseTabView: some View {
TabView(selection: $selectedTab) {
ForEach(availableTabs) { tab in
tab
.makeContentView(selectedTab: $selectedTab)
.toolbar(horizontalSizeClass == .regular ? .hidden : .visible, for: .tabBar)
.tabItem {
tab.label
}
.tag(tab)
}
}
#if !os(visionOS)
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
tabview.tabBar.isHidden = horizontalSizeClass == .regular
tabview.customizableViewControllers = []
tabview.moreNavigationController.isNavigationBarHidden = true
}
#endif
}
var notificationsSecondaryColumn: some View { var notificationsSecondaryColumn: some View {
NotificationsTab(selectedTab: .constant(.notifications), lockedType: nil) NotificationsTab(selectedTab: .constant(.notifications), lockedType: nil)
.environment(\.isSecondaryColumn, true) .environment(\.isSecondaryColumn, true)

View file

@ -26,23 +26,18 @@ extension IceCubesApp {
.environment(appIntentService) .environment(appIntentService)
.environment(\.isSupporter, isSupporter) .environment(\.isSupporter, isSupporter)
.sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in .sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in
if #available(iOS 18.0, *) { if let namespace = quickLook.namespace {
MediaUIView( MediaUIView(
selectedAttachment: selectedMediaAttachment, selectedAttachment: selectedMediaAttachment,
attachments: quickLook.mediaAttachments attachments: quickLook.mediaAttachments
) )
.presentationBackground(.ultraThinMaterial) .navigationTransition(.zoom(sourceID: selectedMediaAttachment.id, in: namespace))
.presentationBackground(theme.primaryBackgroundColor)
.presentationCornerRadius(16) .presentationCornerRadius(16)
.presentationSizing(.page) .presentationSizing(.page)
.withEnvironments() .withEnvironments()
} else { } else {
MediaUIView( EmptyView()
selectedAttachment: selectedMediaAttachment,
attachments: quickLook.mediaAttachments
)
.presentationBackground(.ultraThinMaterial)
.presentationCornerRadius(16)
.withEnvironments()
} }
} }
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in .onChange(of: pushNotificationsService.handledNotification) { _, newValue in
@ -164,6 +159,8 @@ extension IceCubesApp {
{ {
appRouterPath.presentedSheet = .imageURL( appRouterPath.presentedSheet = .imageURL(
urls: urls, urls: urls,
caption: imageIntent.caption,
altTexts: imageIntent.altText.map { [$0] },
visibility: userPreferences.postVisibility) visibility: userPreferences.postVisibility)
} }
} }
@ -171,10 +168,6 @@ extension IceCubesApp {
extension Scene { extension Scene {
func windowResize() -> some Scene { func windowResize() -> some Scene {
if #available(iOS 18.0, *) { return self.windowResizability(.contentSize)
return self.windowResizability(.contentSize)
} else {
return self.defaultSize(width: 1100, height: 1400)
}
} }
} }

View file

@ -5,7 +5,7 @@ import DesignSystem
import Env import Env
import KeychainSwift import KeychainSwift
import MediaUI import MediaUI
import Network import NetworkClient
import RevenueCat import RevenueCat
import StatusKit import StatusKit
import SwiftUI import SwiftUI
@ -33,6 +33,8 @@ struct IceCubesApp: App {
@State var appRouterPath = RouterPath() @State var appRouterPath = RouterPath()
@State var isSupporter: Bool = false @State var isSupporter: Bool = false
@Namespace var namespace
init() { init() {
#if DEBUG #if DEBUG
@ -47,7 +49,8 @@ struct IceCubesApp: App {
otherScenes otherScenes
} }
func setNewClientsInEnv(client: Client) { func setNewClientsInEnv(client: MastodonClient) {
quickLook.namespace = namespace
currentAccount.setClient(client: client) currentAccount.setClient(client: client)
currentInstance.setClient(client: client) currentInstance.setClient(client: client)
userPreferences.setClient(client: client) userPreferences.setClient(client: client)
@ -141,6 +144,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
super.buildMenu(with: builder) super.buildMenu(with: builder)
builder.remove(menu: .document) builder.remove(menu: .document)
builder.remove(menu: .toolbar) builder.remove(menu: .toolbar)
builder.remove(menu: .sidebar)
} }
} }

View file

@ -1,261 +0,0 @@
import Account
import AppAccount
import DesignSystem
import Env
import Models
import SwiftUI
import SwiftUIIntrospect
@MainActor
struct SideBarView<Content: View>: View {
@Environment(\.openWindow) private var openWindow
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(AppAccountsManager.self) private var appAccounts
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Theme.self) private var theme
@Environment(StreamWatcher.self) private var watcher
@Environment(UserPreferences.self) private var userPreferences
@Environment(RouterPath.self) private var routerPath
@Binding var selectedTab: AppTab
var tabs: [AppTab]
@ViewBuilder var content: () -> Content
@State private var sidebarTabs = SidebarTabs.shared
private func badgeFor(tab: AppTab) -> Int {
if tab == .notifications, selectedTab != tab,
let token = appAccounts.currentAccount.oauthToken
{
return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0)
}
return 0
}
private func makeIconForTab(tab: AppTab) -> some View {
ZStack(alignment: .topTrailing) {
HStack {
SideBarIcon(
systemIconName: tab.iconName,
isSelected: tab == selectedTab)
if userPreferences.isSidebarExpanded {
Text(tab.title)
.font(.headline)
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(
width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24,
height: 50
)
.background(
tab == selectedTab ? theme.primaryBackgroundColor : .clear,
in: RoundedRectangle(cornerRadius: 8)
)
.cornerRadius(8)
.shadow(color: tab == selectedTab ? .black.opacity(0.2) : .clear, radius: 5)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(tab == selectedTab ? theme.labelColor.opacity(0.1) : .clear, lineWidth: 1)
)
let badge = badgeFor(tab: tab)
if badge > 0 {
makeBadgeView(count: badge)
}
}
}
private func makeBadgeView(count: Int) -> some View {
ZStack {
Circle()
.fill(.red)
Text(count > 99 ? "99+" : String(count))
.foregroundColor(.white)
.font(.caption2)
}
.frame(width: 24, height: 24)
.offset(x: 5, y: -5)
}
private var postButton: some View {
Button {
#if targetEnvironment(macCatalyst) || os(visionOS)
openWindow(
value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)
)
#else
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
#endif
} label: {
Image(systemName: "square.and.pencil")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 30)
.offset(x: 2, y: -2)
}
.buttonStyle(.borderedProminent)
.help(AppTab.post.title)
}
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
Button {
if account.id == appAccounts.currentAccount.id {
selectedTab = .profile
SoundEffectManager.shared.playSound(.tabSelection)
} else {
var transation = Transaction()
transation.disablesAnimations = true
withTransaction(transation) {
appAccounts.currentAccount = account
}
}
} label: {
ZStack(alignment: .topTrailing) {
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
{
makeBadgeView(count: notificationsCount)
}
}
.padding(.leading, userPreferences.isSidebarExpanded ? 16 : 0)
}
.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 {
AppTab.profile.title
}
}
private var tabsView: some View {
ForEach(tabs) { tab in
if tab != .profile && sidebarTabs.isEnabled(tab) {
Button {
// ensure keyboard is always dismissed when selecting a tab
hideKeyboard()
selectedTab = tab
SoundEffectManager.shared.playSound(.tabSelection)
if tab == .notifications {
if let token = appAccounts.currentAccount.oauthToken {
userPreferences.notificationsCount[token] = 0
}
watcher.unreadNotificationsCount = 0
}
} label: {
makeIconForTab(tab: tab)
}
.help(tab.title)
}
}
}
var body: some View {
@Bindable var routerPath = routerPath
HStack(spacing: 0) {
if horizontalSizeClass == .regular {
ScrollView {
VStack(alignment: .center) {
if appAccounts.availableAccounts.isEmpty {
tabsView
} else {
ForEach(appAccounts.availableAccounts) { account in
makeAccountButton(
account: account,
showBadge: account.id != appAccounts.currentAccount.id)
if account.id == appAccounts.currentAccount.id {
tabsView
}
}
}
}
}
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
.scrollContentBackground(.hidden)
.background(.thinMaterial)
.safeAreaInset(
edge: .bottom,
content: {
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: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
.background(.thinMaterial)
})
Divider().edgesIgnoringSafeArea(.all)
}
content()
}
.background(.thinMaterial)
.edgesIgnoringSafeArea(.bottom)
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
}
}
private struct SideBarIcon: View {
@Environment(Theme.self) private var theme
let systemIconName: String
let isSelected: Bool
@State private var isHovered: Bool = false
var body: some View {
Image(systemName: systemIconName)
.font(.title2)
.fontWeight(.medium)
.foregroundColor(isSelected ? theme.tintColor : theme.labelColor)
.symbolVariant(isSelected ? .fill : .none)
.scaleEffect(isHovered ? 0.8 : 1.0)
.onHover { isHovered in
withAnimation(.interpolatingSpring(stiffness: 300, damping: 15)) {
self.isHovered = isHovered
}
}
.frame(width: 50, height: 40)
}
}
extension View {
@MainActor func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
}

View file

@ -1,7 +1,7 @@
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import StatusKit import StatusKit
import SwiftUI import SwiftUI
@ -9,7 +9,7 @@ public struct ReportView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
let status: Status let status: Status
@State private var commentText: String = "" @State private var commentText: String = ""

View file

@ -18,65 +18,72 @@ extension View {
func withAppRouter() -> some View { func withAppRouter() -> some View {
navigationDestination(for: RouterDestination.self) { destination in navigationDestination(for: RouterDestination.self) { destination in
switch destination { switch destination {
case let .accountDetail(id): case .accountDetail(let id):
AccountDetailView(accountId: id) AccountDetailView(accountId: id)
case let .accountDetailWithAccount(account): case .accountDetailWithAccount(let account):
AccountDetailView(account: account) AccountDetailView(account: account)
case let .accountSettingsWithAccount(account, appAccount): case .accountSettingsWithAccount(let account, let appAccount):
AccountSettingsView(account: account, appAccount: appAccount) AccountSettingsView(account: account, appAccount: appAccount)
case let .accountMediaGridView(account, initialMedia): case .accountMediaGridView(let account, let initialMedia):
AccountDetailMediaGridView(account: account, initialMediaStatuses: initialMedia) AccountDetailMediaGridView(account: account, initialMediaStatuses: initialMedia)
case let .statusDetail(id): case .statusDetail(let id):
StatusDetailView(statusId: id) StatusDetailView(statusId: id)
case let .statusDetailWithStatus(status): case .statusDetailWithStatus(let status):
StatusDetailView(status: status) StatusDetailView(status: status)
case let .remoteStatusDetail(url): case .remoteStatusDetail(let url):
StatusDetailView(remoteStatusURL: url) StatusDetailView(remoteStatusURL: url)
case let .conversationDetail(conversation): case .conversationDetail(let conversation):
ConversationDetailView(conversation: conversation) ConversationDetailView(conversation: conversation)
case let .hashTag(tag, accountId): case .hashTag(let tag, let accountId):
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), TimelineView(
pinnedFilters: .constant([]), timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
selectedTagGroup: .constant(nil), pinnedFilters: .constant([]),
canFilterTimeline: false) selectedTagGroup: .constant(nil),
case let .list(list): canFilterTimeline: false)
TimelineView(timeline: .constant(.list(list: list)), case .list(let list):
pinnedFilters: .constant([]), TimelineView(
selectedTagGroup: .constant(nil), timeline: .constant(.list(list: list)),
canFilterTimeline: false) pinnedFilters: .constant([]),
case let .linkTimeline(url, title): selectedTagGroup: .constant(nil),
TimelineView(timeline: .constant(.link(url: url, title: title)), canFilterTimeline: false)
pinnedFilters: .constant([]), case .linkTimeline(let url, let title):
selectedTagGroup: .constant(nil), TimelineView(
canFilterTimeline: false) timeline: .constant(.link(url: url, title: title)),
case let .following(id): pinnedFilters: .constant([]),
selectedTagGroup: .constant(nil),
canFilterTimeline: false)
case .following(let id):
AccountsListView(mode: .following(accountId: id)) AccountsListView(mode: .following(accountId: id))
case let .followers(id): case .followers(let id):
AccountsListView(mode: .followers(accountId: id)) AccountsListView(mode: .followers(accountId: id))
case let .favoritedBy(id): case .favoritedBy(let id):
AccountsListView(mode: .favoritedBy(statusId: id)) AccountsListView(mode: .favoritedBy(statusId: id))
case let .rebloggedBy(id): case .rebloggedBy(let id):
AccountsListView(mode: .rebloggedBy(statusId: id)) AccountsListView(mode: .rebloggedBy(statusId: id))
case let .accountsList(accounts): case .accountsList(let accounts):
AccountsListView(mode: .accountsList(accounts: accounts)) AccountsListView(mode: .accountsList(accounts: accounts))
case .trendingTimeline: case .trendingTimeline:
TimelineView(timeline: .constant(.trending), TimelineView(
pinnedFilters: .constant([]), timeline: .constant(.trending),
selectedTagGroup: .constant(nil), pinnedFilters: .constant([]),
canFilterTimeline: false) selectedTagGroup: .constant(nil),
case let .trendingLinks(cards): canFilterTimeline: false)
case .trendingLinks(let cards):
TrendingLinksListView(cards: cards) TrendingLinksListView(cards: cards)
case let .tagsList(tags): case .tagsList(let tags):
TagsListView(tags: tags) TagsListView(tags: tags)
case .notificationsRequests: case .notificationsRequests:
NotificationsRequestsListView() NotificationsRequestsListView()
case let .notificationForAccount(accountId): case .notificationForAccount(let accountId):
NotificationsListView(lockedType: nil, NotificationsListView(
lockedAccountId: accountId) lockedType: nil,
lockedAccountId: accountId)
case .blockedAccounts: case .blockedAccounts:
AccountsListView(mode: .blocked) AccountsListView(mode: .blocked)
case .mutedAccounts: case .mutedAccounts:
AccountsListView(mode: .muted) AccountsListView(mode: .muted)
case .conversations:
ConversationsListView()
} }
} }
} }
@ -84,37 +91,37 @@ extension View {
func withSheetDestinations(sheetDestinations: Binding<SheetDestination?>) -> some View { func withSheetDestinations(sheetDestinations: Binding<SheetDestination?>) -> some View {
sheet(item: sheetDestinations) { destination in sheet(item: sheetDestinations) { destination in
switch destination { switch destination {
case let .replyToStatusEditor(status): case .replyToStatusEditor(let status):
StatusEditor.MainView(mode: .replyTo(status: status)) StatusEditor.MainView(mode: .replyTo(status: status))
.withEnvironments() .withEnvironments()
case let .newStatusEditor(visibility): case .newStatusEditor(let visibility):
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility)) StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
.withEnvironments() .withEnvironments()
case let .prefilledStatusEditor(text, visibility): case .prefilledStatusEditor(let text, let visibility):
StatusEditor.MainView(mode: .new(text: text, visibility: visibility)) StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
.withEnvironments() .withEnvironments()
case let .imageURL(urls, visibility): case .imageURL(let urls, let caption, let altTexts, let visibility):
StatusEditor.MainView(mode: .imageURL(urls: urls, visibility: visibility)) StatusEditor.MainView(mode: .imageURL(urls: urls, caption: caption, altTexts: altTexts, visibility: visibility))
.withEnvironments() .withEnvironments()
case let .editStatusEditor(status): case .editStatusEditor(let status):
StatusEditor.MainView(mode: .edit(status: status)) StatusEditor.MainView(mode: .edit(status: status))
.withEnvironments() .withEnvironments()
case let .quoteStatusEditor(status): case .quoteStatusEditor(let status):
StatusEditor.MainView(mode: .quote(status: status)) StatusEditor.MainView(mode: .quote(status: status))
.withEnvironments() .withEnvironments()
case let .quoteLinkStatusEditor(link): case .quoteLinkStatusEditor(let link):
StatusEditor.MainView(mode: .quoteLink(link: link)) StatusEditor.MainView(mode: .quoteLink(link: link))
.withEnvironments() .withEnvironments()
case let .mentionStatusEditor(account, visibility): case .mentionStatusEditor(let account, let visibility):
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility)) StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
.withEnvironments() .withEnvironments()
case .listCreate: case .listCreate:
ListCreateView() ListCreateView()
.withEnvironments() .withEnvironments()
case let .listEdit(list): case .listEdit(let list):
ListEditView(list: list) ListEditView(list: list)
.withEnvironments() .withEnvironments()
case let .listAddAccount(account): case .listAddAccount(let account):
ListAddAccountView(account: account) ListAddAccountView(account: account)
.withEnvironments() .withEnvironments()
case .addAccount: case .addAccount:
@ -126,7 +133,7 @@ extension View {
case .addTagGroup: case .addTagGroup:
EditTagGroupView() EditTagGroupView()
.withEnvironments() .withEnvironments()
case let .statusEditHistory(status): case .statusEditHistory(let status):
StatusEditHistoryView(statusId: status) StatusEditHistoryView(statusId: status)
.withEnvironments() .withEnvironments()
case .settings: case .settings:
@ -134,7 +141,9 @@ extension View {
.withEnvironments() .withEnvironments()
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light) .preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
case .accountPushNotficationsSettings: case .accountPushNotficationsSettings:
if let subscription = PushNotificationsService.shared.subscriptions.first(where: { $0.account.token == AppAccountsManager.shared.currentAccount.oauthToken }) { if let subscription = PushNotificationsService.shared.subscriptions.first(where: {
$0.account.token == AppAccountsManager.shared.currentAccount.oauthToken
}) {
NavigationSheet { PushNotificationsView(subscription: subscription) } NavigationSheet { PushNotificationsView(subscription: subscription) }
.withEnvironments() .withEnvironments()
} else { } else {
@ -146,19 +155,17 @@ extension View {
case .support: case .support:
NavigationSheet { SupportAppView() } NavigationSheet { SupportAppView() }
.withEnvironments() .withEnvironments()
case let .report(status): case .report(let status):
ReportView(status: status) ReportView(status: status)
.withEnvironments() .withEnvironments()
case let .shareImage(image, status): case .shareImage(image: let image, status: let status):
ActivityView(image: image, status: status) ActivityView(image: image, status: status)
.withEnvironments() .withEnvironments()
case let .editTagGroup(tagGroup, onSaved): case .editTagGroup(let tagGroup, let onSaved):
EditTagGroupView(tagGroup: tagGroup, onSaved: onSaved) EditTagGroupView(tagGroup: tagGroup, onSaved: onSaved)
.withEnvironments() .withEnvironments()
case .timelineContentFilter: case .timelineContentFilter:
NavigationSheet { TimelineContentFilterView() } TimelineContentFilterView()
.presentationDetents([.medium])
.presentationBackground(.thinMaterial)
.withEnvironments() .withEnvironments()
case .accountEditInfo: case .accountEditInfo:
EditAccountView() EditAccountView()
@ -216,19 +223,25 @@ struct ActivityView: UIViewControllerRepresentable {
image image
} }
func activityViewController(_: UIActivityViewController, func activityViewController(
itemForActivityType _: UIActivity.ActivityType?) -> Any? _: UIActivityViewController,
{ itemForActivityType _: UIActivity.ActivityType?
) -> Any? {
nil nil
} }
} }
func makeUIViewController(context _: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController { func makeUIViewController(context _: UIViewControllerRepresentableContext<ActivityView>)
UIActivityViewController(activityItems: [image, LinkDelegate(image: image, status: status)], -> UIActivityViewController
applicationActivities: nil) {
UIActivityViewController(
activityItems: [image, LinkDelegate(image: image, status: status)],
applicationActivities: nil)
} }
func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext<ActivityView>) {} func updateUIViewController(
_: UIActivityViewController, context _: UIViewControllerRepresentableContext<ActivityView>
) {}
} }
extension URL: @retroactive Identifiable { extension URL: @retroactive Identifiable {

View file

@ -60,20 +60,6 @@ private struct SafariRouter: ViewModifier {
UIApplication.shared.open(url) UIApplication.shared.open(url)
return .handled return .handled
} }
} else if url.query()?.contains("callback=") == false,
url.host() == AppInfo.premiumInstance,
let accountName = appAccount.currentAccount.accountName
{
let newURL = url.appending(queryItems: [
.init(name: "callback", value: "icecubesapp://subclub"),
.init(name: "id", value: "@\(accountName)"),
])
#if !os(visionOS)
return safariManager.open(newURL)
#else
return .systemAction
#endif
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction } guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }

View file

@ -3,7 +3,7 @@ import DesignSystem
import Env import Env
import Explore import Explore
import Models import Models
import Network import NetworkClient
import SwiftUI import SwiftUI
@MainActor @MainActor
@ -11,7 +11,7 @@ struct ExploreTab: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var preferences @Environment(UserPreferences.self) private var preferences
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@State private var routerPath = RouterPath() @State private var routerPath = RouterPath()
var body: some View { var body: some View {
@ -19,7 +19,6 @@ struct ExploreTab: View {
ExploreView() ExploreView()
.withAppRouter() .withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.toolbar { .toolbar {
ToolbarTab(routerPath: $routerPath) ToolbarTab(routerPath: $routerPath)
} }

View file

@ -4,14 +4,14 @@ import Conversations
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import SwiftUI import SwiftUI
@MainActor @MainActor
struct MessagesTab: View { struct MessagesTab: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(StreamWatcher.self) private var watcher @Environment(StreamWatcher.self) private var watcher
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(AppAccountsManager.self) private var appAccount @Environment(AppAccountsManager.self) private var appAccount
@State private var routerPath = RouterPath() @State private var routerPath = RouterPath()
@ -24,7 +24,6 @@ struct MessagesTab: View {
.toolbar { .toolbar {
ToolbarTab(routerPath: $routerPath) ToolbarTab(routerPath: $routerPath)
} }
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.id(client.id) .id(client.id)
} }
.onChange(of: client.id) { .onChange(of: client.id) {

View file

@ -1,7 +1,7 @@
import AppAccount import AppAccount
import DesignSystem import DesignSystem
import Env import Env
import Network import NetworkClient
import SwiftUI import SwiftUI
@MainActor @MainActor
@ -12,7 +12,7 @@ struct NavigationTab<Content: View>: View {
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
var content: () -> Content var content: () -> Content
@ -32,7 +32,6 @@ struct NavigationTab<Content: View>: View {
.toolbar { .toolbar {
ToolbarTab(routerPath: $routerPath) ToolbarTab(routerPath: $routerPath)
} }
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.onChange(of: client.id) { .onChange(of: client.id) {
routerPath.path = [] routerPath.path = []
} }

View file

@ -2,7 +2,7 @@ import AppAccount
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import Notifications import Notifications
import SwiftUI import SwiftUI
import Timeline import Timeline
@ -13,7 +13,7 @@ struct NotificationsTab: View {
@Environment(\.scenePhase) private var scenePhase @Environment(\.scenePhase) private var scenePhase
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@Environment(StreamWatcher.self) private var watcher @Environment(StreamWatcher.self) private var watcher
@Environment(AppAccountsManager.self) private var appAccount @Environment(AppAccountsManager.self) private var appAccount
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@ -37,15 +37,20 @@ struct NotificationsTab: View {
} label: { } label: {
Image(systemName: "bell") Image(systemName: "bell")
} }
.tint(.label)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarTrailing)
} }
ToolbarTab(routerPath: $routerPath) ToolbarTab(routerPath: $routerPath)
} }
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.id(client.id) .id(client.id)
} }
.onAppear { .onAppear {
routerPath.client = client routerPath.client = client
clearNotifications() DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
clearNotifications()
}
} }
.withSafariRouter() .withSafariRouter()
.environment(routerPath) .environment(routerPath)

View file

@ -4,14 +4,14 @@ import Conversations
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import SwiftUI import SwiftUI
@MainActor @MainActor
struct ProfileTab: View { struct ProfileTab: View {
@Environment(AppAccountsManager.self) private var appAccount @Environment(AppAccountsManager.self) private var appAccount
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@State private var routerPath = RouterPath() @State private var routerPath = RouterPath()
@ -21,7 +21,6 @@ struct ProfileTab: View {
AccountDetailView(account: account) AccountDetailView(account: account)
.withAppRouter() .withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.id(account.id) .id(account.id)
} else { } else {
AccountDetailView(account: .placeholder()) AccountDetailView(account: .placeholder())

View file

@ -2,14 +2,14 @@ import Account
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import SwiftUI import SwiftUI
@MainActor @MainActor
struct AboutView: View { struct AboutView: View {
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@State private var dimillianAccount: AccountsListRowViewModel? @State private var dimillianAccount: AccountsListRowViewModel?
@State private var iceCubesAccount: AccountsListRowViewModel? @State private var iceCubesAccount: AccountsListRowViewModel?
@ -176,7 +176,7 @@ struct AboutView: View {
} }
} }
private func fetchAccountViewModel(_ client: Client, account: String) async throws private func fetchAccountViewModel(_ client: MastodonClient, account: String) async throws
-> AccountsListRowViewModel -> AccountsListRowViewModel
{ {
let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account)) let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account))

View file

@ -3,7 +3,7 @@ import AppAccount
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import SwiftUI import SwiftUI
import Timeline import Timeline
@ -16,7 +16,7 @@ struct AccountSettingsView: View {
@Environment(CurrentInstance.self) private var currentInstance @Environment(CurrentInstance.self) private var currentInstance
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(AppAccountsManager.self) private var appAccountsManager @Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@State private var cachedPostsCount: Int = 0 @State private var cachedPostsCount: Int = 0
@ -84,7 +84,7 @@ struct AccountSettingsView: View {
Button(role: .destructive) { Button(role: .destructive) {
if let token = appAccount.oauthToken { if let token = appAccount.oauthToken {
Task { Task {
let client = Client(server: appAccount.server, oauthToken: token) let client = MastodonClient(server: appAccount.server, oauthToken: token)
await timelineCache.clearCache(for: client.id) await timelineCache.clearCache(for: client.id)
if let sub = pushNotifications.subscriptions.first(where: { if let sub = pushNotifications.subscriptions.first(where: {
$0.account.token == token $0.account.token == token

View file

@ -4,7 +4,7 @@ import Combine
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import NukeUI import NukeUI
import SafariServices import SafariServices
import SwiftUI import SwiftUI
@ -25,7 +25,7 @@ struct AddAccountView: View {
@State private var instanceName: String = "" @State private var instanceName: String = ""
@State private var instance: Instance? @State private var instance: Instance?
@State private var isSigninIn = false @State private var isSigninIn = false
@State private var signInClient: Client? @State private var signInClient: MastodonClient?
@State private var instances: [InstanceSocial] = [] @State private var instances: [InstanceSocial] = []
@State private var instanceFetchError: LocalizedStringKey? @State private var instanceFetchError: LocalizedStringKey?
@State private var instanceSocialClient = InstanceSocialClient() @State private var instanceSocialClient = InstanceSocialClient()
@ -123,7 +123,7 @@ struct AddAccountView: View {
do { do {
// bare bones preflight for domain validity // bare bones preflight for domain validity
let instanceDetailClient = Client(server: sanitizedName) let instanceDetailClient = MastodonClient(server: sanitizedName)
if instanceDetailClient.server.contains("."), if instanceDetailClient.server.contains("."),
instanceDetailClient.server.last != "." instanceDetailClient.server.last != "."
{ {
@ -160,32 +160,40 @@ struct AddAccountView: View {
private var signInSection: some View { private var signInSection: some View {
Section { Section {
Button { if #available(iOS 26.0, *) {
withAnimation { signinButton
isSigninIn = true .buttonStyle(.glassProminent)
} } else {
Task { signinButton
await signIn() .buttonStyle(.borderedProminent)
} }
} label: { }
HStack { }
Spacer()
if isSigninIn || !sanitizedName.isEmpty && instance == nil { private var signinButton: some View {
ProgressView() Button {
.id(sanitizedName) withAnimation {
.tint(theme.labelColor) isSigninIn = true
} else { }
Text("account.add.sign-in") Task {
.font(.scaledHeadline) await signIn()
} }
Spacer() } label: {
HStack {
if isSigninIn || !sanitizedName.isEmpty && instance == nil {
ProgressView()
.id(sanitizedName)
.tint(theme.labelColor)
} else {
Text("account.add.sign-in")
.font(.scaledHeadline)
} }
} }
.buttonStyle(PlainButtonStyle()) .frame(maxWidth: .infinity)
.frame(height: 44)
} }
#if !os(visionOS) .listRowInsets(.init())
.listRowBackground(theme.tintColor) .listRowBackground(Color.clear)
#endif
} }
private var instancesListView: some View { private var instancesListView: some View {
@ -294,7 +302,7 @@ struct AddAccountView: View {
} }
do { do {
let oauthToken = try await client.continueOauthFlow(url: url) let oauthToken = try await client.continueOauthFlow(url: url)
let client = Client(server: client.server, oauthToken: oauthToken) let client = MastodonClient(server: client.server, oauthToken: oauthToken)
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials) let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
Telemetry.signal("account.added") Telemetry.signal("account.added")
appAccountsManager.add( appAccountsManager.add(

View file

@ -2,7 +2,7 @@ import AppAccount
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import NukeUI import NukeUI
import SwiftUI import SwiftUI
import Timeline import Timeline

View file

@ -2,7 +2,7 @@ import Combine
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import Observation import Observation
import StatusKit import StatusKit
import SwiftUI import SwiftUI
@ -31,7 +31,7 @@ struct DisplaySettingsView: View {
private let previewStatusViewModel = StatusRowViewModel( private let previewStatusViewModel = StatusRowViewModel(
status: Status.placeholder(forSettings: true, language: "la"), status: Status.placeholder(forSettings: true, language: "la"),
client: Client(server: ""), client: MastodonClient(server: ""),
routerPath: RouterPath()) // translate from latin button routerPath: RouterPath()) // translate from latin button
var body: some View { var body: some View {
@ -255,7 +255,6 @@ struct DisplaySettingsView: View {
} }
} }
Toggle("settings.display.show-account-popover", isOn: $userPreferences.showAccountPopover) Toggle("settings.display.show-account-popover", isOn: $userPreferences.showAccountPopover)
Toggle("Show Content Gradient", isOn: $theme.showContentGradient)
Toggle("Compact Layout", isOn: $theme.compactLayoutPadding) Toggle("Compact Layout", isOn: $theme.compactLayoutPadding)
} }
#if !os(visionOS) #if !os(visionOS)

View file

@ -17,9 +17,14 @@ struct IconSelectorView: View {
} }
case primary = 0 case primary = 0
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14, case alt1, alt2
alt15
case alt16, alt17, alt18, alt19, alt20, alt21 // Unused icons.
case alt3, alt4, alt5, alt6, alt7, alt8
case alt9, alt10, alt11, alt12, alt13
case alt14, alt15, alt17, atl18, alt19
case alt16, alt20, alt21
case alt22, alt23, alt24, alt25, alt26 case alt22, alt23, alt24, alt25, alt26
case alt27, alt28, alt29 case alt27, alt28, alt29
case alt30, alt31, alt32, alt33, alt34, alt35, alt36 case alt30, alt31, alt32, alt33, alt34, alt35, alt36
@ -48,10 +53,12 @@ struct IconSelectorView: View {
IconSelector( IconSelector(
title: "settings.app.icon.official".localized, title: "settings.app.icon.official".localized,
icons: [ icons: [
.primary, .alt46, .alt1, .alt2, .alt3, .alt4, .primary, .alt46, .alt1,
.alt5, .alt6, .alt7, .alt8, ]),
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15, IconSelector(
.alt16, .alt17, .alt18, .alt19, .alt20, .alt21, title: "\("settings.app.icon.designed-by".localized) Erich Jurgens",
icons: [
.alt2
]), ]),
IconSelector( IconSelector(
title: "\("settings.app.icon.designed-by".localized) Albert Kinng", title: "\("settings.app.icon.designed-by".localized) Albert Kinng",

View file

@ -2,7 +2,7 @@ import AppAccount
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import NukeUI import NukeUI
import SwiftUI import SwiftUI
import UserNotifications import UserNotifications

View file

@ -4,7 +4,7 @@ import DesignSystem
import Env import Env
import Foundation import Foundation
import Models import Models
import Network import NetworkClient
import Nuke import Nuke
import SwiftData import SwiftData
import SwiftUI import SwiftUI
@ -17,7 +17,7 @@ struct SettingsTabs: View {
@Environment(PushNotificationsService.self) private var pushNotifications @Environment(PushNotificationsService.self) private var pushNotifications
@Environment(UserPreferences.self) private var preferences @Environment(UserPreferences.self) private var preferences
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@Environment(CurrentInstance.self) private var currentInstance @Environment(CurrentInstance.self) private var currentInstance
@Environment(AppAccountsManager.self) private var appAccountsManager @Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@ -39,8 +39,6 @@ struct SettingsTabs: View {
accountsSection accountsSection
generalSection generalSection
otherSections otherSections
postStreamingSection
AISection
cacheSection cacheSection
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
@ -49,7 +47,6 @@ struct SettingsTabs: View {
#endif #endif
.navigationTitle(Text("settings.title")) .navigationTitle(Text("settings.title"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.toolbar { .toolbar {
if isModal { if isModal {
ToolbarItem { ToolbarItem {
@ -147,7 +144,7 @@ struct SettingsTabs: View {
if let token = account.oauthToken, if let token = account.oauthToken,
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
{ {
let client = Client(server: account.server, oauthToken: token) let client = MastodonClient(server: account.server, oauthToken: token)
await timelineCache.clearCache(for: client.id) await timelineCache.clearCache(for: client.id)
await sub.deleteSubscription() await sub.deleteSubscription()
appAccountsManager.delete(account: account) appAccountsManager.delete(account: account)
@ -190,12 +187,6 @@ struct SettingsTabs: View {
NavigationLink(destination: TabbarEntriesSettingsView()) { NavigationLink(destination: TabbarEntriesSettingsView()) {
Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone") Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone")
} }
} else if UIDevice.current.userInterfaceIdiom == .pad
|| UIDevice.current.userInterfaceIdiom == .mac
{
NavigationLink(destination: SidebarEntriesSettingsView()) {
Label("settings.general.sidebarEntries", systemImage: "sidebar.squares.leading")
}
} }
NavigationLink(destination: TranslationSettingsView()) { NavigationLink(destination: TranslationSettingsView()) {
Label("settings.general.translate", systemImage: "captions.bubble") Label("settings.general.translate", systemImage: "captions.bubble")
@ -240,9 +231,6 @@ struct SettingsTabs: View {
Toggle(isOn: $preferences.soundEffectEnabled) { Toggle(isOn: $preferences.soundEffectEnabled) {
Label("settings.other.sound-effect", systemImage: "hifispeaker") Label("settings.other.sound-effect", systemImage: "hifispeaker")
} }
Toggle(isOn: $preferences.fastRefreshEnabled) {
Label("settings.other.fast-refresh", systemImage: "arrow.clockwise")
}
} header: { } header: {
Text("settings.section.other") Text("settings.section.other")
} footer: { } footer: {
@ -252,45 +240,7 @@ struct SettingsTabs: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
@ViewBuilder
private var postStreamingSection: some View {
@Bindable var preferences = preferences
Section {
Toggle(isOn: $preferences.isPostsStreamingEnabled) {
Label("Posts streaming", systemImage: "clock.badge")
}
} header: {
Text("Streaming")
} footer: {
Text(
"Enabling post streaming will automatically add new posts at the top of your home timeline. Disable if you get performance issues."
)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
@ViewBuilder
private var AISection: some View {
@Bindable var preferences = preferences
Section {
Toggle(isOn: $preferences.isOpenAIEnabled) {
Label("settings.other.hide-openai", systemImage: "faxmachine")
}
} header: {
Text("AI")
} footer: {
Text(
"Disable to hide AI assisted tool options such as copywritting and alt-image description generated using AI. Uses OpenAI API. See our Privacy Policy for more information."
)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
private var appSection: some View { private var appSection: some View {
Section { Section {
#if !targetEnvironment(macCatalyst) && !os(visionOS) #if !targetEnvironment(macCatalyst) && !os(visionOS)

View file

@ -1,40 +0,0 @@
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 {
@Bindable var userPreferences = userPreferences
Form {
Section {
ForEach($sidebarTabs.tabs, id: \.tab) { $tab in
if tab.tab != .profile && tab.tab != .settings {
Toggle(isOn: $tab.enabled) {
tab.tab.label
}
}
}
.onMove(perform: move)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
.environment(\.editMode, .constant(.active))
.navigationTitle("settings.general.sidebarEntries")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
func move(from source: IndexSet, to destination: Int) {
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
}
}

View file

@ -52,13 +52,6 @@ struct TabbarEntriesSettingsView: View {
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
Section {
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
} }
.navigationTitle("settings.general.tabbarEntries") .navigationTitle("settings.general.tabbarEntries")
#if !os(visionOS) #if !os(visionOS)

View file

@ -5,9 +5,11 @@ import Explore
import Foundation import Foundation
import StatusKit import StatusKit
import SwiftUI import SwiftUI
import Timeline
import Env
@MainActor @MainActor
enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable { enum AppTab: Identifiable, Hashable, CaseIterable, Codable {
case timeline, notifications, mentions, explore, messages, settings, other case timeline, notifications, mentions, explore, messages, settings, other
case trending, federated, local case trending, federated, local
case profile case profile
@ -17,9 +19,91 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
case followedTags case followedTags
case lists case lists
case links case links
case anyTimelineFilter(filter: TimelineFilter)
nonisolated var id: Int { nonisolated var id: Int {
rawValue return switch self {
case .timeline: 0
case .notifications: 1
case .mentions: 2
case .explore: 3
case .messages: 4
case .settings: 5
case .other: 6
case .trending: 7
case .federated: 8
case .local: 9
case .profile: 10
case .bookmarks: 11
case .favorites: 12
case .post: 13
case .followedTags: 14
case .lists: 15
case .links: 16
case .anyTimelineFilter(let filter):
filter.hashValue
}
}
nonisolated static var allCases: [AppTab] {
[.timeline,
.notifications,
.mentions,
.explore,
.messages,
.settings,
.other,
.trending,
.federated,
.local,
.profile,
.bookmarks,
.favorites,
.post,
.followedTags,
.lists,
.links]
}
init(with id: Int) {
switch id {
case 0:
self = .timeline
case 1:
self = .notifications
case 2:
self = .mentions
case 3:
self = .explore
case 4:
self = .messages
case 5:
self = .settings
case 6:
self = .other
case 7:
self = .trending
case 8:
self = .federated
case 9:
self = .local
case 10:
self = .profile
case 11:
self = .bookmarks
case 12:
self = .favorites
case 13:
self = .post
case 14:
self = .followedTags
case 15:
self = .lists
case 16:
self = .links
default:
self = .other
}
} }
static func loggedOutTab() -> [AppTab] { static func loggedOutTab() -> [AppTab] {
@ -31,16 +115,22 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
} }
@ViewBuilder @ViewBuilder
func makeContentView(selectedTab: Binding<AppTab>) -> some View { func makeContentView(
homeTimeline: Binding<TimelineFilter>,
selectedTab: Binding<AppTab>,
pinnedFilters: Binding<[TimelineFilter]>
) -> some View {
switch self { switch self {
case let .anyTimelineFilter(filter):
TimelineTab(timeline: .constant(filter))
case .timeline: case .timeline:
TimelineTab() TimelineTab(canFilterTimeline: true, timeline: homeTimeline, pinedFilters: pinnedFilters)
case .trending: case .trending:
TimelineTab(timeline: .trending) TimelineTab(timeline: .constant(.trending))
case .local: case .local:
TimelineTab(timeline: .local) TimelineTab(timeline: .constant(.local))
case .federated: case .federated:
TimelineTab(timeline: .federated) TimelineTab(timeline: .constant(.federated))
case .notifications: case .notifications:
NotificationsTab(selectedTab: selectedTab, lockedType: nil) NotificationsTab(selectedTab: selectedTab, lockedType: nil)
case .mentions: case .mentions:
@ -78,6 +168,15 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
} }
} }
var tabPlacement: TabPlacement {
switch self {
case .timeline, .notifications, .explore, .links, .profile:
return .pinned
default:
return .sidebarOnly
}
}
@ViewBuilder @ViewBuilder
var label: some View { var label: some View {
if self != .other { if self != .other {
@ -87,6 +186,8 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
var title: LocalizedStringKey { var title: LocalizedStringKey {
switch self { switch self {
case let .anyTimelineFilter(filter):
filter.localizedTitle()
case .timeline: case .timeline:
"tab.timeline" "tab.timeline"
case .trending: case .trending:
@ -126,6 +227,8 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
var iconName: String { var iconName: String {
switch self { switch self {
case let .anyTimelineFilter(filter):
filter.iconName()
case .timeline: case .timeline:
"rectangle.stack" "rectangle.stack"
case .trending: case .trending:
@ -165,49 +268,63 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
} }
@MainActor @MainActor
@Observable enum SidebarSections: Int, Identifiable {
class SidebarTabs { case timeline, activities, account, app, loggedOutTabs, iosTabs, visionOSTabs, lists, tags, localTimeline, tagGroup
struct SidedebarTab: Hashable, Codable {
let tab: AppTab nonisolated var id: Int {
var enabled: Bool rawValue
} }
class Storage { static var macOrIpadOSSections: [SidebarSections] {
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [ [.timeline, .activities, .account, .lists, .tags]
.init(tab: .timeline, enabled: true),
.init(tab: .trending, enabled: true),
.init(tab: .federated, enabled: true),
.init(tab: .local, enabled: true),
.init(tab: .notifications, enabled: true),
.init(tab: .mentions, enabled: true),
.init(tab: .messages, enabled: true),
.init(tab: .explore, enabled: true),
.init(tab: .bookmarks, enabled: true),
.init(tab: .favorites, enabled: true),
.init(tab: .followedTags, enabled: true),
.init(tab: .lists, enabled: true),
.init(tab: .links, enabled: true),
.init(tab: .settings, enabled: true),
.init(tab: .profile, enabled: true),
]
} }
private let storage = Storage() var title: String {
public static let shared = SidebarTabs() switch self {
case .timeline:
var tabs: [SidedebarTab] { "Timeline"
didSet { case .activities:
storage.tabs = tabs "Activities"
case .account:
"Account"
case .app:
"App"
case .lists:
"Lists"
case .tags:
"Followed Hashtags"
case .localTimeline:
"Local Timelines"
case .tagGroup:
"Tag Groups"
case .loggedOutTabs, .iosTabs, .visionOSTabs:
""
} }
} }
func isEnabled(_ tab: AppTab) -> Bool { var tabs: [AppTab] {
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true switch self {
} case .timeline:
return [.timeline, .trending, .local, .federated, .links, .explore]
private init() { case .activities:
tabs = storage.tabs return [.notifications, .mentions, .messages]
case .account:
return [.profile, .bookmarks, .favorites]
case .app:
return [.settings]
case .loggedOutTabs:
return [.timeline, .settings]
case .iosTabs:
return iOSTabs.shared.tabs
case .visionOSTabs:
return AppTab.visionOSTab()
case .lists:
return CurrentAccount.shared.lists.map { .anyTimelineFilter(filter: .list(list: $0)) }
case .tags:
return CurrentAccount.shared.tags.map { .anyTimelineFilter(filter: .hashtag(tag: $0.name, accountId: nil)) }
case .localTimeline, .tagGroup:
return []
}
} }
} }
@ -219,11 +336,11 @@ class iOSTabs {
} }
class Storage { class Storage {
@AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline @AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline.id
@AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications @AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications.id
@AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore @AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore.id
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links @AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links.id
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile @AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile.id
} }
private let storage = Storage() private let storage = Storage()
@ -235,39 +352,39 @@ class iOSTabs {
var firstTab: AppTab { var firstTab: AppTab {
didSet { didSet {
storage.firstTab = firstTab storage.firstTab = firstTab.id
} }
} }
var secondTab: AppTab { var secondTab: AppTab {
didSet { didSet {
storage.secondTab = secondTab storage.secondTab = secondTab.id
} }
} }
var thirdTab: AppTab { var thirdTab: AppTab {
didSet { didSet {
storage.thirdTab = thirdTab storage.thirdTab = thirdTab.id
} }
} }
var fourthTab: AppTab { var fourthTab: AppTab {
didSet { didSet {
storage.fourthTab = fourthTab storage.fourthTab = fourthTab.id
} }
} }
var fifthTab: AppTab { var fifthTab: AppTab {
didSet { didSet {
storage.fifthTab = fifthTab storage.fifthTab = fifthTab.id
} }
} }
private init() { private init() {
firstTab = storage.firstTab firstTab = .init(with: storage.firstTab)
secondTab = storage.secondTab secondTab = .init(with: storage.secondTab)
thirdTab = storage.thirdTab thirdTab = .init(with: storage.thirdTab)
fourthTab = storage.fourthTab fourthTab = .init(with: storage.fourthTab)
fifthTab = storage.fifthTab fifthTab = .init(with: storage.fifthTab)
} }
} }

View file

@ -2,7 +2,7 @@ import Combine
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import NukeUI import NukeUI
import SFSafeSymbols import SFSafeSymbols
import SwiftData import SwiftData

View file

@ -2,7 +2,7 @@ import Combine
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import NukeUI import NukeUI
import SwiftUI import SwiftUI
@ -72,7 +72,7 @@ struct AddRemoteTimelineView: View {
instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { newValue in ) { newValue in
Task { Task {
let client = Client(server: newValue) let client = MastodonClient(server: newValue)
instance = try? await client.get(endpoint: Instances.instance) instance = try? await client.get(endpoint: Instances.instance)
} }
} }

View file

@ -3,7 +3,7 @@ import Combine
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import NetworkClient
import SwiftData import SwiftData
import SwiftUI import SwiftUI
import Timeline import Timeline
@ -16,24 +16,29 @@ struct TimelineTab: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(UserPreferences.self) private var preferences @Environment(UserPreferences.self) private var preferences
@Environment(Client.self) private var client @Environment(MastodonClient.self) private var client
@State private var routerPath = RouterPath() @State private var routerPath = RouterPath()
@State private var didAppear: Bool = false @State private var didAppear: Bool = false
@State private var timeline: TimelineFilter = .home
@State private var selectedTagGroup: TagGroup? @State private var selectedTagGroup: TagGroup?
@Binding var timeline: TimelineFilter
@Binding var pinnedFilters: [TimelineFilter]
@AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] @Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup] @Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
@AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home
@AppStorage("timeline_pinned_filters") private var pinnedFilters: [TimelineFilter] = []
private let canFilterTimeline: Bool private let canFilterTimeline: Bool
init(timeline: TimelineFilter? = nil) { init(
canFilterTimeline = timeline == nil canFilterTimeline: Bool = false, timeline: Binding<TimelineFilter>,
_timeline = .init(initialValue: timeline ?? .home) pinedFilters: Binding<[TimelineFilter]> = .constant([])
) {
self.canFilterTimeline = canFilterTimeline
_timeline = timeline
_pinnedFilters = pinedFilters
} }
var body: some View { var body: some View {
@ -49,7 +54,6 @@ struct TimelineTab: View {
.toolbar { .toolbar {
toolbarView toolbarView
} }
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.id(client.id) .id(client.id)
} }
.onAppear { .onAppear {
@ -83,7 +87,7 @@ struct TimelineTab: View {
lastTimelineFilter = newValue lastTimelineFilter = newValue
} }
switch newValue { switch newValue {
case let .tagGroup(title, _, _): case .tagGroup(let title, _, _):
if let group = tagGroups.first(where: { $0.title == title }) { if let group = tagGroups.first(where: { $0.title == title }) {
selectedTagGroup = group selectedTagGroup = group
} }
@ -149,16 +153,23 @@ struct TimelineTab: View {
} }
} }
switch timeline { switch timeline {
case let .list(list): case .list(let list):
ToolbarItem { if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarTrailing)
}
ToolbarItem(placement: .topBarTrailing) {
Button { Button {
routerPath.presentedSheet = .listEdit(list: list) routerPath.presentedSheet = .listEdit(list: list)
} label: { } label: {
Image(systemName: "list.bullet") Image(systemName: "list.bullet")
.foregroundStyle(theme.labelColor)
} }
} }
case let .remoteLocal(server, _): case .remoteLocal(let server, _):
ToolbarItem { if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarTrailing)
}
ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {
ForEach(RemoteTimelineFilter.allCases, id: \.self) { filter in ForEach(RemoteTimelineFilter.allCases, id: \.self) { filter in
Button { Button {
@ -169,6 +180,7 @@ struct TimelineTab: View {
} }
} label: { } label: {
Image(systemName: "line.3.horizontal.decrease.circle") Image(systemName: "line.3.horizontal.decrease.circle")
.foregroundStyle(theme.labelColor)
} }
} }
default: default:

View file

@ -9,41 +9,27 @@ struct ToolbarTab: ToolbarContent {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme
@Binding var routerPath: RouterPath @Binding var routerPath: RouterPath
var body: some ToolbarContent { var body: some ToolbarContent {
if !isSecondaryColumn { if !isSecondaryColumn {
if horizontalSizeClass == .regular {
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( statusEditorToolbarItem(
routerPath: routerPath, routerPath: routerPath,
visibility: userPreferences.postVisibility) visibility: userPreferences.postVisibility)
if UIDevice.current.userInterfaceIdiom != .pad if #available(iOS 26.0, *) {
|| (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
{
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
AppAccountsSelectorView( AppAccountsSelectorView(
routerPath: routerPath, routerPath: routerPath,
avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded) avatarConfig: .embed)
.offset(x: -12)
}
.sharedBackgroundVisibility(.hidden)
} else {
ToolbarItem(placement: .navigationBarLeading) {
AppAccountsSelectorView(
routerPath: routerPath,
avatarConfig: .embed)
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

View file

@ -1,98 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-fs8.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "dark.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "tinted.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32 1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "256 1.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "512 1.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "Content.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

View file

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "AppIcon-fs8.png", "filename" : "AppIcon.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View file

@ -1,14 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-fs8.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

View file

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "1024.png", "filename" : "AppIconAlternate1-iOS-Default-1024x1024@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 846 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "AppIconAlternate10.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

View file

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

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "icon15.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "icon16.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "icon17.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "icon18.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "Alternate43-fs8.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

View file

@ -1,14 +0,0 @@
{
"images" : [
{
"filename" : "Alternate43-fs8.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "Alternate44-fs8.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

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