Compare commits

...

31 Commits

Author SHA1 Message Date
Shadowfacts c99c397cf6 Bump build number and update changelog 2024-09-11 18:26:20 -04:00
Shadowfacts 814f64b3e2 Simplify add saved hashtag toolbar buttons
Closes #522
2024-09-10 10:20:27 -04:00
Shadowfacts 3a3af77907 Fix swipe action completion handler not being called 2024-09-10 10:18:08 -04:00
Shadowfacts 93e72e1cb6 Fix add saved hashtag search results selection not being cleared 2024-09-09 23:53:37 -04:00
Shadowfacts 522e7830e5 Fix scroll-to-top not working in in-app Safari
Closes #538
2024-09-09 19:42:55 -04:00
Shadowfacts 263210ac3c Fix gallery controls insets on iPhone 16
And change the default to the dynamic island metrics, so I hopefully
don't have to touch this every year
2024-09-09 19:39:30 -04:00
Shadowfacts 506d2ad8a9 Actually fix multi-column nav scrolling animations this time (hopefully)
Closes #539
2024-09-09 19:35:15 -04:00
Shadowfacts f9c0506590 Add tab-switching shortcuts to new tab bar
Closes #541
2024-09-09 19:18:12 -04:00
Shadowfacts 3f4917931b Poll own_votes is a nullable array of nullable ints, at least on pleroma
I do not understand why

Closes #540
2024-09-09 19:13:57 -04:00
Shadowfacts b7166771cf Include SVGs in repo 2024-08-31 11:42:48 -04:00
Shadowfacts 40230c5478 Add dark mode app icons, optimize pngs 2024-08-31 11:37:39 -04:00
Shadowfacts 68bd9e0bed Tweak mute button symbol animation 2024-08-31 11:20:09 -04:00
Shadowfacts 3e28c012d7 Shhh 2024-08-31 11:10:59 -04:00
Shadowfacts 57c023c973 Fix profile tab switching animation ending in bad state
Caused by fda0c18794, old/new.view is no
longer the same as .collectionView, so the transform wasn't being
properly reset.

Closes #536
2024-08-31 11:09:17 -04:00
Shadowfacts cc696e58fc Change how profile header collection view cell is sized
Fixes crash due to collection view layout loop in some circumstances

Closes #537
2024-08-31 10:49:21 -04:00
Shadowfacts 59af29ff64 Fix incorrect background color on feature flag prefs section 2024-08-27 20:42:51 -04:00
Shadowfacts 59fb69525b Custom emoji in push notifications, behind a feature flag 2024-08-27 20:41:39 -04:00
Shadowfacts 1bd4d144a3 Fix crash on launch if there are somehow duplicate saved hashtags 2024-08-27 12:42:41 -04:00
Shadowfacts b54d34ebfc Fix video controls overlay being positioned incorrectly on macOS with Reduce Motion enabled
Closes #535
2024-08-26 19:16:35 -04:00
Shadowfacts d1ffab3e42 Only hide gallery controls automatically while playing 2024-08-26 19:08:44 -04:00
Shadowfacts d873b157ee Fix video gallery controls not auto hiding
#535
2024-08-26 10:25:28 -04:00
Shadowfacts d7be2048af Whoops
Closes #533
Closes #534
2024-08-23 01:19:29 -04:00
Shadowfacts 3d1f506684 Actually show the error message when video loading fails
See #531
2024-08-22 14:54:16 -04:00
Shadowfacts cd8f0e7926 Use navigation sequencing for user activity handling 2024-08-22 14:49:27 -04:00
Shadowfacts 960ba84683 New way of sequencing navigation operations
Better fix for #484
2024-08-22 14:34:05 -04:00
Shadowfacts 2eead1f9de Revert "Fix crash when opening push notification while VC modally presented"
This reverts commit 0f2a85b108.

This fixes state restoration happening asynchronously and causing the
new tab bar animation to run.
2024-08-22 14:17:04 -04:00
Shadowfacts b663335c6d Use the image description from imported image when possible
Closes #523
2024-08-22 13:54:03 -04:00
Shadowfacts 9ce6bd566f Show errors when video loading fails
Closes #532
2024-08-22 13:33:02 -04:00
Shadowfacts 9547bd2913 Fix incorrect split nav layout when closing split with new sidebar 2024-08-22 12:08:43 -04:00
Shadowfacts 9b2e6140a3 Fix reselecting current sidebar item not popping to root on Catalyst and new sidebar
Closes #525
2024-08-22 11:39:39 -04:00
Shadowfacts 6de255681c Fix assorted warnings when building with Xcode 16 2024-08-22 11:08:27 -04:00
87 changed files with 1476 additions and 299 deletions

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="Tusker.svg"
inkscape:version="1.0beta2 (2b71d25, 2019-12-03)"
inkscape:export-ydpi="11.52"
inkscape:export-xdpi="11.52"
inkscape:export-filename="/Users/shadowfacts/Desktop/60x60@2x.png"
id="svg8"
version="1.1"
viewBox="0 0 264.58333 264.58333"
height="1000"
width="1000">
<defs
id="defs2">
<inkscape:path-effect
bendpath1-nodetypes="cc"
bendpath4="M 27.271345,85.808468 V 178.94843"
bendpath3="M 27.271345,178.94843 H 242.39013"
bendpath2="M 242.39013,85.808468 V 178.94843"
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
xx="true"
yy="true"
lpeversion="1"
is_visible="true"
id="path-effect1345"
effect="envelope" />
<inkscape:path-effect
allow_transforms="true"
css_properties=""
attributes=""
method="d"
linkeditem=""
lpeversion="1"
is_visible="true"
id="path-effect38"
effect="clone_original" />
<inkscape:path-effect
scale_y_rel="false"
prop_scale="1"
strokepath="M0,0 L1,0"
endpoint_spacing_variation="0;1"
endpoint_edge_variation="0;1"
startpoint_spacing_variation="0;1"
startpoint_edge_variation="0;1"
count="5"
lpeversion="1"
is_visible="true"
id="path-effect32"
effect="curvestitching" />
<filter
height="1.3500000000000001"
width="1.2"
id="filter1277"
inkscape:label="Drop Shadow"
style="color-interpolation-filters:sRGB;">
<feFlood
id="feFlood1267"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.321569" />
<feComposite
id="feComposite1269"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1271"
result="blur"
stdDeviation="5"
in="composite1" />
<feOffset
id="feOffset1273"
result="offset"
dy="5"
dx="-2.5" />
<feComposite
id="feComposite1275"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
</defs>
<sodipodi:namedview
inkscape:window-maximized="0"
inkscape:window-y="23"
inkscape:window-x="1920"
inkscape:window-height="1395"
inkscape:window-width="1902"
units="px"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="layer2"
inkscape:document-units="px"
inkscape:cy="496.39379"
inkscape:cx="442.66632"
inkscape:zoom="1.4142136"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 2"
id="layer2"
inkscape:groupmode="layer">
<rect
y="-0.14500916"
x="-0.14500916"
height="264.87335"
width="264.87335"
id="rect865"
style="fill:#75e04e;fill-opacity:1;stroke:#75e04e;stroke-width:0.239149;stroke-opacity:1" />
</g>
<g
style="display:none"
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<path
inkscape:connector-curvature="0"
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
id="path28" />
</g>
<g
inkscape:label="Layer 1 copy"
inkscape:groupmode="layer"
id="g1343">
<path
id="path1341"
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.56578600000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
sodipodi:docname="Tusker transparent.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
inkscape:export-ydpi="98.304001"
inkscape:export-xdpi="98.304001"
inkscape:export-filename="../Desktop/1024x1024-dark@1x.png"
id="svg8"
version="1.1"
viewBox="0 0 264.58333 264.58333"
height="1000"
width="1000"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
bendpath1-nodetypes="cc"
bendpath4="M 27.271345,85.808468 V 178.94843"
bendpath3="M 27.271345,178.94843 H 242.39013"
bendpath2="M 242.39013,85.808468 V 178.94843"
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
xx="true"
yy="true"
lpeversion="1"
is_visible="true"
id="path-effect1345"
effect="envelope" />
<inkscape:path-effect
allow_transforms="true"
css_properties=""
attributes=""
method="d"
linkeditem=""
lpeversion="1"
is_visible="true"
id="path-effect38"
effect="clone_original" />
<inkscape:path-effect
scale_y_rel="false"
prop_scale="1"
strokepath="M0,0 L1,0"
endpoint_spacing_variation="0;1"
endpoint_edge_variation="0;1"
startpoint_spacing_variation="0;1"
startpoint_edge_variation="0;1"
count="5"
lpeversion="1"
is_visible="true"
id="path-effect32"
effect="curvestitching" />
<filter
height="1.317445"
width="1.1258237"
id="filter1277"
inkscape:label="Drop Shadow"
style="color-interpolation-filters:sRGB;"
x="-0.068723437"
y="-0.1318855">
<feFlood
id="feFlood1267"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.321569" />
<feComposite
id="feComposite1269"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1271"
result="blur"
stdDeviation="5"
in="composite1" />
<feOffset
id="feOffset1273"
result="offset"
dy="5"
dx="-2.5" />
<feComposite
id="feComposite1275"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
</defs>
<sodipodi:namedview
inkscape:window-maximized="0"
inkscape:window-y="25"
inkscape:window-x="1280"
inkscape:window-height="1387"
inkscape:window-width="1280"
units="px"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="layer2"
inkscape:document-units="px"
inkscape:cy="404.46507"
inkscape:cx="442.29528"
inkscape:zoom="1.4142136"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 2"
id="layer2"
inkscape:groupmode="layer" />
<g
style="display:none"
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<path
inkscape:connector-curvature="0"
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
id="path28" />
</g>
<g
inkscape:label="Layer 1 copy"
inkscape:groupmode="layer"
id="g1343">
<path
id="path1341"
style="fill:#75e04e;fill-opacity:1;stroke:#74e04d;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

