Compare commits

..

No commits in common. "c99c397cf6d4ba2ac36bf40a72874dc8576e67db" and "805e5eddd0ab7adf8ddf470436dbe5bd21925036" have entirely different histories.

87 changed files with 300 additions and 1477 deletions

View File

@ -1,157 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,153 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -1,162 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -1,18 +1,3 @@
## 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
This update includes a number of bugfixes and performance improvements. See below for a list of fixes.

View File

@ -1,20 +1,5 @@
# 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)
Features/Improvements:
- iOS 18: New floating sidebar/tab bar

View File

@ -15,13 +15,9 @@ import Pachyderm
import Intents
import HTMLStreamer
import WebURL
import UIKit
import TuskerPreferences
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
class NotificationService: UNNotificationServiceExtension {
private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self)
@ -229,33 +225,8 @@ class NotificationService: UNNotificationServiceExtension {
}
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 {
let newContent = try content.updating(from: contentProviding)
let newContent = try content.updating(from: intent)
if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent {
pendingRequest?.0 = newMutableContent
updatedContent = newMutableContent

View File

@ -167,23 +167,11 @@ extension DraftAttachment: NSItemProviderReading {
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)
attachment.id = UUID()
attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type)
attachment.fileType = type.identifier
attachment.attachmentDescription = caption
attachment.attachmentDescription = ""
return attachment
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -126,7 +126,7 @@ extension GalleryViewController: UIPageViewControllerDelegate {
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
currentItemViewController.content.galleryContentWillDisappear()
let new = pendingViewControllers[0] as! GalleryItemViewController
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false, dueToUserInteraction: false)
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false)
}
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 votersCount: Int?
public let voted: Bool?
public let ownVotes: [Int?]?
public let ownVotes: [Int]?
public let options: [Option]
public let emojis: [Emoji]
@ -33,7 +33,7 @@ public struct Poll: Codable, Sendable {
self.votesCount = try container.decode(Int.self, forKey: .votesCount)
self.votersCount = try container.decodeIfPresent(Int.self, forKey: .votersCount)
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.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? []
}

View File

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

View File