162
Artwork/Tusker.svg Normal file
View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
sodipodi:docname="Tusker.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
inkscape:export-ydpi="98.304001"
inkscape:export-xdpi="98.304001"
inkscape:export-filename="../Desktop/1024x1024@1x.png"
id="svg8"
version="1.1"
viewBox="0 0 264.58333 264.58333"
height="1000"
width="1000"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
bendpath1-nodetypes="cc"
bendpath4="M 27.271345,85.808468 V 178.94843"
bendpath3="M 27.271345,178.94843 H 242.39013"
bendpath2="M 242.39013,85.808468 V 178.94843"
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
xx="true"
yy="true"
lpeversion="1"
is_visible="true"
id="path-effect1345"
effect="envelope" />
<inkscape:path-effect
allow_transforms="true"
css_properties=""
attributes=""
method="d"
linkeditem=""
lpeversion="1"
is_visible="true"
id="path-effect38"
effect="clone_original" />
<inkscape:path-effect
scale_y_rel="false"
prop_scale="1"
strokepath="M0,0 L1,0"
endpoint_spacing_variation="0;1"
endpoint_edge_variation="0;1"
startpoint_spacing_variation="0;1"
startpoint_edge_variation="0;1"
count="5"
lpeversion="1"
is_visible="true"
id="path-effect32"
effect="curvestitching" />
<filter
height="1.317445"
width="1.1258237"
id="filter1277"
inkscape:label="Drop Shadow"
style="color-interpolation-filters:sRGB;"
x="-0.068723437"
y="-0.1318855">
<feFlood
id="feFlood1267"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.321569" />
<feComposite
id="feComposite1269"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1271"
result="blur"
stdDeviation="5"
in="composite1" />
<feOffset
id="feOffset1273"
result="offset"
dy="5"
dx="-2.5" />
<feComposite
id="feComposite1275"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
</defs>
<sodipodi:namedview
inkscape:window-maximized="0"
inkscape:window-y="25"
inkscape:window-x="1280"
inkscape:window-height="1387"
inkscape:window-width="1280"
units="px"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="layer2"
inkscape:document-units="px"
inkscape:cy="496.38895"
inkscape:cx="442.29528"
inkscape:zoom="1.4142136"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 2"
id="layer2"
inkscape:groupmode="layer">
<rect
y="-0.14500916"
x="-0.14500916"
height="264.87335"
width="264.87335"
id="rect865"
style="fill:#75e04e;fill-opacity:1;stroke:#75e04e;stroke-width:0.239149;stroke-opacity:1" />
</g>
<g
style="display:none"
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<path
inkscape:connector-curvature="0"
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
id="path28" />
</g>
<g
inkscape:label="Layer 1 copy"
inkscape:groupmode="layer"
id="g1343">
<path
id="path1341"
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.56578600000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -1,3 +1,18 @@
## 2024.4
This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements.
Features/Improvements:
- Import image description when adding attachments from Photos if possible
- iPadOS 18: New floating sidebar/tab bar
Bugfixes:
- Fix crash when viewing profiles in certain circumstances
- Fix video controls in attachment gallery not auto-hiding
- Fix crash if hashtag search results includes duplicates
- Fix "no content" text not being removed from list timeline after refreshing
- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on
- macOS: Fix reselecting current item not navigating back
## 2024.3 ## 2024.3
This update includes a number of bugfixes and performance improvements. See below for a list of fixes. This update includes a number of bugfixes and performance improvements. See below for a list of fixes.

View File

@ -1,5 +1,20 @@
# Changelog # Changelog
## 2024.4 (136)
Features/Improvements:
- Import image description when adding attachments from Photos if possible
- Reorganize toolbar buttons when adding saved hashtag
- Show errors when loading video in attachment gallery fails
Bugfixes:
- Fix crash when viewing profiles in certain circumstances
- Fix profile tab switching animation getting stuck
- Fix video controls in attachment gallery not auto-hiding
- Pleroma: Fix error when loading polls in some circumstances
- iPadOS 18: Fix incorrect two-column layout when closing sidebar
- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on
- macOS: Fix reselecting current item not navigating back
## 2024.4 (135) ## 2024.4 (135)
Features/Improvements: Features/Improvements:
- iOS 18: New floating sidebar/tab bar - iOS 18: New floating sidebar/tab bar

View File

@ -15,9 +15,13 @@ import Pachyderm
import Intents import Intents
import HTMLStreamer import HTMLStreamer
import WebURL import WebURL
import UIKit
import TuskerPreferences
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
class NotificationService: UNNotificationServiceExtension { class NotificationService: UNNotificationServiceExtension {
private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self) private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self)
@ -225,8 +229,33 @@ class NotificationService: UNNotificationServiceExtension {
} }
let updatedContent: UNMutableNotificationContent let updatedContent: UNMutableNotificationContent
let contentProviding: any UNNotificationContentProviding
if #available(iOS 18.0, visionOS 2.0, *),
await Preferences.shared.hasFeatureFlag(.pushNotifCustomEmoji) {
let attributedString = NSMutableAttributedString(string: content.body)
for match in emojiRegex.matches(in: content.body, range: NSRange(location: 0, length: content.body.utf16.count)).reversed() {
let emojiName = (content.body as NSString).substring(with: match.range(at: 1))
guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }),
let url = URL(emoji.url),
let (data, _) = try? await URLSession.shared.data(from: url),
let image = UIImage(data: data) else {
continue
}
let attachment = NSTextAttachment(image: image)
let attachmentStr = NSAttributedString(attachment: attachment)
attributedString.replaceCharacters(in: match.range, with: attachmentStr)
}
let attributedCtx = UNNotificationAttributedMessageContext(sendMessageIntent: intent, attributedContent: attributedString)
contentProviding = attributedCtx
} else {
contentProviding = intent
}
do { do {
let newContent = try content.updating(from: intent) let newContent = try content.updating(from: contentProviding)
if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent { if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent {
pendingRequest?.0 = newMutableContent pendingRequest?.0 = newMutableContent
updatedContent = newMutableContent updatedContent = newMutableContent

View File

@ -167,11 +167,23 @@ extension DraftAttachment: NSItemProviderReading {
type = .png type = .png
} }
// Read the caption from the image itself, if there is one.
let caption: String
if let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceTypeIdentifierHint: typeIdentifier as CFString] as CFDictionary),
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any],
// This is the dictionary for TIFF properties, but it's present for other image types too
let tiffProperties = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any],
let imageDescription = tiffProperties[kCGImagePropertyTIFFImageDescription as String] as? String {
caption = imageDescription
} else {
caption = ""
}
let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil) let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil)
attachment.id = UUID() attachment.id = UUID()
attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type) attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type)
attachment.fileType = type.identifier attachment.fileType = type.identifier
attachment.attachmentDescription = "" attachment.attachmentDescription = caption
return attachment return attachment
} }

View File