@ -77,7 +77,6 @@
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.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 */; };
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
@ -166,6 +165,7 @@
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
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 */; };
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
@ -599,6 +599,7 @@
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>"; };
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>"; };
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>"; };
@ -829,7 +830,6 @@
buildActionMask = 2147483647;
files = (
D630C4252BC7845800208903 /* WebURL in Frameworks */,
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */,
@ -1324,6 +1324,7 @@
D667E5F62135C2ED0057A976 /* Extensions */ = {
isa = PBXGroup;
children = (
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
D6333B362137838300CE884A /* AttributedString+Helpers.swift */,
@ -1797,7 +1798,6 @@
D630C3E42BC6313400208903 /* Pachyderm */,
D630C4222BC7842C00208903 /* HTMLStreamer */,
D630C4242BC7845800208903 /* WebURL */,
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
);
productName = NotificationExtension;
productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */;
@ -2216,6 +2216,7 @@
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
@ -3302,10 +3303,6 @@
isa = XCSwiftPackageProductDependency;
productName = Pachyderm;
};
D62220462C7EA8DF003E43B7 /* TuskerPreferences */ = {
isa = XCSwiftPackageProductDependency;
productName = TuskerPreferences;
};
D630C3C72BC43AFD00208903 /* PushNotifications */ = {
isa = XCSwiftPackageProductDependency;
productName = PushNotifications;

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -2,511 +2,109 @@
"images" : [
{
"filename" : "20x20@2x.png",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "20x20@3x.png",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29x29@2x.png",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "29x29@3x.png",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "iphone",
"scale" : "3x",
"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",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "40x40@3x.png",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "40x40@3x 1.png",
"idiom" : "universal",
"platform" : "ios",
"filename" : "60x60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "60x60@3x.png",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "64x64@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
"filename" : "20x20@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "64x64@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
"filename" : "20x20@2x-1.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "68x68@2x.png",
"idiom" : "universal",
"platform" : "ios",
"filename" : "29x29@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "29x29@2x-1.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "68x68"
"size" : "29x29"
},
{
"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",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "83.5x83.5@2x.png",
"idiom" : "universal",
"platform" : "ios",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024x1024@1x.png",
"idiom" : "universal",
"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",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],

View File

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

View File

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

View File

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

View File

@ -9,14 +9,14 @@
import UIKit
import Pachyderm
class AddSavedHashtagViewController: UIViewController, CollectionViewController {
class AddSavedHashtagViewController: UIViewController {
weak var mastodonController: MastodonController!
var resultsController: SearchResultsViewController!
var searchController: UISearchController!
private(set) var collectionView: UICollectionView!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) {
@ -91,12 +91,6 @@ class AddSavedHashtagViewController: UIViewController, CollectionViewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if searchController.isActive {
resultsController.clearSelectionOnAppear(animated: animated)
}
clearSelectionOnAppear(animated: animated)
let request = Client.getTrendingHashtags(limit: 10)
mastodonController.run(request) { (response) in
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
@ -114,63 +108,7 @@ class AddSavedHashtagViewController: UIViewController, CollectionViewController
}
private func selectHashtag(_ hashtag: Hashtag) {
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)
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
}
// MARK: - Interaction
@ -206,7 +144,3 @@ extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate {
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 that it can deselect on appear
if searchController.isActive {
resultsController.clearSelectionOnAppear(animated: animated)
resultsController.viewWillAppear(animated)
}
clearSelectionOnAppear(animated: animated)
@ -308,7 +308,6 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in
context.delete(existing)
try! context.save()
completion(true)
}))
}
if mastodonController.instanceFeatures.canFollowHashtags,

View File

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

View File

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

View File

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

View File

@ -86,10 +86,17 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
updateItemObservations()
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
if player.rate == 0 {
hideControlsWorkItem?.cancel()
} else if player.rate > 0 && info.oldValue == 0 {
scheduleControlsHide()
hideControlsWorkItem?.cancel()
if player.rate > 0 && info.oldValue == 0 {
hideControlsWorkItem = DispatchWorkItem { [weak self] in
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!)
}
})
@ -107,52 +114,12 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
MainActor.runUnsafely {
if item.status == .readyToPlay {
self.container?.setGalleryContentLoading(false)
self.statusObservation = nil
} else if item.status == .failed,
let error = item.error {
self.container?.setGalleryContentLoading(false)
self.showErrorView(error)
self.statusObservation = nil
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() {
if isGrayscale != Preferences.shared.grayscaleImages {
let isPlaying = player.rate > 0
@ -170,20 +137,6 @@ 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
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
@ -211,15 +164,9 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
#endif
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
func setControlsVisible(_ visible: Bool, animated: Bool) {
overlayVC.setVisible(visible)
if !visible {
hideControlsWorkItem?.cancel()
} else if dueToUserInteraction,
player.rate > 0 {
scheduleControlsHide()
}
hideControlsWorkItem?.cancel()
}
func galleryContentDidAppear() {

View File

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

View File

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

View File

@ -15,7 +15,7 @@ protocol MainSidebarViewControllerDelegate: AnyObject {
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
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, didReselectItem item: MainSidebarViewController.Item)
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item)
}
class MainSidebarViewController: UIViewController {
@ -452,7 +452,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
}
itemLastSelectedTimestamps[item] = Date()
if previouslySelectedItem == item {
sidebarDelegate?.sidebar(self, didReselectItem: item)
sidebarDelegate?.sidebar(self, scrollToTopFor: item)
} else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
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
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
tabBarViewController.select(tab: .explore, dismissPresented: false)
case let .tab(tab):
// sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab, dismissPresented: false, animated: false)
tabBarViewController.select(tab: tab, dismissPresented: false)
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
tabBarViewController.select(tab: .explore, dismissPresented: false)
// 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.
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
@ -493,12 +493,8 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
secondaryNavController.viewControllers = [viewController]
}
func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item) {
if secondaryNavController.viewControllers.count == 1 {
(secondaryNavController.topViewController as? TabBarScrollableViewController)?.tabBarScrollToTop()
} else {
secondaryNavController.popToRootViewController(animated: true)
}
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) {
(secondaryNavController as? TabBarScrollableViewController)?.tabBarScrollToTop()
}
}

View File