@ -17,7 +17,7 @@ public protocol GalleryContentViewController: UIViewController {
var bottomControlsAccessoryViewController: UIViewController? { get } var bottomControlsAccessoryViewController: UIViewController? { get }
var canAnimateFromSourceView: Bool { get } var canAnimateFromSourceView: Bool { get }
func setControlsVisible(_ visible: Bool, animated: Bool) func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
func galleryContentDidAppear() func galleryContentDidAppear()
func galleryContentWillDisappear() func galleryContentWillDisappear()
} }
@ -35,7 +35,7 @@ public extension GalleryContentViewController {
true true
} }
func setControlsVisible(_ visible: Bool, animated: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
} }
func galleryContentDidAppear() { func galleryContentDidAppear() {

View File

@ -106,7 +106,7 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
content.view.frame = sourceFrameInContainer content.view.frame = sourceFrameInContainer
content.view.layer.opacity = 0 content.view.layer.opacity = 0
itemViewController.setControlsVisible(false, animated: false) itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
} }
animator.addCompletion { _ in animator.addCompletion { _ in

View File

@ -42,7 +42,7 @@ class GalleryDismissInteraction: NSObject {
origControlsVisible = viewController.currentItemViewController.controlsVisible origControlsVisible = viewController.currentItemViewController.controlsVisible
if origControlsVisible! { if origControlsVisible! {
viewController.currentItemViewController.setControlsVisible(false, animated: true) viewController.currentItemViewController.setControlsVisible(false, animated: true, dueToUserInteraction: false)
} }
case .changed: case .changed:

View File

@ -81,10 +81,10 @@ class GalleryItemViewController: UIViewController {
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(overlayVC.view) view.addSubview(overlayVC.view)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor), overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor), overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor), overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor),
overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor), overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])
} }
@ -213,7 +213,7 @@ class GalleryItemViewController: UIViewController {
updateZoomScale(resetZoom: false) updateZoomScale(resetZoom: false)
// Ensure the transform is correct if the controls are hidden // Ensure the transform is correct if the controls are hidden
setControlsVisible(controlsVisible, animated: false) setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
updateTopControlsInsets() updateTopControlsInsets()
} }
@ -229,7 +229,7 @@ class GalleryItemViewController: UIViewController {
} }
centerContent() centerContent()
// Ensure the transform is correct if the controls are hidden and their size changed. // Ensure the transform is correct if the controls are hidden and their size changed.
setControlsVisible(controlsVisible, animated: false) setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -250,7 +250,7 @@ class GalleryItemViewController: UIViewController {
func addContent() { func addContent() {
content.loadViewIfNeeded() content.loadViewIfNeeded()
content.setControlsVisible(controlsVisible, animated: false) content.setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
content.view.translatesAutoresizingMaskIntoConstraints = false content.view.translatesAutoresizingMaskIntoConstraints = false
if content.parent != self { if content.parent != self {
@ -290,7 +290,7 @@ class GalleryItemViewController: UIViewController {
content.view.layoutIfNeeded() content.view.layoutIfNeeded()
} }
func setControlsVisible(_ visible: Bool, animated: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
controlsVisible = visible controlsVisible = visible
guard let topControlsView, guard let topControlsView,
@ -301,7 +301,7 @@ class GalleryItemViewController: UIViewController {
func updateControlsViews() { func updateControlsViews() {
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height) topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height) bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
content.setControlsVisible(visible, animated: animated) content.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
} }
if animated { if animated {
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters()) let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
@ -378,9 +378,6 @@ class GalleryItemViewController: UIViewController {
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus 47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
50, // iPhone 12 mini, 13 mini 50, // iPhone 12 mini, 13 mini
] ]
let islandDeviceTopInsets: [CGFloat] = [
59, // iPhone 14 Pro, 14 Pro Max, 15 Pro, 15 Pro Max
]
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) { if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
// the notch width is not the same for the iPhones 13, // the notch width is not the same for the iPhones 13,
// but what we actually want is the same offset from the edges // but what we actually want is the same offset from the edges
@ -390,16 +387,18 @@ class GalleryItemViewController: UIViewController {
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2 let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
shareButtonLeadingConstraint.constant = offset shareButtonLeadingConstraint.constant = offset
closeButtonTrailingConstraint.constant = offset closeButtonTrailingConstraint.constant = offset
} else if islandDeviceTopInsets.contains(view.safeAreaInsets.top) { } else if view.safeAreaInsets.top == 0 {
shareButtonLeadingConstraint.constant = 24 // square corner devices
shareButtonTopConstraint.constant = 24
closeButtonTrailingConstraint.constant = 24
closeButtonTopConstraint.constant = 24
} else {
shareButtonLeadingConstraint.constant = 8 shareButtonLeadingConstraint.constant = 8
shareButtonTopConstraint.constant = 8 shareButtonTopConstraint.constant = 8
closeButtonTrailingConstraint.constant = 8 closeButtonTrailingConstraint.constant = 8
closeButtonTopConstraint.constant = 8 closeButtonTopConstraint.constant = 8
} else {
// dynamic island devices
shareButtonLeadingConstraint.constant = 24
shareButtonTopConstraint.constant = 24
closeButtonTrailingConstraint.constant = 24
closeButtonTopConstraint.constant = 24
} }
} }
@ -429,7 +428,7 @@ class GalleryItemViewController: UIViewController {
scrollView.zoomScale > scrollView.minimumZoomScale { scrollView.zoomScale > scrollView.minimumZoomScale {
animateZoomOut() animateZoomOut()
} else { } else {
setControlsVisible(!controlsVisible, animated: true) setControlsVisible(!controlsVisible, animated: true, dueToUserInteraction: true)
} }
} }
@ -531,7 +530,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
} }
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) { func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
setControlsVisible(visible, animated: animated) setControlsVisible(visible, animated: animated, dueToUserInteraction: false)
} }
} }
@ -546,9 +545,9 @@ extension GalleryItemViewController: UIScrollViewDelegate {
func scrollViewDidZoom(_ scrollView: UIScrollView) { func scrollViewDidZoom(_ scrollView: UIScrollView) {
if scrollView.zoomScale <= scrollView.minimumZoomScale { if scrollView.zoomScale <= scrollView.minimumZoomScale {
setControlsVisible(true, animated: true) setControlsVisible(true, animated: true, dueToUserInteraction: true)
} else { } else {
setControlsVisible(false, animated: true) setControlsVisible(false, animated: true, dueToUserInteraction: true)
} }
centerContent() centerContent()

View File

@ -75,7 +75,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
container.layoutIfNeeded() container.layoutIfNeeded()
// This needs to take place after the layout, so that the transform is correct. // This needs to take place after the layout, so that the transform is correct.
itemViewController.setControlsVisible(false, animated: false) itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
let duration = self.transitionDuration(using: transitionContext) let duration = self.transitionDuration(using: transitionContext)
// rougly equivalent to duration: 0.35, bounce: 0.3 // rougly equivalent to duration: 0.35, bounce: 0.3
@ -90,7 +90,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
content.view.frame = destFrameInContainer content.view.frame = destFrameInContainer
content.view.layer.opacity = 1 content.view.layer.opacity = 1
itemViewController.setControlsVisible(true, animated: false) itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
if let sourceToDestTransform { if let sourceToDestTransform {
self.sourceView.transform = sourceToDestTransform self.sourceView.transform = sourceToDestTransform

View File

@ -126,7 +126,7 @@ extension GalleryViewController: UIPageViewControllerDelegate {
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
currentItemViewController.content.galleryContentWillDisappear() currentItemViewController.content.galleryContentWillDisappear()
let new = pendingViewControllers[0] as! GalleryItemViewController let new = pendingViewControllers[0] as! GalleryItemViewController
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false) new.setControlsVisible(currentItemViewController.controlsVisible, animated: false, dueToUserInteraction: false)
} }
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {

View File

@ -16,7 +16,7 @@ public struct Poll: Codable, Sendable {
public let votesCount: Int public let votesCount: Int
public let votersCount: Int? public let votersCount: Int?
public let voted: Bool? public let voted: Bool?
public let ownVotes: [Int]? public let ownVotes: [Int?]?
public let options: [Option] public let options: [Option]
public let emojis: [Emoji] public let emojis: [Emoji]
@ -33,7 +33,7 @@ public struct Poll: Codable, Sendable {
self.votesCount = try container.decode(Int.self, forKey: .votesCount) self.votesCount = try container.decode(Int.self, forKey: .votesCount)
self.votersCount = try container.decodeIfPresent(Int.self, forKey: .votersCount) self.votersCount = try container.decodeIfPresent(Int.self, forKey: .votersCount)
self.voted = try container.decodeIfPresent(Bool.self, forKey: .voted) self.voted = try container.decodeIfPresent(Bool.self, forKey: .voted)
self.ownVotes = try container.decodeIfPresent([Int].self, forKey: .ownVotes) self.ownVotes = try container.decodeIfPresent([Int?].self, forKey: .ownVotes)
self.options = try container.decode([Poll.Option].self, forKey: .options) self.options = try container.decode([Poll.Option].self, forKey: .options)
self.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? [] self.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? []
} }

View File

@ -9,4 +9,5 @@ import Foundation
public enum FeatureFlag: String, Codable { public enum FeatureFlag: String, Codable {
case iPadBrowserNavigation = "ipad-browser-navigation" case iPadBrowserNavigation = "ipad-browser-navigation"
case pushNotifCustomEmoji = "push-notif-custom-emoji"
} }

View File

@ -77,6 +77,7 @@
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; }; D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */; }; D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */; };
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621733228F1D5ED004C7DB1 /* ReblogService.swift */; }; D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621733228F1D5ED004C7DB1 /* ReblogService.swift */; };
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D62220462C7EA8DF003E43B7 /* TuskerPreferences */; };
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; }; D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; }; D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; }; D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
@ -165,7 +166,6 @@
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; }; D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; }; D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; }; D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; }; D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; }; D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; }; D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
@ -599,7 +599,6 @@
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; }; D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; }; D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; }; D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; }; D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; }; D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; }; D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
@ -830,6 +829,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D630C4252BC7845800208903 /* WebURL in Frameworks */, D630C4252BC7845800208903 /* WebURL in Frameworks */,
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */, D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */, D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */, D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */,
@ -1324,7 +1324,6 @@
D667E5F62135C2ED0057A976 /* Extensions */ = { D667E5F62135C2ED0057A976 /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */, D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */, D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
D6333B362137838300CE884A /* AttributedString+Helpers.swift */, D6333B362137838300CE884A /* AttributedString+Helpers.swift */,
@ -1798,6 +1797,7 @@
D630C3E42BC6313400208903 /* Pachyderm */, D630C3E42BC6313400208903 /* Pachyderm */,
D630C4222BC7842C00208903 /* HTMLStreamer */, D630C4222BC7842C00208903 /* HTMLStreamer */,
D630C4242BC7845800208903 /* WebURL */, D630C4242BC7845800208903 /* WebURL */,
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
); );
productName = NotificationExtension; productName = NotificationExtension;
productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */; productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */;
@ -2216,7 +2216,6 @@
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */, D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */, D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */, D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */, D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */, D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */, D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
@ -3303,6 +3302,10 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Pachyderm; productName = Pachyderm;
}; };
D62220462C7EA8DF003E43B7 /* TuskerPreferences */ = {
isa = XCSwiftPackageProductDependency;
productName = TuskerPreferences;
};
D630C3C72BC43AFD00208903 /* PushNotifications */ = { D630C3C72BC43AFD00208903 /* PushNotifications */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = PushNotifications; productName = PushNotifications;

View File

@ -292,12 +292,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let rootViewController = delegate.rootViewController { let rootViewController = delegate.rootViewController {
let mastodonController = MastodonController.getForAccount(account) let mastodonController = MastodonController.getForAccount(account)
// if the scene is already active, then we animate the account switching if necessary // if the scene is already active, then we animate things
delegate.activateAccount(account, animated: scene.activationState == .foregroundActive) let animated = scene.activationState == .foregroundActive
rootViewController.select(route: .notifications, animated: false) { delegate.activateAccount(account, animated: animated)
rootViewController.runNavigation(animated: animated) { navigation in
navigation.select(route: .notifications)
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
rootViewController.getNavigationController().pushViewController(vc, animated: false) navigation.push(viewController: vc)
} }
} else { } else {
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID) let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -2,109 +2,511 @@
"images" : [ "images" : [
{ {
"filename" : "20x20@2x.png", "filename" : "20x20@2x.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "20x20@3x.png", "filename" : "20x20@3x.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "3x", "scale" : "3x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "29x29@2x.png", "filename" : "29x29@2x.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "29x29@3x.png", "filename" : "29x29@3x.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "3x", "scale" : "3x",
"size" : "29x29" "size" : "29x29"
}, },
{
"filename" : "38x38@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"filename" : "38x38@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{ {
"filename" : "40x40@2x.png", "filename" : "40x40@2x.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "2x", "scale" : "2x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "40x40@3x.png", "filename" : "40x40@3x.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "3x", "scale" : "3x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "60x60@2x.png", "filename" : "40x40@3x 1.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "2x", "scale" : "2x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "60x60@3x.png", "filename" : "60x60@3x.png",
"idiom" : "iphone", "idiom" : "universal",
"platform" : "ios",
"scale" : "3x", "scale" : "3x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "20x20@1x.png", "filename" : "64x64@2x.png",
"idiom" : "ipad", "idiom" : "universal",
"scale" : "1x", "platform" : "ios",
"size" : "20x20"
},
{
"filename" : "20x20@2x-1.png",
"idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "64x64"
}, },
{ {
"filename" : "29x29@1x.png", "filename" : "64x64@3x.png",
"idiom" : "ipad", "idiom" : "universal",
"scale" : "1x", "platform" : "ios",
"size" : "29x29" "scale" : "3x",
"size" : "64x64"
}, },
{ {
"filename" : "29x29@2x-1.png", "filename" : "68x68@2x.png",
"idiom" : "ipad", "idiom" : "universal",
"platform" : "ios",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "68x68"
},
{
"filename" : "40x40@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "40x40@2x-1.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
}, },
{ {
"filename" : "76x76@2x.png", "filename" : "76x76@2x.png",
"idiom" : "ipad", "idiom" : "universal",
"platform" : "ios",
"scale" : "2x", "scale" : "2x",
"size" : "76x76" "size" : "76x76"
}, },
{ {
"filename" : "83.5x83.5@2x.png", "filename" : "83.5x83.5@2x.png",
"idiom" : "ipad", "idiom" : "universal",
"platform" : "ios",
"scale" : "2x", "scale" : "2x",
"size" : "83.5x83.5" "size" : "83.5x83.5"
}, },
{ {
"filename" : "1024x1024@1x.png", "filename" : "1024x1024@1x.png",
"idiom" : "ios-marketing", "idiom" : "universal",
"scale" : "1x", "platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "20x20-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "20x20-dark@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "29x29-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "29x29-dark@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "38x38-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "38x38-dark@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "40x40-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "40x40-dark@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "60x60-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "60x60-dark@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "64x64-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "64x64-dark@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "68x68-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "68x68"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "76x76-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "83.5x83.5-dark@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "1024x1024-dark@1x.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "68x68"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"
} }
], ],

View File

@ -19,7 +19,7 @@ import UserAccounts
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore") fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
class MastodonCachePersistentStore: NSPersistentCloudKitContainer { class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Sendable {
private let accountInfo: UserAccountInfo? private let accountInfo: UserAccountInfo?

View File

@ -1,21 +0,0 @@
//
// Status+Equatable.swift
// Tusker
//
// Created by Shadowfacts on 8/28/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Pachyderm
extension Status: Equatable {
public static func ==(lhs: Status, rhs: Status) -> Bool {
return lhs.id == rhs.id
}
}
extension Account: Equatable {
public static func ==(lhs: Account, rhs: Account) -> Bool {
return lhs.id == rhs.id
}
}

View File

@ -83,9 +83,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} else { } else {
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!) context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
} }
Task(priority: .userInitiated) { _ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
_ = await userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
}
} }
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
@ -193,10 +191,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
if let activity = launchActivity { if let activity = launchActivity {
func doRestoreActivity(context: UserActivityHandlingContext) { func doRestoreActivity(context: UserActivityHandlingContext) {
Task(priority: .userInitiated) { _ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
_ = await activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context)) context.finalize(activity: activity)
context.finalize(activity: activity)
}
} }
if activity.isStateRestorationActivity { if activity.isStateRestorationActivity {
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!)) doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))

View File

@ -88,7 +88,7 @@ struct AnnouncementListRow: View {
Button(role: .destructive) { Button(role: .destructive) {
Task { Task {
await dismissAnnouncement() await dismissAnnouncement()
await removeAnnouncement() removeAnnouncement()
} }
} label: { } label: {
Label("Dismiss", systemImage: "xmark") Label("Dismiss", systemImage: "xmark")

View File

@ -9,14 +9,14 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class AddSavedHashtagViewController: UIViewController { class AddSavedHashtagViewController: UIViewController, CollectionViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
var resultsController: SearchResultsViewController! var resultsController: SearchResultsViewController!
var searchController: UISearchController! var searchController: UISearchController!
private var collectionView: UICollectionView! private(set) var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
@ -91,6 +91,12 @@ class AddSavedHashtagViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
if searchController.isActive {
resultsController.clearSelectionOnAppear(animated: animated)
}
clearSelectionOnAppear(animated: animated)
let request = Client.getTrendingHashtags(limit: 10) let request = Client.getTrendingHashtags(limit: 10)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
@ -108,7 +114,63 @@ class AddSavedHashtagViewController: UIViewController {
} }
private func selectHashtag(_ hashtag: Hashtag) { private func selectHashtag(_ hashtag: Hashtag) {
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil) let vc = HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
vc.loadViewIfNeeded()
let mastodonController = mastodonController!
let context = mastodonController.persistentContainer.viewContext
let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first
let saveItem = UIBarButtonItem()
func updateSaveItem(saved: Bool) {
saveItem.title = saved ? "Unsave Hashag" : "Save Hashtag"
saveItem.image = UIImage(systemName: saved ? "minus" : "plus")
}
saveItem.primaryAction = UIAction(handler: { [unowned self] _ in
// re-fetch this in case the button's been tapped before and the captured var would be out of date
let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first
if let existingSaved {
context.delete(existingSaved)
} else {
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
}
mastodonController.persistentContainer.save(context: context)
updateSaveItem(saved: existingSaved == nil)
if existingSaved == nil {
self.presentingViewController?.dismiss(animated: true)
}
})
// setting primaryAction replace's the bar button's title/image with the action, so do this after
updateSaveItem(saved: existingSaved != nil)
vc.navigationItem.rightBarButtonItems = [
saveItem,
]
if mastodonController.instanceFeatures.canFollowHashtags {
let existingFollowed = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == hashtag.name })
let followItem = UIBarButtonItem()
func updateFollowItem(followed: Bool) {
followItem.title = followed ? "Unfollow Hashtag" : "Follow Hashtag"
followItem.image = UIImage(systemName: "person.badge.\(followed ? "minus" : "plus")")
}
followItem.primaryAction = UIAction(handler: { [unowned self] _ in
Task {
let success = await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: self).toggleFollow()
if success {
let followed = mastodonController.followedHashtags.contains(where: { $0.name.lowercased() == hashtag.name })
updateFollowItem(followed: followed)
if followed {
self.presentingViewController?.dismiss(animated: true)
}
}
}
})
updateFollowItem(followed: existingFollowed != nil)
vc.navigationItem.rightBarButtonItems!.append(followItem)
}
show(vc, sender: self)
} }
// MARK: - Interaction // MARK: - Interaction
@ -144,3 +206,7 @@ extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate {
selectHashtag(hashtag) selectHashtag(hashtag)
} }
} }
extension AddSavedHashtagViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController }
}