@ -54,22 +54,19 @@ class MainTabBarViewController: BaseMainTabBarViewController {
view.backgroundColor = .appBackground
}
func select(tab: Tab, dismissPresented: Bool, animated: Bool, completion: (() -> Void)? = nil) {
func select(tab: Tab, dismissPresented: Bool) {
if tab == .compose {
compose(editing: nil, completion: completion)
compose(editing: nil)
} else {
// when switching tabs, dismiss the currently presented VC
// otherwise the selected tab changes behind the presented VC
if presentedViewController != nil && dismissPresented {
dismiss(animated: animated) {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
dismiss(animated: true) {
self.selectedIndex = tab.rawValue
completion?()
}
} else {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
selectedIndex = tab.rawValue
completion?()
}
}
}
@ -154,24 +151,25 @@ extension MainTabBarViewController: TuskerRootViewController {
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
switch route {
case .timelines:
select(tab: .timelines, dismissPresented: true, animated: animated, completion: completion)
select(tab: .timelines, dismissPresented: true)
case .notifications:
select(tab: .notifications, dismissPresented: true, animated: animated, completion: completion)
select(tab: .notifications, dismissPresented: true)
case .myProfile:
select(tab: .myProfile, dismissPresented: true, animated: animated, completion: completion)
select(tab: .myProfile, dismissPresented: true)
case .explore:
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
select(tab: .explore, dismissPresented: true)
case .bookmarks:
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
select(tab: .explore, dismissPresented: true)
getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
case .list(id: let id):
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
select(tab: .explore, dismissPresented: true)
if let list = mastodonController.getCachedList(id: id) {
let nav = getNavigationController()
_ = nav.popToRootViewController(animated: animated)
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
}
}
completion?()
}
func getNavigationDelegate() -> TuskerNavigationDelegate? {
@ -188,7 +186,7 @@ extension MainTabBarViewController: TuskerRootViewController {
return
}
select(tab: .explore, dismissPresented: true, animated: false)
select(tab: .explore, dismissPresented: true)
exploreNavController.popToRootViewController(animated: false)
// 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 {
if UIDevice.current.userInterfaceIdiom == .phone {
return EnhancedNavigationViewController(rootViewController: vc)
return UINavigationController(rootViewController: vc)
} else {
let nav = AdaptableNavigationController()
nav.viewControllers = [vc]
@ -285,7 +285,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
var tabs: [UITab] = []
let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? []
for hashtag in saved where !seenTags.contains(hashtag.name) {
for hashtag in saved {
seenTags.insert(hashtag.name)
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
}
@ -293,7 +293,6 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
let followedReq = FollowedHashtag.fetchRequest()
let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? []
for hashtag in followed where !seenTags.contains(hashtag.name) {
seenTags.insert(hashtag.name)
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
}
@ -314,6 +313,10 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
}
}
@objc func handleComposeKeyCommand() {
compose(editing: nil)
}
@objc private func sidebarTapped() {
#if !os(visionOS)
fastAccountSwitcher?.hide()
@ -404,33 +407,6 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
return true
}
#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, *)
@ -465,12 +441,10 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
return true
} else if let selectedTab,
selectedTab == tab,
let nav = selectedViewController as? any NavigationControllerProtocol {
if nav.viewControllers.count == 1 {
(nav.viewControllers[0] as? TabBarScrollableViewController)?.tabBarScrollToTop()
} else {
nav.popToRootViewController(animated: true)
}
let nav = selectedViewController as? any NavigationControllerProtocol,
nav.viewControllers.count == 1,
let scrollableVC = nav.viewControllers[0] as? TabBarScrollableViewController {
scrollableVC.tabBarScrollToTop()
return false
} else {
return true

View File

@ -20,14 +20,6 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll
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 {
case timelines
case notifications
@ -37,67 +29,6 @@ enum TuskerRoute {
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
protocol NavigationControllerProtocol: UIViewController {
var viewControllers: [UIViewController] { get set }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -241,19 +241,13 @@ class SplitNavigationController: UIViewController {
// otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
self.isLayingOutForAnimation = true
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.setSecondaryVisible(false)
self.view.layoutIfNeeded()
}
animator.addCompletion { _ in
self.isLayingOutForAnimation = false
self.secondaryNav.viewControllers = []
self.updateSecondaryNavVisibility()
self.isLayingOutForAnimation = false
// self.updateSecondaryNavVisibility()
}
animator.startAnimation()
} else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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