View File

@ -96,7 +96,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
// so we manually propagate this down to the results controller // so we manually propagate this down to the results controller
// so that it can deselect on appear // so that it can deselect on appear
if searchController.isActive { if searchController.isActive {
resultsController.viewWillAppear(animated) resultsController.clearSelectionOnAppear(animated: animated)
} }
clearSelectionOnAppear(animated: animated) clearSelectionOnAppear(animated: animated)
@ -308,6 +308,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in
context.delete(existing) context.delete(existing)
try! context.save() try! context.save()
completion(true)
})) }))
} }
if mastodonController.instanceFeatures.canFollowHashtags, if mastodonController.instanceFeatures.canFollowHashtags,

View File

@ -128,7 +128,7 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
} }
} }
func setControlsVisible(_ visible: Bool, animated: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
if #available(iOS 16.0, macCatalyst 17.0, *), if #available(iOS 16.0, macCatalyst 17.0, *),
let analysisInteraction { let analysisInteraction {
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated) analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)

View File

@ -52,7 +52,7 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
if let wrapped = await provider() { if let wrapped = await provider() {
self.wrapped = wrapped self.wrapped = wrapped
wrapped.container = container wrapped.container = container
wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false) wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false, dueToUserInteraction: false)
addChild(wrapped) addChild(wrapped)
wrapped.view.translatesAutoresizingMaskIntoConstraints = false wrapped.view.translatesAutoresizingMaskIntoConstraints = false
@ -102,8 +102,8 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
]) ])
} }
func setControlsVisible(_ visible: Bool, animated: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
wrapped?.setControlsVisible(visible, animated: animated) wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
} }
func galleryContentDidAppear() { func galleryContentDidAppear() {

View File

@ -408,7 +408,7 @@ private class MuteButton: UIControl {
let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")! let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")!
if animated, if animated,
#available(iOS 17.0, *) { #available(iOS 17.0, *) {
imageView.setSymbolImage(image, contentTransition: .replace.wholeSymbol, options: .speed(5)) imageView.setSymbolImage(image, contentTransition: .replace.byLayer)
} else { } else {
imageView.image = image imageView.image = image
} }

View File

@ -86,17 +86,10 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
updateItemObservations() updateItemObservations()
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
hideControlsWorkItem?.cancel() if player.rate == 0 {
if player.rate > 0 && info.oldValue == 0 { hideControlsWorkItem?.cancel()
hideControlsWorkItem = DispatchWorkItem { [weak self] in } else if player.rate > 0 && info.oldValue == 0 {
guard let self, scheduleControlsHide()
let container = self.container,
container.galleryControlsVisible else {
return
}
container.setGalleryControlsVisible(false, animated: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
} }
}) })
@ -114,12 +107,52 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
MainActor.runUnsafely { MainActor.runUnsafely {
if item.status == .readyToPlay { if item.status == .readyToPlay {
self.container?.setGalleryContentLoading(false) self.container?.setGalleryContentLoading(false)
statusObservation = nil self.statusObservation = nil
} else if item.status == .failed,
let error = item.error {
self.container?.setGalleryContentLoading(false)
self.showErrorView(error)
self.statusObservation = nil
} }
} }
}) })
} }
private func showErrorView(_ error: any Error) {
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
image.tintColor = .secondaryLabel
image.contentMode = .scaleAspectFit
let label = UILabel()
label.text = "Error Loading"
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
label.textColor = .secondaryLabel
label.adjustsFontForContentSizeCategory = true
let reason = UILabel()
reason.text = error.localizedDescription
reason.font = .preferredFont(forTextStyle: .subheadline)
reason.textColor = .secondaryLabel
reason.adjustsFontForContentSizeCategory = true
let stackView = UIStackView(arrangedSubviews: [
image,
label,
reason,
])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 8
view.addSubview(stackView)
NSLayoutConstraint.activate([
image.widthAnchor.constraint(equalToConstant: 64),
image.heightAnchor.constraint(equalToConstant: 64),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
@objc private func preferencesChanged() { @objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages { if isGrayscale != Preferences.shared.grayscaleImages {
let isPlaying = player.rate > 0 let isPlaying = player.rate > 0
@ -137,6 +170,20 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
} }
} }
private func scheduleControlsHide() {
hideControlsWorkItem = DispatchWorkItem { [weak self] in
MainActor.runUnsafely {
guard let self,
let container = self.container,
container.galleryControlsVisible else {
return
}
container.setGalleryControlsVisible(false, animated: true)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
}
// MARK: GalleryContentViewController // MARK: GalleryContentViewController
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)? weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
@ -164,9 +211,15 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed) private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
#endif #endif
func setControlsVisible(_ visible: Bool, animated: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
overlayVC.setVisible(visible) overlayVC.setVisible(visible)
hideControlsWorkItem?.cancel()
if !visible {
hideControlsWorkItem?.cancel()
} else if dueToUserInteraction,
player.rate > 0 {
scheduleControlsHide()
}
} }
func galleryContentDidAppear() { func galleryContentDidAppear() {

View File

@ -79,12 +79,10 @@ class AccountSwitchingContainerViewController: UIViewController {
stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)") stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)")
} else { } else {
newRoot = newRootProvider() newRoot = newRootProvider()
Task(priority: .userInitiated) { stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)") let context = StateRestorationUserActivityHandlingContext(root: newRoot)
let context = StateRestorationUserActivityHandlingContext(root: newRoot) _ = activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
_ = await activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context)) context.finalize(activity: activity)
context.finalize(activity: activity)
}
} }
} else { } else {
newRoot = newRootProvider() newRoot = newRootProvider()

View File

@ -184,10 +184,8 @@ extension BaseMainTabBarViewController: BackgroundableViewController {
extension BaseMainTabBarViewController: StatusBarTappableViewController { extension BaseMainTabBarViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
guard presentedViewController == nil else { guard presentedViewController == nil,
return .stop let vc = selectedViewController as? StatusBarTappableViewController else {
}
guard let vc = selectedViewController as? StatusBarTappableViewController else {
return .continue return .continue
} }
return vc.handleStatusBarTapped(xPosition: xPosition) return vc.handleStatusBarTapped(xPosition: xPosition)

View File

@ -15,7 +15,7 @@ protocol MainSidebarViewControllerDelegate: AnyObject {
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?) func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?)
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?) func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?)
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item)
} }
class MainSidebarViewController: UIViewController { class MainSidebarViewController: UIViewController {
@ -452,7 +452,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
} }
itemLastSelectedTimestamps[item] = Date() itemLastSelectedTimestamps[item] = Date()
if previouslySelectedItem == item { if previouslySelectedItem == item {
sidebarDelegate?.sidebar(self, scrollToTopFor: item) sidebarDelegate?.sidebar(self, didReselectItem: item)
} else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) { } else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) { if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically) collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)

View File

@ -333,14 +333,14 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened // Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true) transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
case let .tab(tab): case let .tab(tab):
// sidebar items that map 1 <-> 1 can be transferred directly // sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab, dismissPresented: false) tabBarViewController.select(tab: tab, dismissPresented: false, animated: false)
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_): case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously // Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
// in compact mode and performing a search. // in compact mode and performing a search.
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
@ -493,8 +493,12 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
secondaryNavController.viewControllers = [viewController] secondaryNavController.viewControllers = [viewController]
} }
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) { func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item) {
(secondaryNavController as? TabBarScrollableViewController)?.tabBarScrollToTop() if secondaryNavController.viewControllers.count == 1 {
(secondaryNavController.topViewController as? TabBarScrollableViewController)?.tabBarScrollToTop()
} else {
secondaryNavController.popToRootViewController(animated: true)
}
} }
} }

View File

@ -54,19 +54,22 @@ class MainTabBarViewController: BaseMainTabBarViewController {
view.backgroundColor = .appBackground view.backgroundColor = .appBackground
} }
func select(tab: Tab, dismissPresented: Bool) { func select(tab: Tab, dismissPresented: Bool, animated: Bool, completion: (() -> Void)? = nil) {
if tab == .compose { if tab == .compose {
compose(editing: nil) compose(editing: nil, completion: completion)
} else { } else {
// when switching tabs, dismiss the currently presented VC // when switching tabs, dismiss the currently presented VC
// otherwise the selected tab changes behind the presented VC // otherwise the selected tab changes behind the presented VC
if presentedViewController != nil && dismissPresented { if presentedViewController != nil && dismissPresented {
dismiss(animated: true) { dismiss(animated: animated) {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
self.selectedIndex = tab.rawValue self.selectedIndex = tab.rawValue
completion?()
} }
} else { } else {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)") stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
selectedIndex = tab.rawValue selectedIndex = tab.rawValue
completion?()
} }
} }
} }
@ -151,25 +154,24 @@ extension MainTabBarViewController: TuskerRootViewController {
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
switch route { switch route {
case .timelines: case .timelines:
select(tab: .timelines, dismissPresented: true) select(tab: .timelines, dismissPresented: true, animated: animated, completion: completion)
case .notifications: case .notifications:
select(tab: .notifications, dismissPresented: true) select(tab: .notifications, dismissPresented: true, animated: animated, completion: completion)
case .myProfile: case .myProfile:
select(tab: .myProfile, dismissPresented: true) select(tab: .myProfile, dismissPresented: true, animated: animated, completion: completion)
case .explore: case .explore:
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
case .bookmarks: case .bookmarks:
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated) getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
case .list(id: let id): case .list(id: let id):
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
if let list = mastodonController.getCachedList(id: id) { if let list = mastodonController.getCachedList(id: id) {
let nav = getNavigationController() let nav = getNavigationController()
_ = nav.popToRootViewController(animated: animated) _ = nav.popToRootViewController(animated: animated)
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated) nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
} }
} }
completion?()
} }
func getNavigationDelegate() -> TuskerNavigationDelegate? { func getNavigationDelegate() -> TuskerNavigationDelegate? {
@ -186,7 +188,7 @@ extension MainTabBarViewController: TuskerRootViewController {
return return
} }
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: false)
exploreNavController.popToRootViewController(animated: false) exploreNavController.popToRootViewController(animated: false)
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time // setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time

View File

@ -247,7 +247,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
private func embedInNavigationController(_ vc: UIViewController) -> UIViewController { private func embedInNavigationController(_ vc: UIViewController) -> UIViewController {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
return UINavigationController(rootViewController: vc) return EnhancedNavigationViewController(rootViewController: vc)
} else { } else {
let nav = AdaptableNavigationController() let nav = AdaptableNavigationController()
nav.viewControllers = [vc] nav.viewControllers = [vc]
@ -285,7 +285,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
var tabs: [UITab] = [] var tabs: [UITab] = []
let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!) let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? [] let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? []
for hashtag in saved { for hashtag in saved where !seenTags.contains(hashtag.name) {
seenTags.insert(hashtag.name) seenTags.insert(hashtag.name)
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
} }
@ -293,6 +293,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
let followedReq = FollowedHashtag.fetchRequest() let followedReq = FollowedHashtag.fetchRequest()
let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? [] let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? []
for hashtag in followed where !seenTags.contains(hashtag.name) { for hashtag in followed where !seenTags.contains(hashtag.name) {
seenTags.insert(hashtag.name)
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
} }
@ -313,10 +314,6 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
} }
} }
@objc func handleComposeKeyCommand() {
compose(editing: nil)
}
@objc private func sidebarTapped() { @objc private func sidebarTapped() {
#if !os(visionOS) #if !os(visionOS)
fastAccountSwitcher?.hide() fastAccountSwitcher?.hide()
@ -407,6 +404,33 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
return true return true
} }
#endif #endif
// MARK: Keyboard shortcuts
@objc func handleSidebarCommandTimelines() {
selectedTab = homeTab
}
@objc func handleSidebarCommandNotifications() {
selectedTab = notificationsTab
}
@objc func handleSidebarCommandExplore() {
selectedTab = exploreTab
}
@objc func handleSidebarCommandBookmarks() {
selectedTab = bookmarksTab
}
@objc func handleSidebarCommandMyProfile() {
selectedTab = myProfileTab
}
@objc func handleComposeKeyCommand() {
compose(editing: nil)
}
} }
@available(iOS 18.0, *) @available(iOS 18.0, *)
@ -441,10 +465,12 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
return true return true
} else if let selectedTab, } else if let selectedTab,
selectedTab == tab, selectedTab == tab,
let nav = selectedViewController as? any NavigationControllerProtocol, let nav = selectedViewController as? any NavigationControllerProtocol {
nav.viewControllers.count == 1, if nav.viewControllers.count == 1 {
let scrollableVC = nav.viewControllers[0] as? TabBarScrollableViewController { (nav.viewControllers[0] as? TabBarScrollableViewController)?.tabBarScrollToTop()
scrollableVC.tabBarScrollToTop() } else {
nav.popToRootViewController(animated: true)
}
return false return false
} else { } else {
return true return true

View File

@ -20,6 +20,14 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController?
} }
extension TuskerRootViewController {
func runNavigation(animated: Bool, _ builder: (_ navigation: TuskerNavigationSequence) -> Void) {
let sequence = TuskerNavigationSequence(root: self, animated: animated)
builder(sequence)
sequence.run()
}
}
enum TuskerRoute { enum TuskerRoute {
case timelines case timelines
case notifications case notifications
@ -29,6 +37,67 @@ enum TuskerRoute {
case list(id: String) case list(id: String)
} }
/// A class that manages running a sequence of navigation operations on a ``TuskerRootViewController``.
///
/// Use this type, rather than calling multiple methods on the root VC in a row, because it manages waiting until each previous step finishes.
@MainActor
final class TuskerNavigationSequence {
private let root: any TuskerRootViewController
private let animated: Bool
private var operations = [() -> Void]()
init(root: any TuskerRootViewController, animated: Bool) {
self.root = root
self.animated = animated
}
func select(route: TuskerRoute) {
operations.append {
self.root.select(route: route, animated: self.animated, completion: self.run)
}
}
func push(viewController: UIViewController) {
operations.append {
let nav = self.root.getNavigationController()
nav.pushViewController(viewController, animated: self.animated)
self.run()
}
}
func popToRoot() {
operations.append {
let nav = self.root.getNavigationController()
nav.popToRootViewController(animated: self.animated)
self.run()
}
}
func present(viewController: UIViewController) {
operations.append {
self.root.present(viewController, animated: self.animated, completion: self.run)
}
}
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
operations.append {
block(self.root.getNavigationController().topViewController, self.run)
}
}
func addOperation(_ operation: @escaping (_ completion: @escaping () -> Void) -> Void) {
operations.append {
operation(self.run)
}
}
func run() {
if !operations.isEmpty {
operations.removeFirst()()
}
}
}
@MainActor @MainActor
protocol NavigationControllerProtocol: UIViewController { protocol NavigationControllerProtocol: UIViewController {
var viewControllers: [UIViewController] { get set } var viewControllers: [UIViewController] { get set }

View File

@ -199,6 +199,7 @@ struct AdvancedPrefsView : View {
} header: { } header: {
Text("Feature Flags") Text("Feature Flags")
} }
.appGroupedListRowBackground()
} }
} }

View File

@ -87,7 +87,7 @@ struct PushInstanceSettingsView: View {
} }
let subscription = try await PushManager.shared.createSubscription(account: account) let subscription = try await PushManager.shared.createSubscription(account: account)
let mastodonController = await MastodonController.getForAccount(account) let mastodonController = MastodonController.getForAccount(account)
do { do {
let result = try await mastodonController.createPushSubscription(subscription: subscription) let result = try await mastodonController.createPushSubscription(subscription: subscription)
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) created on \(account.instanceURL) with endpoint \(result.endpoint, privacy: .public)") PushManager.logger.debug("Push subscription \(result.id, privacy: .public) created on \(account.instanceURL) with endpoint \(result.endpoint, privacy: .public)")
@ -95,25 +95,25 @@ struct PushInstanceSettingsView: View {
return true return true
} catch { } catch {
// if creation failed, remove the subscription locally as well // if creation failed, remove the subscription locally as well
await PushManager.shared.removeSubscription(account: account) PushManager.shared.removeSubscription(account: account)
throw error throw error
} }
} }
private func disableNotifications() async throws { private func disableNotifications() async throws {
let mastodonController = await MastodonController.getForAccount(account) let mastodonController = MastodonController.getForAccount(account)
try await mastodonController.deletePushSubscription() try await mastodonController.deletePushSubscription()
await PushManager.shared.removeSubscription(account: account) PushManager.shared.removeSubscription(account: account)
subscription = nil subscription = nil
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)") PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
} }
private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool { private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool {
let mastodonController = await MastodonController.getForAccount(account) let mastodonController = MastodonController.getForAccount(account)
do { do {
let result = try await mastodonController.updatePushSubscription(alerts: alerts, policy: policy) let result = try await mastodonController.updatePushSubscription(alerts: alerts, policy: policy)
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) updated on \(account.instanceURL)") PushManager.logger.debug("Push subscription \(result.id, privacy: .public) updated on \(account.instanceURL)")
await PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy) PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy)
subscription?.alerts = alerts subscription?.alerts = alerts
subscription?.policy = policy subscription?.policy = policy
return true return true

View File

@ -63,11 +63,18 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
} }
} }
// overrides an internal method override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
// when the super impl is used, preferredLayoutAttributesFitting(_:) isn't called while the view is offscreen (i.e., window == nil) switch state {
// and so the collection view imposes a height of 44pts which breaks the layout case .unloaded:
@objc func _preferredLayoutAttributesFittingAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { return super.preferredLayoutAttributesFitting(layoutAttributes)
return preferredLayoutAttributesFitting(attributes) case .placeholder(let heightConstraint):
layoutAttributes.size.height = heightConstraint.constant
return layoutAttributes
case .view(let profileHeaderView):
let size = profileHeaderView.systemLayoutSizeFitting(layoutAttributes.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
layoutAttributes.size = size
return layoutAttributes
}
} }
enum State { enum State {

View File

@ -213,7 +213,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
// old header cell must have the header view // old header cell must have the header view
let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)! let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)!
// Set the outgoing VC's header view mode to placeholder, so that it does steal the header view back // Set the outgoing VC's header view mode to placeholder, so that it does not steal the header view back
// in case it updates the cell in the background. // in case it updates the cell in the background.
old.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height) old.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height)
@ -224,12 +224,13 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
} }
// disable user interaction during animation, to avoid any potential weird race conditions // disable user interaction during animation, to avoid any potential weird race conditions
headerView.isUserInteractionEnabled = false view.isUserInteractionEnabled = false
headerView.layer.zPosition = 100 headerView.layer.zPosition = 100
view.addSubview(headerView) view.addSubview(headerView)
let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y// - view.safeAreaInsets.top let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset), headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset), headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset),
@ -272,16 +273,17 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
} }
animator.addCompletion { _ in animator.addCompletion { _ in
old.removeViewAndController() old.removeViewAndController()
old.collectionView.transform = .identity old.view.transform = .identity
new.collectionView.transform = .identity new.view.transform = .identity
new.collectionView.contentOffset = origOldContentOffset new.collectionView.contentOffset = origOldContentOffset
// reenable scroll indicators after the switching animation is done // reenable scroll indicators after the switching animation is done
old.collectionView.showsVerticalScrollIndicator = true old.collectionView.showsVerticalScrollIndicator = true
new.collectionView.showsVerticalScrollIndicator = true new.collectionView.showsVerticalScrollIndicator = true
headerView.isUserInteractionEnabled = true self.view.isUserInteractionEnabled = true
headerView.transform = .identity headerView.transform = .identity
headerView.layer.zPosition = 0 headerView.layer.zPosition = 0
// move the header view into the new page controller's cell // move the header view into the new page controller's cell

View File

@ -1295,7 +1295,7 @@ extension TimelineViewController {
// if we didn't add any items, that implies the gap was removed, and we want to to make clear what's happening // if we didn't add any items, that implies the gap was removed, and we want to to make clear what's happening
if !addedItems { if !addedItems {
var config = ToastConfiguration(title: "There's nothing in between!") var config = ToastConfiguration(title: "That's all, folks!")
config.dismissAutomaticallyAfter = 2 config.dismissAutomaticallyAfter = 2
showToast(configuration: config, animated: true) showToast(configuration: config, animated: true)
} }

View File

@ -152,10 +152,10 @@ class MultiColumnNavigationController: UIViewController {
let column = stackView.arrangedSubviews[columnIndex] let column = stackView.arrangedSubviews[columnIndex]
let columnFrame = column.convert(column.bounds, to: scrollView) let columnFrame = column.convert(column.bounds, to: scrollView)
let offset: CGFloat let offset: CGFloat
if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset { if columnFrame.maxX <= view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right - scrollView.adjustedTrailingContentInset {
offset = -scrollView.adjustedLeadingContentInset offset = -scrollView.adjustedLeadingContentInset
} else { } else {
offset = scrollView.contentSize.width - scrollView.bounds.width + scrollView.adjustedTrailingContentInset offset = columnFrame.maxX - scrollView.bounds.width + scrollView.adjustedTrailingContentInset
} }
scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated) scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated)
} }

View File

@ -241,13 +241,19 @@ class SplitNavigationController: UIViewController {
// otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen // otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) { let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
self.isLayingOutForAnimation = true self.isLayingOutForAnimation = true
self.setSecondaryVisible(false) NSLayoutConstraint.deactivate(self.constraints)
self.constraints = [
self.rootNav.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: self.rootNav.view.bounds.minX),
self.rootNav.view.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
self.secondaryNav.view.widthAnchor.constraint(equalToConstant: self.secondaryNav.view.bounds.width),
]
NSLayoutConstraint.activate(self.constraints)
self.view.layoutIfNeeded() self.view.layoutIfNeeded()
} }
animator.addCompletion { _ in animator.addCompletion { _ in
self.secondaryNav.viewControllers = []
self.isLayingOutForAnimation = false self.isLayingOutForAnimation = false
// self.updateSecondaryNavVisibility() self.secondaryNav.viewControllers = []
self.updateSecondaryNavVisibility()
} }
animator.startAnimation() animator.startAnimation()
} else { } else {

View File

@ -43,9 +43,9 @@ extension NSUserActivity {
} }
@MainActor @MainActor
func handleResume(manager: UserActivityManager) async -> Bool { func handleResume(manager: UserActivityManager) -> Bool {
guard let type = UserActivityType(rawValue: activityType) else { return false } guard let type = UserActivityType(rawValue: activityType) else { return false }
await type.handle(manager)(self) type.handle(manager)(self)
return true return true
} }

View File

@ -16,93 +16,94 @@ import ComposeUI
protocol UserActivityHandlingContext { protocol UserActivityHandlingContext {
var isHandoff: Bool { get } var isHandoff: Bool { get }
func select(route: TuskerRoute) async func select(route: TuskerRoute)
func select(route: TuskerRoute, completion: (() -> Void)?)
func present(_ vc: UIViewController)
var topViewController: UIViewController? { get }
func popToRoot() func popToRoot()
func push(_ vc: UIViewController) func push(_ vc: UIViewController)
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void)
func present(_ vc: UIViewController)
func compose(editing draft: Draft) func compose(editing draft: Draft)
func finalize(activity: NSUserActivity) func finalize(activity: NSUserActivity)
} }
extension UserActivityHandlingContext {
func select(route: TuskerRoute) async {
await withCheckedContinuation { continuation in
select(route: route) {
continuation.resume()
}
}
}
}
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
let isHandoff: Bool let isHandoff: Bool
let root: TuskerRootViewController private let root: TuskerRootViewController
var navigationDelegate: TuskerNavigationDelegate { private let navigation: TuskerNavigationSequence
root.getNavigationDelegate()!
init(isHandoff: Bool, root: TuskerRootViewController) {
self.isHandoff = isHandoff
self.root = root
self.navigation = TuskerNavigationSequence(root: root, animated: true)
} }
func select(route: TuskerRoute, completion: (() -> Void)?) { func select(route: TuskerRoute) {
root.select(route: route, animated: true, completion: completion) navigation.select(route: route)
} }
func present(_ vc: UIViewController) { func present(_ vc: UIViewController) {
navigationDelegate.present(vc, animated: true) navigation.present(viewController: vc)
} }
var topViewController: UIViewController? { root.getNavigationController().topViewController }
func popToRoot() { func popToRoot() {
_ = root.getNavigationController().popToRootViewController(animated: true) navigation.popToRoot()
} }
func push(_ vc: UIViewController) { func push(_ vc: UIViewController) {
navigationDelegate.show(vc, sender: nil) navigation.push(viewController: vc)
} }
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
navigation.withTopViewController(block)
}
func compose(editing draft: Draft) { func compose(editing draft: Draft) {
navigationDelegate.compose(editing: draft, animated: true, isDucked: true) navigation.addOperation { completion in
root.compose(editing: draft, animated: true, isDucked: true, completion: completion)
}
} }
func finalize(activity: NSUserActivity) { func finalize(activity: NSUserActivity) {
navigation.run()
} }
} }
class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
private var state = State.initial private var state = State.initial
let root: TuskerRootViewController private let root: TuskerRootViewController
private let navigation: TuskerNavigationSequence
init(root: TuskerRootViewController) { init(root: TuskerRootViewController) {
self.root = root self.root = root
self.navigation = TuskerNavigationSequence(root: root, animated: false)
} }
var isHandoff: Bool { false } var isHandoff: Bool {
false
func select(route: TuskerRoute, completion: (() -> Void)?) {
root.select(route: route, animated: false) {
self.state = .selectedRoute
completion?()
}
} }
var topViewController: UIViewController? { root.getNavigationController().topViewController } func select(route: TuskerRoute) {
navigation.select(route: route)
state = .selectedRoute
}
func popToRoot() { func popToRoot() {
// unnecessary during state restoration navigation.popToRoot()
} }
func push(_ vc: UIViewController) { func push(_ vc: UIViewController) {
precondition(state >= .selectedRoute) precondition(state >= .selectedRoute)
root.getNavigationController().pushViewController(vc, animated: false) navigation.push(viewController: vc)
state = .pushed state = .pushed
} }
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
navigation.withTopViewController(block)
}
func present(_ vc: UIViewController) { func present(_ vc: UIViewController) {
root.present(vc, animated: false) navigation.present(viewController: vc)
state = .presented state = .presented
} }
@ -120,6 +121,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
func finalize(activity: NSUserActivity) { func finalize(activity: NSUserActivity) {
precondition(state > .initial) precondition(state > .initial)
navigation.run()
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 16.0, *), if #available(iOS 16.0, *),
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {

View File

@ -133,12 +133,15 @@ class UserActivityManager {
return activity return activity
} }
func handleCheckNotifications(activity: NSUserActivity) async { func handleCheckNotifications(activity: NSUserActivity) {
await context.select(route: .notifications) context.select(route: .notifications)
context.popToRoot() context.popToRoot()
if let notificationsPageController = context.topViewController as? NotificationsPageViewController { context.withTopViewController { topViewController, completion in
notificationsPageController.loadViewIfNeeded() if let notificationsPageController = topViewController as? NotificationsPageViewController {
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
}
completion()
} }
} }
@ -204,32 +207,41 @@ class UserActivityManager {
return (timeline, positionInfo) return (timeline, positionInfo)
} }
func handleShowTimeline(activity: NSUserActivity) async { func handleShowTimeline(activity: NSUserActivity) {
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return } guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
var timelineVC: TimelineViewController?
if let pinned = PinnedTimeline(timeline: timeline), if let pinned = PinnedTimeline(timeline: timeline),
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
await context.select(route: .timelines) context.select(route: .timelines)
context.popToRoot() context.popToRoot()
let pageController = context.topViewController as! TimelinesPageViewController context.withTopViewController { topViewController, completion in
pageController.selectTimeline(pinned, animated: false) let pageController = topViewController as! TimelinesPageViewController
timelineVC = pageController.currentViewController as? TimelineViewController pageController.selectTimeline(pinned, animated: false)
}
} else if case .list(let id) = timeline { } else if case .list(let id) = timeline {
await context.select(route: .list(id: id)) context.select(route: .list(id: id))
timelineVC = context.topViewController as? TimelineViewController
} else { } else {
await context.select(route: .explore) context.select(route: .explore)
context.popToRoot() context.popToRoot()
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) let timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
context.push(timelineVC!) context.push(timelineVC)
} }
if let timelineVC, if let positionInfo,
let positionInfo,
context.isHandoff { context.isHandoff {
Task { context.withTopViewController { topViewController, completion in
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) let timelineVC: TimelineViewController
if let topViewController = topViewController as? TimelineViewController {
timelineVC = topViewController
} else if let topViewController = topViewController as? TimelinesPageViewController {
timelineVC = topViewController.currentViewController as! TimelineViewController
} else {
return
}
Task {
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
completion()
}
} }
} }
} }
@ -249,11 +261,11 @@ class UserActivityManager {
return activity.userInfo?["mainStatusID"] as? String return activity.userInfo?["mainStatusID"] as? String
} }
func handleShowConversation(activity: NSUserActivity) async { func handleShowConversation(activity: NSUserActivity) {
guard let mainStatusID = Self.getConversationStatus(from: activity) else { guard let mainStatusID = Self.getConversationStatus(from: activity) else {
return return
} }
await context.select(route: .timelines) context.select(route: .timelines)
context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController)) context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController))
} }
@ -274,32 +286,34 @@ class UserActivityManager {
return activity.userInfo?["query"] as? String return activity.userInfo?["query"] as? String
} }
func handleSearch(activity: NSUserActivity) async { func handleSearch(activity: NSUserActivity) {
await context.select(route: .explore) context.select(route: .explore)
context.popToRoot() context.popToRoot()
let searchController: UISearchController context.withTopViewController { topViewController, completion in
let resultsController: SearchResultsViewController let searchController: UISearchController
if let explore = context.topViewController as? ExploreViewController { let resultsController: SearchResultsViewController
explore.loadViewIfNeeded() if let explore = topViewController as? ExploreViewController {
explore.searchControllerStatusOnAppearance = true explore.loadViewIfNeeded()
searchController = explore.searchController explore.searchControllerStatusOnAppearance = true
resultsController = explore.resultsController searchController = explore.searchController
} else if let inlineTrends = context.topViewController as? InlineTrendsViewController { resultsController = explore.resultsController
inlineTrends.loadViewIfNeeded() } else if let inlineTrends = topViewController as? InlineTrendsViewController {
inlineTrends.searchControllerStatusOnAppearance = true inlineTrends.loadViewIfNeeded()
searchController = inlineTrends.searchController inlineTrends.searchControllerStatusOnAppearance = true
resultsController = inlineTrends.resultsController searchController = inlineTrends.searchController
} else { resultsController = inlineTrends.resultsController
return } else {
} return
}
if let query = Self.getSearchQuery(from: activity),
!query.isEmpty { if let query = Self.getSearchQuery(from: activity),
searchController.searchBar.text = query !query.isEmpty {
resultsController.performSearch(query: query) searchController.searchBar.text = query
} else { resultsController.performSearch(query: query)
searchController.searchBar.becomeFirstResponder() } else {
searchController.searchBar.becomeFirstResponder()
}
} }
} }
@ -311,8 +325,8 @@ class UserActivityManager {
return activity return activity
} }
func handleBookmarks(activity: NSUserActivity) async { func handleBookmarks(activity: NSUserActivity) {
await context.select(route: .bookmarks) context.select(route: .bookmarks)
} }
// MARK: - My Profile // MARK: - My Profile
@ -325,8 +339,8 @@ class UserActivityManager {
return activity return activity
} }
func handleMyProfile(activity: NSUserActivity) async { func handleMyProfile(activity: NSUserActivity) {
await context.select(route: .myProfile) context.select(route: .myProfile)
} }
// MARK: - Show Profile // MARK: - Show Profile
@ -344,11 +358,11 @@ class UserActivityManager {
return activity.userInfo?["profileID"] as? String return activity.userInfo?["profileID"] as? String
} }
func handleShowProfile(activity: NSUserActivity) async { func handleShowProfile(activity: NSUserActivity) {
guard let accountID = Self.getProfile(from: activity) else { guard let accountID = Self.getProfile(from: activity) else {
return return
} }
await context.select(route: .timelines) context.select(route: .timelines)
context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController)) context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController))
} }
@ -361,11 +375,11 @@ class UserActivityManager {
return activity return activity
} }
func handleShowNotification(activity: NSUserActivity) async { func handleShowNotification(activity: NSUserActivity) {
guard let notificationID = activity.userInfo?["notificationID"] as? String else { guard let notificationID = activity.userInfo?["notificationID"] as? String else {
return return
} }
await context.select(route: .notifications) context.select(route: .notifications)
context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)) context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController))
} }

View File

@ -23,7 +23,7 @@ enum UserActivityType: String {
extension UserActivityType { extension UserActivityType {
@MainActor @MainActor
var handle: (UserActivityManager) -> @MainActor (NSUserActivity) async -> Void { var handle: (UserActivityManager) -> @MainActor (NSUserActivity) -> Void {
switch self { switch self {
case .mainScene: case .mainScene:
fatalError("cannot handle main scene activity") fatalError("cannot handle main scene activity")

View File

@ -61,7 +61,9 @@ class GifvController {
private func updatePresentationSizeObservation() { private func updatePresentationSizeObservation() {
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
self.presentationSizeSubject.send(item.presentationSize) DispatchQueue.main.async {
self.presentationSizeSubject.send(item.presentationSize)
}
}) })
} }

View File

@ -10,7 +10,7 @@
// https://help.apple.com/xcode/#/dev745c5c974 // https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 2024.4 MARKETING_VERSION = 2024.4
CURRENT_PROJECT_VERSION = 135 CURRENT_PROJECT_VERSION = 136
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION)) CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev