Merge branch 'develop' into compose-rewrite
|
@ -0,0 +1,157 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
sodipodi:docname="Tusker.svg"
|
||||||
|
inkscape:version="1.0beta2 (2b71d25, 2019-12-03)"
|
||||||
|
inkscape:export-ydpi="11.52"
|
||||||
|
inkscape:export-xdpi="11.52"
|
||||||
|
inkscape:export-filename="/Users/shadowfacts/Desktop/60x60@2x.png"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 264.58333 264.58333"
|
||||||
|
height="1000"
|
||||||
|
width="1000">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<inkscape:path-effect
|
||||||
|
bendpath1-nodetypes="cc"
|
||||||
|
bendpath4="M 27.271345,85.808468 V 178.94843"
|
||||||
|
bendpath3="M 27.271345,178.94843 H 242.39013"
|
||||||
|
bendpath2="M 242.39013,85.808468 V 178.94843"
|
||||||
|
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
|
||||||
|
xx="true"
|
||||||
|
yy="true"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect1345"
|
||||||
|
effect="envelope" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
allow_transforms="true"
|
||||||
|
css_properties=""
|
||||||
|
attributes=""
|
||||||
|
method="d"
|
||||||
|
linkeditem=""
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect38"
|
||||||
|
effect="clone_original" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
scale_y_rel="false"
|
||||||
|
prop_scale="1"
|
||||||
|
strokepath="M0,0 L1,0"
|
||||||
|
endpoint_spacing_variation="0;1"
|
||||||
|
endpoint_edge_variation="0;1"
|
||||||
|
startpoint_spacing_variation="0;1"
|
||||||
|
startpoint_edge_variation="0;1"
|
||||||
|
count="5"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect32"
|
||||||
|
effect="curvestitching" />
|
||||||
|
<filter
|
||||||
|
height="1.3500000000000001"
|
||||||
|
width="1.2"
|
||||||
|
id="filter1277"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
style="color-interpolation-filters:sRGB;">
|
||||||
|
<feFlood
|
||||||
|
id="feFlood1267"
|
||||||
|
result="flood"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
flood-opacity="0.321569" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1269"
|
||||||
|
result="composite1"
|
||||||
|
operator="in"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
in="flood" />
|
||||||
|
<feGaussianBlur
|
||||||
|
id="feGaussianBlur1271"
|
||||||
|
result="blur"
|
||||||
|
stdDeviation="5"
|
||||||
|
in="composite1" />
|
||||||
|
<feOffset
|
||||||
|
id="feOffset1273"
|
||||||
|
result="offset"
|
||||||
|
dy="5"
|
||||||
|
dx="-2.5" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1275"
|
||||||
|
result="composite2"
|
||||||
|
operator="over"
|
||||||
|
in2="offset"
|
||||||
|
in="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:window-y="23"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-height="1395"
|
||||||
|
inkscape:window-width="1902"
|
||||||
|
units="px"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
inkscape:current-layer="layer2"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:cy="496.39379"
|
||||||
|
inkscape:cx="442.66632"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
borderopacity="1.0"
|
||||||
|
bordercolor="#666666"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
id="base" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 2"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:groupmode="layer">
|
||||||
|
<rect
|
||||||
|
y="-0.14500916"
|
||||||
|
x="-0.14500916"
|
||||||
|
height="264.87335"
|
||||||
|
width="264.87335"
|
||||||
|
id="rect865"
|
||||||
|
style="fill:#75e04e;fill-opacity:1;stroke:#75e04e;stroke-width:0.239149;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="display:none"
|
||||||
|
id="layer1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
inkscape:label="Layer 1">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
|
||||||
|
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||||
|
id="path28" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1 copy"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="g1343">
|
||||||
|
<path
|
||||||
|
id="path1341"
|
||||||
|
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.56578600000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
|
@ -0,0 +1,153 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
sodipodi:docname="Tusker transparent.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
inkscape:export-ydpi="98.304001"
|
||||||
|
inkscape:export-xdpi="98.304001"
|
||||||
|
inkscape:export-filename="../Desktop/1024x1024-dark@1x.png"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 264.58333 264.58333"
|
||||||
|
height="1000"
|
||||||
|
width="1000"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<inkscape:path-effect
|
||||||
|
bendpath1-nodetypes="cc"
|
||||||
|
bendpath4="M 27.271345,85.808468 V 178.94843"
|
||||||
|
bendpath3="M 27.271345,178.94843 H 242.39013"
|
||||||
|
bendpath2="M 242.39013,85.808468 V 178.94843"
|
||||||
|
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
|
||||||
|
xx="true"
|
||||||
|
yy="true"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect1345"
|
||||||
|
effect="envelope" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
allow_transforms="true"
|
||||||
|
css_properties=""
|
||||||
|
attributes=""
|
||||||
|
method="d"
|
||||||
|
linkeditem=""
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect38"
|
||||||
|
effect="clone_original" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
scale_y_rel="false"
|
||||||
|
prop_scale="1"
|
||||||
|
strokepath="M0,0 L1,0"
|
||||||
|
endpoint_spacing_variation="0;1"
|
||||||
|
endpoint_edge_variation="0;1"
|
||||||
|
startpoint_spacing_variation="0;1"
|
||||||
|
startpoint_edge_variation="0;1"
|
||||||
|
count="5"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect32"
|
||||||
|
effect="curvestitching" />
|
||||||
|
<filter
|
||||||
|
height="1.317445"
|
||||||
|
width="1.1258237"
|
||||||
|
id="filter1277"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
x="-0.068723437"
|
||||||
|
y="-0.1318855">
|
||||||
|
<feFlood
|
||||||
|
id="feFlood1267"
|
||||||
|
result="flood"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
flood-opacity="0.321569" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1269"
|
||||||
|
result="composite1"
|
||||||
|
operator="in"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
in="flood" />
|
||||||
|
<feGaussianBlur
|
||||||
|
id="feGaussianBlur1271"
|
||||||
|
result="blur"
|
||||||
|
stdDeviation="5"
|
||||||
|
in="composite1" />
|
||||||
|
<feOffset
|
||||||
|
id="feOffset1273"
|
||||||
|
result="offset"
|
||||||
|
dy="5"
|
||||||
|
dx="-2.5" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1275"
|
||||||
|
result="composite2"
|
||||||
|
operator="over"
|
||||||
|
in2="offset"
|
||||||
|
in="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-height="1387"
|
||||||
|
inkscape:window-width="1280"
|
||||||
|
units="px"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
inkscape:current-layer="layer2"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:cy="404.46507"
|
||||||
|
inkscape:cx="442.29528"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
borderopacity="1.0"
|
||||||
|
bordercolor="#666666"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
id="base"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 2"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:groupmode="layer" />
|
||||||
|
<g
|
||||||
|
style="display:none"
|
||||||
|
id="layer1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
inkscape:label="Layer 1">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
|
||||||
|
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||||
|
id="path28" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1 copy"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="g1343">
|
||||||
|
<path
|
||||||
|
id="path1341"
|
||||||
|
style="fill:#75e04e;fill-opacity:1;stroke:#74e04d;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.9 KiB |
|
@ -0,0 +1,162 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
sodipodi:docname="Tusker.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
inkscape:export-ydpi="98.304001"
|
||||||
|
inkscape:export-xdpi="98.304001"
|
||||||
|
inkscape:export-filename="../Desktop/1024x1024@1x.png"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 264.58333 264.58333"
|
||||||
|
height="1000"
|
||||||
|
width="1000"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<inkscape:path-effect
|
||||||
|
bendpath1-nodetypes="cc"
|
||||||
|
bendpath4="M 27.271345,85.808468 V 178.94843"
|
||||||
|
bendpath3="M 27.271345,178.94843 H 242.39013"
|
||||||
|
bendpath2="M 242.39013,85.808468 V 178.94843"
|
||||||
|
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
|
||||||
|
xx="true"
|
||||||
|
yy="true"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect1345"
|
||||||
|
effect="envelope" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
allow_transforms="true"
|
||||||
|
css_properties=""
|
||||||
|
attributes=""
|
||||||
|
method="d"
|
||||||
|
linkeditem=""
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect38"
|
||||||
|
effect="clone_original" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
scale_y_rel="false"
|
||||||
|
prop_scale="1"
|
||||||
|
strokepath="M0,0 L1,0"
|
||||||
|
endpoint_spacing_variation="0;1"
|
||||||
|
endpoint_edge_variation="0;1"
|
||||||
|
startpoint_spacing_variation="0;1"
|
||||||
|
startpoint_edge_variation="0;1"
|
||||||
|
count="5"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect32"
|
||||||
|
effect="curvestitching" />
|
||||||
|
<filter
|
||||||
|
height="1.317445"
|
||||||
|
width="1.1258237"
|
||||||
|
id="filter1277"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
x="-0.068723437"
|
||||||
|
y="-0.1318855">
|
||||||
|
<feFlood
|
||||||
|
id="feFlood1267"
|
||||||
|
result="flood"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
flood-opacity="0.321569" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1269"
|
||||||
|
result="composite1"
|
||||||
|
operator="in"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
in="flood" />
|
||||||
|
<feGaussianBlur
|
||||||
|
id="feGaussianBlur1271"
|
||||||
|
result="blur"
|
||||||
|
stdDeviation="5"
|
||||||
|
in="composite1" />
|
||||||
|
<feOffset
|
||||||
|
id="feOffset1273"
|
||||||
|
result="offset"
|
||||||
|
dy="5"
|
||||||
|
dx="-2.5" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1275"
|
||||||
|
result="composite2"
|
||||||
|
operator="over"
|
||||||
|
in2="offset"
|
||||||
|
in="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-height="1387"
|
||||||
|
inkscape:window-width="1280"
|
||||||
|
units="px"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
inkscape:current-layer="layer2"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:cy="496.38895"
|
||||||
|
inkscape:cx="442.29528"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
borderopacity="1.0"
|
||||||
|
bordercolor="#666666"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
id="base"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 2"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:groupmode="layer">
|
||||||
|
<rect
|
||||||
|
y="-0.14500916"
|
||||||
|
x="-0.14500916"
|
||||||
|
height="264.87335"
|
||||||
|
width="264.87335"
|
||||||
|
id="rect865"
|
||||||
|
style="fill:#75e04e;fill-opacity:1;stroke:#75e04e;stroke-width:0.239149;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="display:none"
|
||||||
|
id="layer1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
inkscape:label="Layer 1">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
|
||||||
|
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||||
|
id="path28" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1 copy"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="g1343">
|
||||||
|
<path
|
||||||
|
id="path1341"
|
||||||
|
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.56578600000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||||
|
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 8.2 KiB |
|
@ -1,3 +1,18 @@
|
||||||
|
## 2024.4
|
||||||
|
This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements.
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- Import image description when adding attachments from Photos if possible
|
||||||
|
- iPadOS 18: New floating sidebar/tab bar
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash when viewing profiles in certain circumstances
|
||||||
|
- Fix video controls in attachment gallery not auto-hiding
|
||||||
|
- Fix crash if hashtag search results includes duplicates
|
||||||
|
- Fix "no content" text not being removed from list timeline after refreshing
|
||||||
|
- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on
|
||||||
|
- macOS: Fix reselecting current item not navigating back
|
||||||
|
|
||||||
## 2024.3
|
## 2024.3
|
||||||
This update includes a number of bugfixes and performance improvements. See below for a list of fixes.
|
This update includes a number of bugfixes and performance improvements. See below for a list of fixes.
|
||||||
|
|
||||||
|
|
23
CHANGELOG.md
|
@ -1,5 +1,28 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2024.4 (136)
|
||||||
|
Features/Improvements:
|
||||||
|
- Import image description when adding attachments from Photos if possible
|
||||||
|
- Reorganize toolbar buttons when adding saved hashtag
|
||||||
|
- Show errors when loading video in attachment gallery fails
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash when viewing profiles in certain circumstances
|
||||||
|
- Fix profile tab switching animation getting stuck
|
||||||
|
- Fix video controls in attachment gallery not auto-hiding
|
||||||
|
- Pleroma: Fix error when loading polls in some circumstances
|
||||||
|
- iPadOS 18: Fix incorrect two-column layout when closing sidebar
|
||||||
|
- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on
|
||||||
|
- macOS: Fix reselecting current item not navigating back
|
||||||
|
|
||||||
|
## 2024.4 (135)
|
||||||
|
Features/Improvements:
|
||||||
|
- iOS 18: New floating sidebar/tab bar
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash when hashtag search results include duplicates
|
||||||
|
- Fix "no content" text not being removed from list timeline after refreshing
|
||||||
|
|
||||||
## 2024.3 (133)
|
## 2024.3 (133)
|
||||||
- Add additional info to Tip Jar
|
- Add additional info to Tip Jar
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,13 @@ import Pachyderm
|
||||||
import Intents
|
import Intents
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
import WebURL
|
import WebURL
|
||||||
|
import UIKit
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
|
||||||
|
|
||||||
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self)
|
private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self)
|
||||||
|
@ -225,8 +229,33 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedContent: UNMutableNotificationContent
|
let updatedContent: UNMutableNotificationContent
|
||||||
|
|
||||||
|
let contentProviding: any UNNotificationContentProviding
|
||||||
|
if #available(iOS 18.0, visionOS 2.0, *),
|
||||||
|
await Preferences.shared.hasFeatureFlag(.pushNotifCustomEmoji) {
|
||||||
|
let attributedString = NSMutableAttributedString(string: content.body)
|
||||||
|
|
||||||
|
for match in emojiRegex.matches(in: content.body, range: NSRange(location: 0, length: content.body.utf16.count)).reversed() {
|
||||||
|
let emojiName = (content.body as NSString).substring(with: match.range(at: 1))
|
||||||
|
guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }),
|
||||||
|
let url = URL(emoji.url),
|
||||||
|
let (data, _) = try? await URLSession.shared.data(from: url),
|
||||||
|
let image = UIImage(data: data) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let attachment = NSTextAttachment(image: image)
|
||||||
|
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||||
|
attributedString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributedCtx = UNNotificationAttributedMessageContext(sendMessageIntent: intent, attributedContent: attributedString)
|
||||||
|
contentProviding = attributedCtx
|
||||||
|
} else {
|
||||||
|
contentProviding = intent
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let newContent = try content.updating(from: intent)
|
let newContent = try content.updating(from: contentProviding)
|
||||||
if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent {
|
if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent {
|
||||||
pendingRequest?.0 = newMutableContent
|
pendingRequest?.0 = newMutableContent
|
||||||
updatedContent = newMutableContent
|
updatedContent = newMutableContent
|
||||||
|
|
|
@ -167,11 +167,23 @@ extension DraftAttachment: NSItemProviderReading {
|
||||||
type = .png
|
type = .png
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the caption from the image itself, if there is one.
|
||||||
|
let caption: String
|
||||||
|
if let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceTypeIdentifierHint: typeIdentifier as CFString] as CFDictionary),
|
||||||
|
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any],
|
||||||
|
// This is the dictionary for TIFF properties, but it's present for other image types too
|
||||||
|
let tiffProperties = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any],
|
||||||
|
let imageDescription = tiffProperties[kCGImagePropertyTIFFImageDescription as String] as? String {
|
||||||
|
caption = imageDescription
|
||||||
|
} else {
|
||||||
|
caption = ""
|
||||||
|
}
|
||||||
|
|
||||||
let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil)
|
let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil)
|
||||||
attachment.id = UUID()
|
attachment.id = UUID()
|
||||||
attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type)
|
attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type)
|
||||||
attachment.fileType = type.identifier
|
attachment.fileType = type.identifier
|
||||||
attachment.attachmentDescription = ""
|
attachment.attachmentDescription = caption
|
||||||
return attachment
|
return attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,11 @@ public enum DuckAttemptAction {
|
||||||
|
|
||||||
extension UIViewController {
|
extension UIViewController {
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false) -> Bool {
|
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false, completion: (() -> Void)? = nil) -> Bool {
|
||||||
var cur: UIViewController? = self
|
var cur: UIViewController? = self
|
||||||
while let vc = cur {
|
while let vc = cur {
|
||||||
if let container = vc as? DuckableContainerViewController {
|
if let container = vc as? DuckableContainerViewController {
|
||||||
container.presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: nil)
|
container._presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: completion)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
cur = vc.parent
|
cur = vc.parent
|
||||||
|
|
|
@ -58,7 +58,7 @@ public class DuckableContainerViewController: UIViewController {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
func _presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||||
guard case .idle = state else {
|
guard case .idle = state else {
|
||||||
if animated,
|
if animated,
|
||||||
case .ducked(_, placeholder: let placeholder) = state {
|
case .ducked(_, placeholder: let placeholder) = state {
|
||||||
|
|
|
@ -17,7 +17,7 @@ public protocol GalleryContentViewController: UIViewController {
|
||||||
var bottomControlsAccessoryViewController: UIViewController? { get }
|
var bottomControlsAccessoryViewController: UIViewController? { get }
|
||||||
var canAnimateFromSourceView: Bool { get }
|
var canAnimateFromSourceView: Bool { get }
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool)
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
|
||||||
func galleryContentDidAppear()
|
func galleryContentDidAppear()
|
||||||
func galleryContentWillDisappear()
|
func galleryContentWillDisappear()
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ public extension GalleryContentViewController {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentDidAppear() {
|
func galleryContentDidAppear() {
|
||||||
|
|
|
@ -106,7 +106,7 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
||||||
content.view.frame = sourceFrameInContainer
|
content.view.frame = sourceFrameInContainer
|
||||||
content.view.layer.opacity = 0
|
content.view.layer.opacity = 0
|
||||||
|
|
||||||
itemViewController.setControlsVisible(false, animated: false)
|
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
|
|
|
@ -42,7 +42,7 @@ class GalleryDismissInteraction: NSObject {
|
||||||
|
|
||||||
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
||||||
if origControlsVisible! {
|
if origControlsVisible! {
|
||||||
viewController.currentItemViewController.setControlsVisible(false, animated: true)
|
viewController.currentItemViewController.setControlsVisible(false, animated: true, dueToUserInteraction: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .changed:
|
case .changed:
|
||||||
|
|
|
@ -81,10 +81,10 @@ class GalleryItemViewController: UIViewController {
|
||||||
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
|
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(overlayVC.view)
|
view.addSubview(overlayVC.view)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor),
|
overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor),
|
overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor),
|
overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor),
|
overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
|
|
||||||
updateZoomScale(resetZoom: false)
|
updateZoomScale(resetZoom: false)
|
||||||
// Ensure the transform is correct if the controls are hidden
|
// Ensure the transform is correct if the controls are hidden
|
||||||
setControlsVisible(controlsVisible, animated: false)
|
setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
updateTopControlsInsets()
|
updateTopControlsInsets()
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
}
|
}
|
||||||
centerContent()
|
centerContent()
|
||||||
// Ensure the transform is correct if the controls are hidden and their size changed.
|
// Ensure the transform is correct if the controls are hidden and their size changed.
|
||||||
setControlsVisible(controlsVisible, animated: false)
|
setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -250,7 +250,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
func addContent() {
|
func addContent() {
|
||||||
content.loadViewIfNeeded()
|
content.loadViewIfNeeded()
|
||||||
|
|
||||||
content.setControlsVisible(controlsVisible, animated: false)
|
content.setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
content.view.translatesAutoresizingMaskIntoConstraints = false
|
content.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
if content.parent != self {
|
if content.parent != self {
|
||||||
|
@ -290,7 +290,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
content.view.layoutIfNeeded()
|
content.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
controlsVisible = visible
|
controlsVisible = visible
|
||||||
|
|
||||||
guard let topControlsView,
|
guard let topControlsView,
|
||||||
|
@ -301,7 +301,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
func updateControlsViews() {
|
func updateControlsViews() {
|
||||||
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
|
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
|
||||||
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
|
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
|
||||||
content.setControlsVisible(visible, animated: animated)
|
content.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
||||||
}
|
}
|
||||||
if animated {
|
if animated {
|
||||||
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
|
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
|
||||||
|
@ -378,9 +378,6 @@ class GalleryItemViewController: UIViewController {
|
||||||
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
|
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
|
||||||
50, // iPhone 12 mini, 13 mini
|
50, // iPhone 12 mini, 13 mini
|
||||||
]
|
]
|
||||||
let islandDeviceTopInsets: [CGFloat] = [
|
|
||||||
59, // iPhone 14 Pro, 14 Pro Max, 15 Pro, 15 Pro Max
|
|
||||||
]
|
|
||||||
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||||
// the notch width is not the same for the iPhones 13,
|
// the notch width is not the same for the iPhones 13,
|
||||||
// but what we actually want is the same offset from the edges
|
// but what we actually want is the same offset from the edges
|
||||||
|
@ -390,16 +387,18 @@ class GalleryItemViewController: UIViewController {
|
||||||
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
|
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
|
||||||
shareButtonLeadingConstraint.constant = offset
|
shareButtonLeadingConstraint.constant = offset
|
||||||
closeButtonTrailingConstraint.constant = offset
|
closeButtonTrailingConstraint.constant = offset
|
||||||
} else if islandDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
} else if view.safeAreaInsets.top == 0 {
|
||||||
shareButtonLeadingConstraint.constant = 24
|
// square corner devices
|
||||||
shareButtonTopConstraint.constant = 24
|
|
||||||
closeButtonTrailingConstraint.constant = 24
|
|
||||||
closeButtonTopConstraint.constant = 24
|
|
||||||
} else {
|
|
||||||
shareButtonLeadingConstraint.constant = 8
|
shareButtonLeadingConstraint.constant = 8
|
||||||
shareButtonTopConstraint.constant = 8
|
shareButtonTopConstraint.constant = 8
|
||||||
closeButtonTrailingConstraint.constant = 8
|
closeButtonTrailingConstraint.constant = 8
|
||||||
closeButtonTopConstraint.constant = 8
|
closeButtonTopConstraint.constant = 8
|
||||||
|
} else {
|
||||||
|
// dynamic island devices
|
||||||
|
shareButtonLeadingConstraint.constant = 24
|
||||||
|
shareButtonTopConstraint.constant = 24
|
||||||
|
closeButtonTrailingConstraint.constant = 24
|
||||||
|
closeButtonTopConstraint.constant = 24
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,7 +428,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
scrollView.zoomScale > scrollView.minimumZoomScale {
|
scrollView.zoomScale > scrollView.minimumZoomScale {
|
||||||
animateZoomOut()
|
animateZoomOut()
|
||||||
} else {
|
} else {
|
||||||
setControlsVisible(!controlsVisible, animated: true)
|
setControlsVisible(!controlsVisible, animated: true, dueToUserInteraction: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,7 +530,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
|
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
|
||||||
setControlsVisible(visible, animated: animated)
|
setControlsVisible(visible, animated: animated, dueToUserInteraction: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,9 +545,9 @@ extension GalleryItemViewController: UIScrollViewDelegate {
|
||||||
|
|
||||||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||||
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
||||||
setControlsVisible(true, animated: true)
|
setControlsVisible(true, animated: true, dueToUserInteraction: true)
|
||||||
} else {
|
} else {
|
||||||
setControlsVisible(false, animated: true)
|
setControlsVisible(false, animated: true, dueToUserInteraction: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
centerContent()
|
centerContent()
|
||||||
|
|
|
@ -75,7 +75,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
container.layoutIfNeeded()
|
container.layoutIfNeeded()
|
||||||
|
|
||||||
// This needs to take place after the layout, so that the transform is correct.
|
// This needs to take place after the layout, so that the transform is correct.
|
||||||
itemViewController.setControlsVisible(false, animated: false)
|
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
let duration = self.transitionDuration(using: transitionContext)
|
let duration = self.transitionDuration(using: transitionContext)
|
||||||
// rougly equivalent to duration: 0.35, bounce: 0.3
|
// rougly equivalent to duration: 0.35, bounce: 0.3
|
||||||
|
@ -90,7 +90,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
content.view.frame = destFrameInContainer
|
content.view.frame = destFrameInContainer
|
||||||
content.view.layer.opacity = 1
|
content.view.layer.opacity = 1
|
||||||
|
|
||||||
itemViewController.setControlsVisible(true, animated: false)
|
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
if let sourceToDestTransform {
|
if let sourceToDestTransform {
|
||||||
self.sourceView.transform = sourceToDestTransform
|
self.sourceView.transform = sourceToDestTransform
|
||||||
|
|
|
@ -126,7 +126,7 @@ extension GalleryViewController: UIPageViewControllerDelegate {
|
||||||
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||||
currentItemViewController.content.galleryContentWillDisappear()
|
currentItemViewController.content.galleryContentWillDisappear()
|
||||||
let new = pendingViewControllers[0] as! GalleryItemViewController
|
let new = pendingViewControllers[0] as! GalleryItemViewController
|
||||||
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false)
|
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false, dueToUserInteraction: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ public struct Poll: Codable, Sendable {
|
||||||
public let votesCount: Int
|
public let votesCount: Int
|
||||||
public let votersCount: Int?
|
public let votersCount: Int?
|
||||||
public let voted: Bool?
|
public let voted: Bool?
|
||||||
public let ownVotes: [Int]?
|
public let ownVotes: [Int?]?
|
||||||
public let options: [Option]
|
public let options: [Option]
|
||||||
public let emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ public struct Poll: Codable, Sendable {
|
||||||
self.votesCount = try container.decode(Int.self, forKey: .votesCount)
|
self.votesCount = try container.decode(Int.self, forKey: .votesCount)
|
||||||
self.votersCount = try container.decodeIfPresent(Int.self, forKey: .votersCount)
|
self.votersCount = try container.decodeIfPresent(Int.self, forKey: .votersCount)
|
||||||
self.voted = try container.decodeIfPresent(Bool.self, forKey: .voted)
|
self.voted = try container.decodeIfPresent(Bool.self, forKey: .voted)
|
||||||
self.ownVotes = try container.decodeIfPresent([Int].self, forKey: .ownVotes)
|
self.ownVotes = try container.decodeIfPresent([Int?].self, forKey: .ownVotes)
|
||||||
self.options = try container.decode([Poll.Option].self, forKey: .options)
|
self.options = try container.decode([Poll.Option].self, forKey: .options)
|
||||||
self.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? []
|
self.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? []
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,4 +10,5 @@ import Foundation
|
||||||
public enum FeatureFlag: String, Codable {
|
public enum FeatureFlag: String, Codable {
|
||||||
case iPadBrowserNavigation = "ipad-browser-navigation"
|
case iPadBrowserNavigation = "ipad-browser-navigation"
|
||||||
case composeRewrite = "compose-rewrite"
|
case composeRewrite = "compose-rewrite"
|
||||||
|
case pushNotifCustomEmoji = "push-notif-custom-emoji"
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||||
D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */; };
|
D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */; };
|
||||||
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621733228F1D5ED004C7DB1 /* ReblogService.swift */; };
|
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621733228F1D5ED004C7DB1 /* ReblogService.swift */; };
|
||||||
|
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D62220462C7EA8DF003E43B7 /* TuskerPreferences */; };
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
|
||||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
|
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
|
||||||
|
@ -129,6 +130,10 @@
|
||||||
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
|
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
|
||||||
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
|
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
|
||||||
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; };
|
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; };
|
||||||
|
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; };
|
||||||
|
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; };
|
||||||
|
D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */; };
|
||||||
|
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */; };
|
||||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||||
|
@ -161,7 +166,6 @@
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
|
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
||||||
|
@ -215,15 +219,12 @@
|
||||||
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
|
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
|
||||||
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; };
|
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; };
|
||||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
||||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
|
|
||||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; };
|
|
||||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
||||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
|
||||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; };
|
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; };
|
||||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; };
|
|
||||||
D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */; };
|
D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */; };
|
||||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */; };
|
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */; };
|
||||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
||||||
|
@ -560,6 +561,10 @@
|
||||||
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptableNavigationController.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
@ -594,7 +599,6 @@
|
||||||
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
||||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
|
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
|
||||||
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
||||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
|
||||||
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
||||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
|
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
@ -649,15 +653,12 @@
|
||||||
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; };
|
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; };
|
||||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = "<group>"; };
|
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
|
||||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
|
||||||
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; };
|
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; };
|
|
||||||
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidescreenNavigationPrefsView.swift; sourceTree = "<group>"; };
|
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidescreenNavigationPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = "<group>"; };
|
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = "<group>"; };
|
||||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
|
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
@ -828,6 +829,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D630C4252BC7845800208903 /* WebURL in Frameworks */,
|
D630C4252BC7845800208903 /* WebURL in Frameworks */,
|
||||||
|
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
|
||||||
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
||||||
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
||||||
D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */,
|
D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */,
|
||||||
|
@ -977,7 +979,7 @@
|
||||||
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */,
|
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */,
|
||||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
||||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
||||||
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */,
|
||||||
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */,
|
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */,
|
||||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
|
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
|
||||||
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
|
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
|
||||||
|
@ -987,8 +989,6 @@
|
||||||
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */,
|
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */,
|
||||||
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */,
|
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */,
|
||||||
D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */,
|
D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */,
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
|
||||||
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
|
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
|
||||||
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */,
|
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */,
|
||||||
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */,
|
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */,
|
||||||
|
@ -1124,7 +1124,9 @@
|
||||||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
||||||
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */,
|
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */,
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||||
|
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */,
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||||
|
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */,
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||||
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
|
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
||||||
|
@ -1322,7 +1324,6 @@
|
||||||
D667E5F62135C2ED0057A976 /* Extensions */ = {
|
D667E5F62135C2ED0057A976 /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
|
|
||||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
|
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
|
||||||
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
|
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
|
||||||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */,
|
D6333B362137838300CE884A /* AttributedString+Helpers.swift */,
|
||||||
|
@ -1556,6 +1557,7 @@
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||||
D61F759129365C6C00C0B37F /* CollectionViewController.swift */,
|
D61F759129365C6C00C0B37F /* CollectionViewController.swift */,
|
||||||
|
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1795,6 +1797,7 @@
|
||||||
D630C3E42BC6313400208903 /* Pachyderm */,
|
D630C3E42BC6313400208903 /* Pachyderm */,
|
||||||
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
||||||
D630C4242BC7845800208903 /* WebURL */,
|
D630C4242BC7845800208903 /* WebURL */,
|
||||||
|
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
|
||||||
);
|
);
|
||||||
productName = NotificationExtension;
|
productName = NotificationExtension;
|
||||||
productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */;
|
productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */;
|
||||||
|
@ -2014,7 +2017,6 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
|
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
|
||||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
|
|
||||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
||||||
D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */,
|
D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||||
|
@ -2141,6 +2143,7 @@
|
||||||
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
||||||
|
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */,
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||||
|
@ -2193,6 +2196,7 @@
|
||||||
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
|
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||||
|
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
|
||||||
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
||||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
||||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
||||||
|
@ -2212,7 +2216,6 @@
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
||||||
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||||
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
||||||
|
@ -2233,6 +2236,7 @@
|
||||||
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */,
|
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */,
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
||||||
|
D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */,
|
||||||
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */,
|
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */,
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||||
D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */,
|
D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */,
|
||||||
|
@ -2252,7 +2256,6 @@
|
||||||
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
|
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
|
||||||
D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */,
|
D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */,
|
||||||
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
|
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
|
||||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||||
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */,
|
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
|
@ -2272,7 +2275,6 @@
|
||||||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
||||||
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
||||||
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
|
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
|
||||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
|
||||||
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */,
|
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */,
|
||||||
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
||||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
|
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
|
||||||
|
@ -2341,6 +2343,7 @@
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
|
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
|
||||||
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
|
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
|
||||||
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
|
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
|
@ -3299,6 +3302,10 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
};
|
};
|
||||||
|
D62220462C7EA8DF003E43B7 /* TuskerPreferences */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = TuskerPreferences;
|
||||||
|
};
|
||||||
D630C3C72BC43AFD00208903 /* PushNotifications */ = {
|
D630C3C72BC43AFD00208903 /* PushNotifications */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = PushNotifications;
|
productName = PushNotifications;
|
||||||
|
|
|
@ -292,12 +292,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||||
let rootViewController = delegate.rootViewController {
|
let rootViewController = delegate.rootViewController {
|
||||||
let mastodonController = MastodonController.getForAccount(account)
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
|
|
||||||
// if the scene is already active, then we animate the account switching if necessary
|
// if the scene is already active, then we animate things
|
||||||
delegate.activateAccount(account, animated: scene.activationState == .foregroundActive)
|
let animated = scene.activationState == .foregroundActive
|
||||||
|
|
||||||
rootViewController.select(route: .notifications, animated: false) {
|
delegate.activateAccount(account, animated: animated)
|
||||||
|
|
||||||
|
rootViewController.runNavigation(animated: animated) { navigation in
|
||||||
|
navigation.select(route: .notifications)
|
||||||
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
|
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
|
||||||
rootViewController.getNavigationController().pushViewController(vc, animated: false)
|
navigation.push(viewController: vc)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)
|
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)
|
||||||
|
|
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 723 B |
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 580 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 968 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1022 B |
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 917 B |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -2,109 +2,511 @@
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "20x20@2x.png",
|
"filename" : "20x20@2x.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "20x20@3x.png",
|
"filename" : "20x20@3x.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "29x29@2x.png",
|
"filename" : "29x29@2x.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "29x29@3x.png",
|
"filename" : "29x29@3x.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"filename" : "38x38@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "38x38@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"filename" : "40x40@2x.png",
|
"filename" : "40x40@2x.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "40x40@3x.png",
|
"filename" : "40x40@3x.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "60x60@2x.png",
|
"filename" : "40x40@3x 1.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "60x60"
|
"size" : "60x60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "60x60@3x.png",
|
"filename" : "60x60@3x.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "60x60"
|
"size" : "60x60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "20x20@1x.png",
|
"filename" : "64x64@2x.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "universal",
|
||||||
"scale" : "1x",
|
"platform" : "ios",
|
||||||
"size" : "20x20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "20x20@2x-1.png",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "20x20"
|
"size" : "64x64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "29x29@1x.png",
|
"filename" : "64x64@3x.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "universal",
|
||||||
"scale" : "1x",
|
"platform" : "ios",
|
||||||
"size" : "29x29"
|
"scale" : "3x",
|
||||||
|
"size" : "64x64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "29x29@2x-1.png",
|
"filename" : "68x68@2x.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "29x29"
|
"size" : "68x68"
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "40x40@1x.png",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "40x40"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "40x40@2x-1.png",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "40x40"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "76x76"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "76x76@2x.png",
|
"filename" : "76x76@2x.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "76x76"
|
"size" : "76x76"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "83.5x83.5@2x.png",
|
"filename" : "83.5x83.5@2x.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "83.5x83.5"
|
"size" : "83.5x83.5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "1024x1024@1x.png",
|
"filename" : "1024x1024@1x.png",
|
||||||
"idiom" : "ios-marketing",
|
"idiom" : "universal",
|
||||||
"scale" : "1x",
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "20x20-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "20x20-dark@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "29x29-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "29x29-dark@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "38x38-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "38x38-dark@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "40x40-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "40x40-dark@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "60x60-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "60x60-dark@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "64x64-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "64x64-dark@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "68x68-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "68x68"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "76x76-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "83.5x83.5-dark@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "1024x1024-dark@1x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "68x68"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -9,10 +9,14 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
class Box<Value> {
|
final class Box<Value> {
|
||||||
var wrappedValue: Value
|
var wrappedValue: Value
|
||||||
|
|
||||||
init(wrappedValue: Value) {
|
init(wrappedValue: Value) {
|
||||||
self.wrappedValue = wrappedValue
|
self.wrappedValue = wrappedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var projectedValue: Box<Value> {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import UserAccounts
|
||||||
|
|
||||||
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
|
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
|
||||||
|
|
||||||
class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Sendable {
|
||||||
|
|
||||||
private let accountInfo: UserAccountInfo?
|
private let accountInfo: UserAccountInfo?
|
||||||
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
//
|
|
||||||
// Status+Equatable.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 8/28/18.
|
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
extension Status: Equatable {
|
|
||||||
public static func ==(lhs: Status, rhs: Status) -> Bool {
|
|
||||||
return lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Account: Equatable {
|
|
||||||
public static func ==(lhs: Account, rhs: Account) -> Bool {
|
|
||||||
return lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -63,7 +63,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
let draft = mastodonController.createDraft()
|
let draft = mastodonController.createDraft()
|
||||||
let text = components.queryItems?.first(where: { $0.name == "text" })?.value
|
let text = components.queryItems?.first(where: { $0.name == "text" })?.value
|
||||||
draft.text = text ?? ""
|
draft.text = text ?? ""
|
||||||
rootViewController.compose(editing: draft, animated: true, isDucked: false)
|
rootViewController.compose(editing: draft, animated: true, isDucked: false, completion: nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Assume anything else is a search query
|
// Assume anything else is a search query
|
||||||
|
@ -83,9 +83,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
} else {
|
} else {
|
||||||
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
||||||
}
|
}
|
||||||
Task(priority: .userInitiated) {
|
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||||
_ = await userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||||
|
@ -193,11 +191,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
|
|
||||||
if let activity = launchActivity {
|
if let activity = launchActivity {
|
||||||
func doRestoreActivity(context: UserActivityHandlingContext) {
|
func doRestoreActivity(context: UserActivityHandlingContext) {
|
||||||
Task(priority: .userInitiated) {
|
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
||||||
_ = await activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
|
||||||
context.finalize(activity: activity)
|
context.finalize(activity: activity)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if activity.isStateRestorationActivity {
|
if activity.isStateRestorationActivity {
|
||||||
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
|
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
|
||||||
} else if activity.activityType != UserActivityType.mainScene.rawValue {
|
} else if activity.activityType != UserActivityType.mainScene.rawValue {
|
||||||
|
@ -266,15 +262,24 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
mastodonController.initialize()
|
mastodonController.initialize()
|
||||||
|
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
|
if #available(visionOS 2.0, *) {
|
||||||
|
return NewMainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
} else {
|
||||||
return MainTabBarViewController(mastodonController: mastodonController)
|
return MainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
let split = MainSplitViewController(mastodonController: mastodonController)
|
let mainVC: UIViewController & AccountSwitchableViewController
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
mainVC = NewMainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
} else {
|
||||||
|
mainVC = MainSplitViewController(mastodonController: mastodonController)
|
||||||
|
}
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone,
|
if UIDevice.current.userInterfaceIdiom == .phone,
|
||||||
#available(iOS 16.0, *) {
|
#available(iOS 16.0, *) {
|
||||||
// TODO: maybe the duckable container should be outside the account switching container
|
// TODO: maybe the duckable container should be outside the account switching container
|
||||||
return DuckableContainerViewController(child: split)
|
return DuckableContainerViewController(child: mainVC)
|
||||||
} else {
|
} else {
|
||||||
return split
|
return mainVC
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,7 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
let mode: AccountFollowsViewController.Mode
|
let mode: AccountFollowsViewController.Mode
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
private(set) var collectionView: UICollectionView!
|
||||||
view as? UICollectionView
|
|
||||||
}
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var state: State = .unloaded
|
private var state: State = .unloaded
|
||||||
|
@ -40,7 +38,11 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
config.backgroundColor = .appBackground
|
config.backgroundColor = .appBackground
|
||||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||||
|
@ -65,10 +67,19 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
section.readableContentInset(in: environment)
|
section.readableContentInset(in: environment)
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,7 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
private let accountIDs: [String]
|
private let accountIDs: [String]
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
private(set) var collectionView: UICollectionView!
|
||||||
view as? UICollectionView
|
|
||||||
}
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(accountIDs: [String], mastodonController: MastodonController) {
|
init(accountIDs: [String], mastodonController: MastodonController) {
|
||||||
|
@ -30,7 +28,11 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
config.backgroundColor = .appGroupedBackground
|
config.backgroundColor = .appGroupedBackground
|
||||||
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
|
@ -40,11 +42,25 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
section.readableContentInset(in: environment)
|
section.readableContentInset(in: environment)
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
snapshot.appendSections([.accounts])
|
||||||
|
snapshot.appendItems(accountIDs)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
@ -57,15 +73,6 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
||||||
snapshot.appendSections([.accounts])
|
|
||||||
snapshot.appendItems(accountIDs)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ struct AnnouncementListRow: View {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
Task {
|
Task {
|
||||||
await dismissAnnouncement()
|
await dismissAnnouncement()
|
||||||
await removeAnnouncement()
|
removeAnnouncement()
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("Dismiss", systemImage: "xmark")
|
Label("Dismiss", systemImage: "xmark")
|
||||||
|
|
|
@ -19,9 +19,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
var statusIDToScrollToOnLoad: String
|
var statusIDToScrollToOnLoad: String
|
||||||
var showStatusesAutomatically = false
|
var showStatusesAutomatically = false
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
private(set) var collectionView: UICollectionView!
|
||||||
view as? UICollectionView
|
|
||||||
}
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) {
|
init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) {
|
||||||
|
@ -38,7 +36,9 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
config.backgroundColor = .appSecondaryBackground
|
config.backgroundColor = .appSecondaryBackground
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
|
@ -66,12 +66,18 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
viewRespectsSystemMinimumLayoutMargins = false
|
viewRespectsSystemMinimumLayoutMargins = false
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
// something about the autoresizing mask breaks resizing the vc
|
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
collectionView.refreshControl = UIRefreshControl()
|
collectionView.refreshControl = UIRefreshControl()
|
||||||
|
|
|
@ -9,14 +9,14 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class AddSavedHashtagViewController: UIViewController {
|
class AddSavedHashtagViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
var resultsController: SearchResultsViewController!
|
var resultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
private(set) var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
|
@ -91,6 +91,12 @@ class AddSavedHashtagViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if searchController.isActive {
|
||||||
|
resultsController.clearSelectionOnAppear(animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
|
||||||
let request = Client.getTrendingHashtags(limit: 10)
|
let request = Client.getTrendingHashtags(limit: 10)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
@ -108,7 +114,63 @@ class AddSavedHashtagViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectHashtag(_ hashtag: Hashtag) {
|
private func selectHashtag(_ hashtag: Hashtag) {
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
let vc = HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
||||||
|
vc.loadViewIfNeeded()
|
||||||
|
|
||||||
|
let mastodonController = mastodonController!
|
||||||
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
|
let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first
|
||||||
|
let saveItem = UIBarButtonItem()
|
||||||
|
func updateSaveItem(saved: Bool) {
|
||||||
|
saveItem.title = saved ? "Unsave Hashag" : "Save Hashtag"
|
||||||
|
saveItem.image = UIImage(systemName: saved ? "minus" : "plus")
|
||||||
|
}
|
||||||
|
saveItem.primaryAction = UIAction(handler: { [unowned self] _ in
|
||||||
|
// re-fetch this in case the button's been tapped before and the captured var would be out of date
|
||||||
|
let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first
|
||||||
|
if let existingSaved {
|
||||||
|
context.delete(existingSaved)
|
||||||
|
} else {
|
||||||
|
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
|
||||||
|
}
|
||||||
|
mastodonController.persistentContainer.save(context: context)
|
||||||
|
updateSaveItem(saved: existingSaved == nil)
|
||||||
|
if existingSaved == nil {
|
||||||
|
self.presentingViewController?.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// setting primaryAction replace's the bar button's title/image with the action, so do this after
|
||||||
|
updateSaveItem(saved: existingSaved != nil)
|
||||||
|
|
||||||
|
vc.navigationItem.rightBarButtonItems = [
|
||||||
|
saveItem,
|
||||||
|
]
|
||||||
|
|
||||||
|
if mastodonController.instanceFeatures.canFollowHashtags {
|
||||||
|
let existingFollowed = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == hashtag.name })
|
||||||
|
let followItem = UIBarButtonItem()
|
||||||
|
func updateFollowItem(followed: Bool) {
|
||||||
|
followItem.title = followed ? "Unfollow Hashtag" : "Follow Hashtag"
|
||||||
|
followItem.image = UIImage(systemName: "person.badge.\(followed ? "minus" : "plus")")
|
||||||
|
}
|
||||||
|
followItem.primaryAction = UIAction(handler: { [unowned self] _ in
|
||||||
|
Task {
|
||||||
|
let success = await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: self).toggleFollow()
|
||||||
|
if success {
|
||||||
|
let followed = mastodonController.followedHashtags.contains(where: { $0.name.lowercased() == hashtag.name })
|
||||||
|
updateFollowItem(followed: followed)
|
||||||
|
if followed {
|
||||||
|
self.presentingViewController?.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
updateFollowItem(followed: existingFollowed != nil)
|
||||||
|
|
||||||
|
vc.navigationItem.rightBarButtonItems!.append(followItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
show(vc, sender: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
@ -144,3 +206,7 @@ extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate {
|
||||||
selectHashtag(hashtag)
|
selectHashtag(hashtag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AddSavedHashtagViewController: TuskerNavigationDelegate {
|
||||||
|
var apiController: MastodonController! { mastodonController }
|
||||||
|
}
|
||||||
|
|
|
@ -48,11 +48,17 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
configuration.headerMode = .supplementary
|
configuration.headerMode = .supplementary
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
applyInitialSnapshot()
|
applyInitialSnapshot()
|
||||||
|
@ -90,7 +96,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
// so we manually propagate this down to the results controller
|
// so we manually propagate this down to the results controller
|
||||||
// so that it can deselect on appear
|
// so that it can deselect on appear
|
||||||
if searchController.isActive {
|
if searchController.isActive {
|
||||||
resultsController.viewWillAppear(animated)
|
resultsController.clearSelectionOnAppear(animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSelectionOnAppear(animated: animated)
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
@ -302,6 +308,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in
|
actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
try! context.save()
|
try! context.save()
|
||||||
|
completion(true)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if mastodonController.instanceFeatures.canFollowHashtags,
|
if mastodonController.instanceFeatures.canFollowHashtags,
|
||||||
|
|
|
@ -1,152 +0,0 @@
|
||||||
//
|
|
||||||
// FeaturedProfileCollectionViewCell.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 2/6/21.
|
|
||||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
|
||||||
|
|
||||||
@IBOutlet weak var clippingView: UIView!
|
|
||||||
@IBOutlet weak var headerImageView: UIImageView!
|
|
||||||
@IBOutlet weak var avatarContainerView: UIView!
|
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
|
||||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
|
||||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
|
||||||
|
|
||||||
var account: Account?
|
|
||||||
|
|
||||||
private var accountImagesTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
accountImagesTask?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
|
||||||
super.awakeFromNib()
|
|
||||||
|
|
||||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
|
||||||
avatarContainerView.layer.cornerCurve = .continuous
|
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
|
||||||
avatarImageView.layer.cornerCurve = .continuous
|
|
||||||
|
|
||||||
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
|
||||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
|
||||||
|
|
||||||
noteTextView.adjustsFontForContentSizeCategory = true
|
|
||||||
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
|
|
||||||
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
|
|
||||||
|
|
||||||
backgroundColor = .clear
|
|
||||||
clippingView.backgroundColor = .appBackground
|
|
||||||
clippingView.layer.cornerRadius = 5
|
|
||||||
clippingView.layer.cornerCurve = .continuous
|
|
||||||
clippingView.layer.borderWidth = 1
|
|
||||||
clippingView.layer.masksToBounds = true
|
|
||||||
layer.shadowOpacity = 0.2
|
|
||||||
layer.shadowRadius = 8
|
|
||||||
layer.shadowOffset = .zero
|
|
||||||
layer.masksToBounds = false
|
|
||||||
updateLayerColors()
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUI(account: Account) {
|
|
||||||
self.account = account
|
|
||||||
|
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
|
||||||
|
|
||||||
noteTextView.setBodyTextFromHTML(account.note)
|
|
||||||
noteTextView.setEmojis(account.emojis, identifier: account.id)
|
|
||||||
|
|
||||||
avatarImageView.image = nil
|
|
||||||
headerImageView.image = nil
|
|
||||||
|
|
||||||
accountImagesTask?.cancel()
|
|
||||||
accountImagesTask = Task {
|
|
||||||
await updateImages(account: account)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private nonisolated func updateImages(account: Account) async {
|
|
||||||
await withTaskGroup(of: Void.self) { group in
|
|
||||||
group.addTask {
|
|
||||||
guard let avatar = account.avatar,
|
|
||||||
let image = await ImageCache.avatars.get(avatar).1 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await MainActor.run {
|
|
||||||
self.avatarImageView.image = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.addTask {
|
|
||||||
guard let header = account.header,
|
|
||||||
let image = await ImageCache.headers.get(header).1 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await MainActor.run {
|
|
||||||
self.headerImageView.image = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await group.waitForAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateLayerColors() {
|
|
||||||
if traitCollection.userInterfaceStyle == .dark {
|
|
||||||
clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor
|
|
||||||
layer.shadowColor = UIColor.darkGray.cgColor
|
|
||||||
} else {
|
|
||||||
clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
|
|
||||||
layer.shadowColor = UIColor.black.cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unneeded on visionOS because there is no light/dark mode
|
|
||||||
#if !os(visionOS)
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
|
||||||
updateLayerColors()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 5, cornerHeight: 5, transform: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
|
||||||
|
|
||||||
if let account = account {
|
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Accessibility
|
|
||||||
|
|
||||||
override var isAccessibilityElement: Bool {
|
|
||||||
get { true }
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var accessibilityAttributedLabel: NSAttributedString? {
|
|
||||||
get {
|
|
||||||
guard let account else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let s = NSMutableAttributedString(string: "\(account.displayNameWithoutCustomEmoji), ")
|
|
||||||
s.append(noteTextView.attributedText)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<objects>
|
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
|
||||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FeaturedProfileCollectionViewCell" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YkJ-rV-f3C">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="400" height="66"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemGray5Color"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="66" id="9Aa-Up-chJ"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RQe-uE-TEv">
|
|
||||||
<rect key="frame" x="8" y="34" width="64" height="64"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4wd-wq-Sh2">
|
|
||||||
<rect key="frame" x="2" y="2" width="60" height="60"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" constant="60" id="Xyl-Ry-J3r"/>
|
|
||||||
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="height" multiplier="1:1" id="YEc-fT-FRB"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="RQe-uE-TEv" secondAttribute="height" multiplier="1:1" id="4vR-IF-yS8"/>
|
|
||||||
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="width" constant="4" id="52Q-zq-k28"/>
|
|
||||||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerY" secondItem="RQe-uE-TEv" secondAttribute="centerY" id="Ped-H7-QtP"/>
|
|
||||||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="voW-Is-1b2" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="76" y="72" width="316" height="24"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="8" y="102" width="384" height="98"/>
|
|
||||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
|
||||||
<color key="textColor" systemColor="labelColor"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
|
||||||
</textView>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="bvj-F0-ggC" secondAttribute="trailing" constant="8" id="1sd-Df-jR1"/>
|
|
||||||
<constraint firstItem="bvj-F0-ggC" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="35h-Wh-fvk"/>
|
|
||||||
<constraint firstItem="voW-Is-1b2" firstAttribute="top" relation="greaterThanOrEqual" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="39l-yo-g8V"/>
|
|
||||||
<constraint firstItem="bo4-Sd-caI" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" id="3lQ-uN-93N"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="voW-Is-1b2" secondAttribute="trailing" constant="8" id="Ckp-Bq-lB5"/>
|
|
||||||
<constraint firstItem="bo4-Sd-caI" firstAttribute="top" secondItem="YkJ-rV-f3C" secondAttribute="top" id="DWh-S5-PLQ"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="bvj-F0-ggC" secondAttribute="bottom" id="MH3-7E-THx"/>
|
|
||||||
<constraint firstItem="RQe-uE-TEv" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="Tzo-aN-Bxq"/>
|
|
||||||
<constraint firstItem="voW-Is-1b2" firstAttribute="bottom" secondItem="4wd-wq-Sh2" secondAttribute="bottom" id="Wk6-u2-azz"/>
|
|
||||||
<constraint firstItem="RQe-uE-TEv" firstAttribute="centerY" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="bon-bj-qnk"/>
|
|
||||||
<constraint firstItem="bvj-F0-ggC" firstAttribute="top" secondItem="RQe-uE-TEv" secondAttribute="bottom" constant="4" id="dyg-LN-BDn"/>
|
|
||||||
<constraint firstItem="voW-Is-1b2" firstAttribute="leading" secondItem="RQe-uE-TEv" secondAttribute="trailing" constant="4" id="shC-67-vC2"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="bo4-Sd-caI" secondAttribute="trailing" id="wZn-gO-zue"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
</subviews>
|
|
||||||
</view>
|
|
||||||
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="YkJ-rV-f3C" secondAttribute="trailing" id="Dy3-h1-zfM"/>
|
|
||||||
<constraint firstItem="YkJ-rV-f3C" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="Gld-3x-oE0"/>
|
|
||||||
<constraint firstItem="YkJ-rV-f3C" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="NIV-n8-4Rl"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="YkJ-rV-f3C" secondAttribute="bottom" id="zNw-2z-Hlx"/>
|
|
||||||
</constraints>
|
|
||||||
<connections>
|
|
||||||
<outlet property="avatarContainerView" destination="RQe-uE-TEv" id="tBI-fT-26P"/>
|
|
||||||
<outlet property="avatarImageView" destination="4wd-wq-Sh2" id="rba-cv-8fb"/>
|
|
||||||
<outlet property="clippingView" destination="YkJ-rV-f3C" id="hLI-4z-yIc"/>
|
|
||||||
<outlet property="displayNameLabel" destination="voW-Is-1b2" id="XVS-4d-PKx"/>
|
|
||||||
<outlet property="headerImageView" destination="bo4-Sd-caI" id="YkL-Wi-BXb"/>
|
|
||||||
<outlet property="noteTextView" destination="bvj-F0-ggC" id="Bbm-ai-bu1"/>
|
|
||||||
</connections>
|
|
||||||
<point key="canvasLocation" x="535" y="428"/>
|
|
||||||
</collectionViewCell>
|
|
||||||
</objects>
|
|
||||||
<resources>
|
|
||||||
<systemColor name="labelColor">
|
|
||||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</systemColor>
|
|
||||||
<systemColor name="systemBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
<systemColor name="systemGray5Color">
|
|
||||||
<color red="0.89803921568627454" green="0.89803921568627454" blue="0.91764705882352937" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
|
@ -46,8 +46,8 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
|
@ -53,11 +53,17 @@ class TrendingHashtagsViewController: UIViewController, CollectionViewController
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
cell.indicator.startAnimating()
|
cell.indicator.startAnimating()
|
||||||
|
|
|
@ -40,6 +40,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Trending Links", comment: "trending links screen title")
|
title = NSLocalizedString("Trending Links", comment: "trending links screen title")
|
||||||
|
|
||||||
|
view.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
switch dataSource.sectionIdentifier(for: sectionIndex) {
|
switch dataSource.sectionIdentifier(for: sectionIndex) {
|
||||||
case nil:
|
case nil:
|
||||||
|
@ -80,8 +82,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
|
@ -14,9 +14,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
let filterer: Filterer
|
let filterer: Filterer
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
private(set) var collectionView: UICollectionView!
|
||||||
view as? UICollectionView
|
|
||||||
}
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var loaded = false
|
private var loaded = false
|
||||||
|
@ -34,7 +32,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||||
|
@ -62,12 +62,22 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
||||||
section.readableContentInset(in: environment)
|
section.readableContentInset(in: environment)
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
@ -96,12 +106,6 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,8 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
|
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
|
||||||
switch sectionIdentifier {
|
switch sectionIdentifier {
|
||||||
|
@ -114,12 +116,18 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.backgroundColor = .appGroupedBackground
|
collectionView.backgroundColor = .appGroupedBackground
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import UserAccounts
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||||
|
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
||||||
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
|
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
||||||
|
@ -31,7 +32,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
#endif
|
#endif
|
||||||
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
var itemOrientation: ItemOrientation = .iconsTrailing
|
private var itemOrientation: ItemOrientation = .iconsTrailing
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
||||||
|
@ -60,6 +61,9 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
|
if let delegate {
|
||||||
|
itemOrientation = delegate.fastAccountSwitcherItemOrientation(self)
|
||||||
|
}
|
||||||
createAccountViews()
|
createAccountViews()
|
||||||
// add after creating account views so that the presenter can align based on them
|
// add after creating account views so that the presenter can align based on them
|
||||||
delegate?.fastAccountSwitcherAddToViewHierarchy(self)
|
delegate?.fastAccountSwitcherAddToViewHierarchy(self)
|
||||||
|
|
|
@ -128,7 +128,7 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
if #available(iOS 16.0, macCatalyst 17.0, *),
|
if #available(iOS 16.0, macCatalyst 17.0, *),
|
||||||
let analysisInteraction {
|
let analysisInteraction {
|
||||||
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
|
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
|
||||||
|
|
|
@ -52,7 +52,7 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
||||||
if let wrapped = await provider() {
|
if let wrapped = await provider() {
|
||||||
self.wrapped = wrapped
|
self.wrapped = wrapped
|
||||||
wrapped.container = container
|
wrapped.container = container
|
||||||
wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false)
|
wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
addChild(wrapped)
|
addChild(wrapped)
|
||||||
wrapped.view.translatesAutoresizingMaskIntoConstraints = false
|
wrapped.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -102,8 +102,8 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
wrapped?.setControlsVisible(visible, animated: animated)
|
wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentDidAppear() {
|
func galleryContentDidAppear() {
|
||||||
|
|
|
@ -408,7 +408,7 @@ private class MuteButton: UIControl {
|
||||||
let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")!
|
let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")!
|
||||||
if animated,
|
if animated,
|
||||||
#available(iOS 17.0, *) {
|
#available(iOS 17.0, *) {
|
||||||
imageView.setSymbolImage(image, contentTransition: .replace.wholeSymbol, options: .speed(5))
|
imageView.setSymbolImage(image, contentTransition: .replace.byLayer)
|
||||||
} else {
|
} else {
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,17 +86,10 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
|
|
||||||
updateItemObservations()
|
updateItemObservations()
|
||||||
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
|
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
|
||||||
|
if player.rate == 0 {
|
||||||
hideControlsWorkItem?.cancel()
|
hideControlsWorkItem?.cancel()
|
||||||
if player.rate > 0 && info.oldValue == 0 {
|
} else if player.rate > 0 && info.oldValue == 0 {
|
||||||
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
scheduleControlsHide()
|
||||||
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!)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -114,12 +107,52 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
MainActor.runUnsafely {
|
MainActor.runUnsafely {
|
||||||
if item.status == .readyToPlay {
|
if item.status == .readyToPlay {
|
||||||
self.container?.setGalleryContentLoading(false)
|
self.container?.setGalleryContentLoading(false)
|
||||||
statusObservation = nil
|
self.statusObservation = nil
|
||||||
|
} else if item.status == .failed,
|
||||||
|
let error = item.error {
|
||||||
|
self.container?.setGalleryContentLoading(false)
|
||||||
|
self.showErrorView(error)
|
||||||
|
self.statusObservation = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showErrorView(_ error: any Error) {
|
||||||
|
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
||||||
|
image.tintColor = .secondaryLabel
|
||||||
|
image.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "Error Loading"
|
||||||
|
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
let reason = UILabel()
|
||||||
|
reason.text = error.localizedDescription
|
||||||
|
reason.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
reason.textColor = .secondaryLabel
|
||||||
|
reason.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
let stackView = UIStackView(arrangedSubviews: [
|
||||||
|
image,
|
||||||
|
label,
|
||||||
|
reason,
|
||||||
|
])
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.spacing = 8
|
||||||
|
view.addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
image.widthAnchor.constraint(equalToConstant: 64),
|
||||||
|
image.heightAnchor.constraint(equalToConstant: 64),
|
||||||
|
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
let isPlaying = player.rate > 0
|
let isPlaying = player.rate > 0
|
||||||
|
@ -137,6 +170,20 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func scheduleControlsHide() {
|
||||||
|
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
||||||
|
MainActor.runUnsafely {
|
||||||
|
guard let self,
|
||||||
|
let container = self.container,
|
||||||
|
container.galleryControlsVisible else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
container.setGalleryControlsVisible(false, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
@ -164,9 +211,15 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
overlayVC.setVisible(visible)
|
overlayVC.setVisible(visible)
|
||||||
|
|
||||||
|
if !visible {
|
||||||
hideControlsWorkItem?.cancel()
|
hideControlsWorkItem?.cancel()
|
||||||
|
} else if dueToUserInteraction,
|
||||||
|
player.rate > 0 {
|
||||||
|
scheduleControlsHide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentDidAppear() {
|
func galleryContentDidAppear() {
|
||||||
|
|
|
@ -56,6 +56,10 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createNoContentView() {
|
private func createNoContentView() {
|
||||||
|
guard noContentView == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let title = UILabel()
|
let title = UILabel()
|
||||||
title.textColor = .secondaryLabel
|
title.textColor = .secondaryLabel
|
||||||
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||||
|
@ -133,6 +137,9 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
override func handleReplaceAllItems(_ timelineItems: [String]) async {
|
override func handleReplaceAllItems(_ timelineItems: [String]) async {
|
||||||
if timelineItems.isEmpty {
|
if timelineItems.isEmpty {
|
||||||
createNoContentView()
|
createNoContentView()
|
||||||
|
} else {
|
||||||
|
noContentView?.removeFromSuperview()
|
||||||
|
noContentView = nil
|
||||||
}
|
}
|
||||||
await super.handleReplaceAllItems(timelineItems)
|
await super.handleReplaceAllItems(timelineItems)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
private let predicateTitle: String
|
private let predicateTitle: String
|
||||||
private let request: (RequestRange) -> Request<[TryDecode<Status>]>
|
private let request: (RequestRange) -> Request<[TryDecode<Status>]>
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
private(set) var collectionView: UICollectionView!
|
||||||
view as? UICollectionView
|
|
||||||
}
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var state = State.unloaded
|
private var state = State.unloaded
|
||||||
|
@ -43,7 +41,9 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
config.backgroundColor = .appBackground
|
config.backgroundColor = .appBackground
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
|
@ -71,12 +71,30 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
section.readableContentInset(in: environment)
|
section.readableContentInset(in: environment)
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
collectionView.refreshControl = UIRefreshControl()
|
||||||
|
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)"))
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
@ -97,20 +115,6 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
collectionView.refreshControl = UIRefreshControl()
|
|
||||||
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)"))
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,16 @@ class AccountSwitchingContainerViewController: UIViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
embedChild(root)
|
addChild(root)
|
||||||
|
root.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(root.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
root.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
root.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
root.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
root.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
root.didMove(toParent: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didReceiveMemoryWarning() {
|
override func didReceiveMemoryWarning() {
|
||||||
|
@ -70,13 +79,11 @@ class AccountSwitchingContainerViewController: UIViewController {
|
||||||
stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)")
|
stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)")
|
||||||
} else {
|
} else {
|
||||||
newRoot = newRootProvider()
|
newRoot = newRootProvider()
|
||||||
Task(priority: .userInitiated) {
|
|
||||||
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
|
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
|
||||||
let context = StateRestorationUserActivityHandlingContext(root: newRoot)
|
let context = StateRestorationUserActivityHandlingContext(root: newRoot)
|
||||||
_ = await activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
|
_ = activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
|
||||||
context.finalize(activity: activity)
|
context.finalize(activity: activity)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
newRoot = newRootProvider()
|
newRoot = newRootProvider()
|
||||||
}
|
}
|
||||||
|
@ -147,9 +154,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
return root.stateRestorationActivity()
|
return root.stateRestorationActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) {
|
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
root.compose(editing: draft, animated: animated, isDucked: isDucked)
|
root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||||
|
@ -157,11 +164,6 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
root.select(route: route, animated: animated, completion: completion)
|
root.select(route: route, animated: animated, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
|
||||||
loadViewIfNeeded()
|
|
||||||
return root.getTabController(tab: tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
return root.getNavigationDelegate()
|
return root.getNavigationDelegate()
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
//
|
||||||
|
// BaseMainTabBarViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/19/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate {
|
||||||
|
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
private(set) var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||||
|
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
||||||
|
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
||||||
|
#endif
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func show(_ vc: UIViewController, sender: Any?) {
|
||||||
|
if let nav = selectedViewController as? UINavigationController {
|
||||||
|
nav.pushViewController(vc, animated: true)
|
||||||
|
} else {
|
||||||
|
present(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast account switcher is not supported on visionOS
|
||||||
|
#if !os(visionOS)
|
||||||
|
func setupFastAccountSwitcher() {
|
||||||
|
fastAccountSwitcher = FastAccountSwitcherViewController()
|
||||||
|
fastAccountSwitcher.delegate = self
|
||||||
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
||||||
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
||||||
|
tapRecognizer.cancelsTouchesInView = false
|
||||||
|
tabBar.addGestureRecognizer(tapRecognizer)
|
||||||
|
|
||||||
|
if findMyProfileTabBarButton() != nil {
|
||||||
|
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
||||||
|
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(fastSwitcherIndicator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
// i hate that we have to do this so often :S
|
||||||
|
// but doing it only in viewWillAppear makes it not appear initially
|
||||||
|
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
|
||||||
|
repositionFastSwitcherIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
repositionFastSwitcherIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func repositionFastSwitcherIndicator() {
|
||||||
|
guard let myProfileButton = findMyProfileTabBarButton(),
|
||||||
|
myProfileButton.window != nil,
|
||||||
|
let fastSwitcherIndicator else {
|
||||||
|
fastSwitcherIndicator?.isHidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fastSwitcherIndicator.isHidden = false
|
||||||
|
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
|
||||||
|
let isPortrait = view.bounds.width < view.bounds.height
|
||||||
|
if traitCollection.horizontalSizeClass == .compact && isPortrait {
|
||||||
|
fastSwitcherConstraints = [
|
||||||
|
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
|
||||||
|
// tab bar button image width is 30
|
||||||
|
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
fastSwitcherConstraints = [
|
||||||
|
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
|
||||||
|
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate(fastSwitcherConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findMyProfileTabBarButton() -> UIView? {
|
||||||
|
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
|
||||||
|
let tabCount: Int?
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
tabCount = viewControllers?.count ?? tabs.count
|
||||||
|
} else {
|
||||||
|
tabCount = viewControllers?.count
|
||||||
|
}
|
||||||
|
// sanity check that there is 1 button per VC
|
||||||
|
guard tabBarButtons.count == tabCount,
|
||||||
|
let myProfileButton = tabBarButtons.last else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return myProfileButton
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
fastAccountSwitcher.hide()
|
||||||
|
}
|
||||||
|
#endif // !os(visionOS)
|
||||||
|
|
||||||
|
// MARK: FastAccountSwitcherViewControllerDelegate
|
||||||
|
|
||||||
|
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||||
|
return .iconsTrailing
|
||||||
|
}
|
||||||
|
|
||||||
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
|
#if !os(visionOS)
|
||||||
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
||||||
|
|
||||||
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
||||||
|
|
||||||
|
// The safe area insets don't automatically propagate for some reason, so do it ourselves.
|
||||||
|
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
])
|
||||||
|
#endif // !os(visionOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
|
#if !os(visionOS)
|
||||||
|
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
||||||
|
return myProfileButton.bounds.contains(locationInButton)
|
||||||
|
#else
|
||||||
|
return false
|
||||||
|
#endif // !os(visionOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
||||||
|
var apiController: MastodonController! { mastodonController }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BaseMainTabBarViewController: StateRestorableViewController {
|
||||||
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
|
var activity: NSUserActivity?
|
||||||
|
if let presentedNav = presentedViewController as? UINavigationController,
|
||||||
|
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
|
||||||
|
let draft = compose.controller.draft
|
||||||
|
activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID)
|
||||||
|
} else if let vc = (selectedViewController as? any NavigationControllerProtocol)?.topViewController as? StateRestorableViewController {
|
||||||
|
activity = vc.stateRestorationActivity()
|
||||||
|
}
|
||||||
|
if activity == nil {
|
||||||
|
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
|
||||||
|
}
|
||||||
|
return activity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BaseMainTabBarViewController: BackgroundableViewController {
|
||||||
|
func sceneDidEnterBackground() {
|
||||||
|
if let selectedVC = selectedViewController as? BackgroundableViewController {
|
||||||
|
selectedVC.sceneDidEnterBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BaseMainTabBarViewController: StatusBarTappableViewController {
|
||||||
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
|
guard presentedViewController == nil,
|
||||||
|
let vc = selectedViewController as? StatusBarTappableViewController else {
|
||||||
|
return .continue
|
||||||
|
}
|
||||||
|
return vc.handleStatusBarTapped(xPosition: xPosition)
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,8 +23,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) {
|
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||||
(child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked)
|
(child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||||
|
@ -39,10 +39,6 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
|
||||||
(child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion)
|
(child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
|
||||||
return (child as? TuskerRootViewController)?.getTabController(tab: tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
func performSearch(query: String) {
|
func performSearch(query: String) {
|
||||||
(child as? TuskerRootViewController)?.performSearch(query: query)
|
(child as? TuskerRootViewController)?.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,14 @@ import UserAccounts
|
||||||
|
|
||||||
class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
private var verticalImageInset: CGFloat {
|
static var verticalImageInset: CGFloat {
|
||||||
if UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
return (28 - avatarImageSize) / 2
|
return (28 - avatarImageSize) / 2
|
||||||
} else {
|
} else {
|
||||||
return (44 - avatarImageSize) / 2
|
return (44 - avatarImageSize) / 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var avatarImageSize: CGFloat {
|
static var avatarImageSize: CGFloat {
|
||||||
if UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
return 20
|
return 20
|
||||||
} else {
|
} else {
|
||||||
|
@ -72,11 +72,11 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.image = image
|
config.image = image
|
||||||
config.directionalLayoutMargins.top = self.verticalImageInset
|
config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset
|
||||||
config.directionalLayoutMargins.bottom = self.verticalImageInset
|
config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset
|
||||||
config.imageProperties.maximumSize = CGSize(width: self.avatarImageSize, height: self.avatarImageSize)
|
config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize)
|
||||||
config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0)
|
config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0)
|
||||||
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * self.avatarImageSize
|
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize
|
||||||
self.contentConfiguration = config
|
self.contentConfiguration = config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
||||||
guard var config = self.contentConfiguration as? UIListContentConfiguration else {
|
guard var config = self.contentConfiguration as? UIListContentConfiguration else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize
|
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize
|
||||||
self.contentConfiguration = config
|
self.contentConfiguration = config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ protocol MainSidebarViewControllerDelegate: AnyObject {
|
||||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?)
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?)
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?)
|
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?)
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item)
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainSidebarViewController: UIViewController {
|
class MainSidebarViewController: UIViewController {
|
||||||
|
@ -269,8 +269,9 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showAddList() {
|
private func showAddList() {
|
||||||
let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true
|
let service = CreateListService(mastodonController: mastodonController, present: {
|
||||||
) }) { list in
|
self.present($0, animated: true)
|
||||||
|
}) { list in
|
||||||
let oldItem = self.selectedItem
|
let oldItem = self.selectedItem
|
||||||
self.select(item: .list(list), animated: false)
|
self.select(item: .list(list), animated: false)
|
||||||
let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
||||||
|
@ -370,7 +371,7 @@ extension MainSidebarViewController {
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
return url.host!
|
return url.host!
|
||||||
case .addSavedInstance:
|
case .addSavedInstance:
|
||||||
return "Find An Instance..."
|
return "Find an Instance..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,7 +452,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
itemLastSelectedTimestamps[item] = Date()
|
itemLastSelectedTimestamps[item] = Date()
|
||||||
if previouslySelectedItem == item {
|
if previouslySelectedItem == item {
|
||||||
sidebarDelegate?.sidebar(self, scrollToTopFor: item)
|
sidebarDelegate?.sidebar(self, didReselectItem: item)
|
||||||
} else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
|
} else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
|
||||||
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
|
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
|
||||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import TuskerPreferences
|
import TuskerPreferences
|
||||||
|
|
||||||
|
@available(iOS, obsoleted: 18.0)
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
@ -92,7 +93,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
if UIDevice.current.userInterfaceIdiom != .mac {
|
if UIDevice.current.userInterfaceIdiom != .mac {
|
||||||
let switcher = FastAccountSwitcherViewController()
|
let switcher = FastAccountSwitcherViewController()
|
||||||
fastAccountSwitcher = switcher
|
fastAccountSwitcher = switcher
|
||||||
switcher.itemOrientation = .iconsLeading
|
|
||||||
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
switcher.delegate = self
|
switcher.delegate = self
|
||||||
// accessing .view unconditionally loads the view, which we don't want to happen
|
// accessing .view unconditionally loads the view, which we don't want to happen
|
||||||
|
@ -333,14 +333,14 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
||||||
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
|
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
|
||||||
|
|
||||||
tabBarViewController.select(tab: .explore, dismissPresented: false)
|
tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
|
||||||
|
|
||||||
case let .tab(tab):
|
case let .tab(tab):
|
||||||
// sidebar items that map 1 <-> 1 can be transferred directly
|
// sidebar items that map 1 <-> 1 can be transferred directly
|
||||||
tabBarViewController.select(tab: tab, dismissPresented: false)
|
tabBarViewController.select(tab: tab, dismissPresented: false, animated: false)
|
||||||
|
|
||||||
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
|
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||||
tabBarViewController.select(tab: .explore, dismissPresented: false)
|
tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
|
||||||
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
|
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
|
||||||
// in compact mode and performing a search.
|
// in compact mode and performing a search.
|
||||||
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||||
|
@ -493,8 +493,12 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||||
secondaryNavController.viewControllers = [viewController]
|
secondaryNavController.viewControllers = [viewController]
|
||||||
}
|
}
|
||||||
|
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) {
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item) {
|
||||||
(secondaryNavController as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
if secondaryNavController.viewControllers.count == 1 {
|
||||||
|
(secondaryNavController.topViewController as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
||||||
|
} else {
|
||||||
|
secondaryNavController.popToRootViewController(animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -578,20 +582,6 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
completion?()
|
completion?()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
|
||||||
if traitCollection.horizontalSizeClass == .compact {
|
|
||||||
return tabBarViewController?.getTabController(tab: tab)
|
|
||||||
} else {
|
|
||||||
if tab == .compose {
|
|
||||||
return nil
|
|
||||||
} else if case .tab(tab) = sidebar.selectedItem {
|
|
||||||
return secondaryNavController
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||||
if traitCollection.horizontalSizeClass == .compact {
|
if traitCollection.horizontalSizeClass == .compact {
|
||||||
return tabBarViewController.getNavigationDelegate()
|
return tabBarViewController.getNavigationDelegate()
|
||||||
|
@ -677,6 +667,10 @@ extension MainSplitViewController: BackgroundableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||||
|
return .iconsLeading
|
||||||
|
}
|
||||||
|
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
||||||
|
@ -690,6 +684,7 @@ extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
guard !isCollapsed,
|
guard !isCollapsed,
|
||||||
let cell = sidebar.myProfileCell() else {
|
let cell = sidebar.myProfileCell() else {
|
||||||
|
|
|
@ -9,18 +9,11 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
|
|
||||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
@available(iOS, obsoleted: 18.0)
|
||||||
|
class MainTabBarViewController: BaseMainTabBarViewController {
|
||||||
private let mastodonController: MastodonController
|
|
||||||
|
|
||||||
private var composePlaceholder: UIViewController!
|
private var composePlaceholder: UIViewController!
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
|
||||||
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
|
||||||
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var currentTab: Tab {
|
var currentTab: Tab {
|
||||||
return Tab(rawValue: selectedIndex)!
|
return Tab(rawValue: selectedIndex)!
|
||||||
}
|
}
|
||||||
|
@ -33,16 +26,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
|
||||||
self.mastodonController = mastodonController
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -63,107 +46,34 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
]
|
]
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
fastAccountSwitcher = FastAccountSwitcherViewController()
|
setupFastAccountSwitcher()
|
||||||
fastAccountSwitcher.delegate = self
|
|
||||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
|
||||||
tapRecognizer.cancelsTouchesInView = false
|
|
||||||
tabBar.addGestureRecognizer(tapRecognizer)
|
|
||||||
|
|
||||||
if findMyProfileTabBarButton() != nil {
|
|
||||||
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
|
||||||
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
view.addSubview(fastSwitcherIndicator)
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
tabBar.isSpringLoaded = true
|
tabBar.isSpringLoaded = true
|
||||||
|
|
||||||
|
view.backgroundColor = .appBackground
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast account switcher is not supported on visionOS
|
func select(tab: Tab, dismissPresented: Bool, animated: Bool, completion: (() -> Void)? = nil) {
|
||||||
#if !os(visionOS)
|
|
||||||
override func viewDidLayoutSubviews() {
|
|
||||||
super.viewDidLayoutSubviews()
|
|
||||||
|
|
||||||
// i hate that we have to do this so often :S
|
|
||||||
// but doing it only in viewWillAppear makes it not appear initially
|
|
||||||
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
|
|
||||||
repositionFastSwitcherIndicator()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
|
||||||
|
|
||||||
repositionFastSwitcherIndicator()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
func select(tab: Tab, dismissPresented: Bool) {
|
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
compose(editing: nil)
|
compose(editing: nil, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
// when switching tabs, dismiss the currently presented VC
|
// when switching tabs, dismiss the currently presented VC
|
||||||
// otherwise the selected tab changes behind the presented VC
|
// otherwise the selected tab changes behind the presented VC
|
||||||
if presentedViewController != nil && dismissPresented {
|
if presentedViewController != nil && dismissPresented {
|
||||||
dismiss(animated: true) {
|
dismiss(animated: animated) {
|
||||||
|
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
|
||||||
self.selectedIndex = tab.rawValue
|
self.selectedIndex = tab.rawValue
|
||||||
|
completion?()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
|
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
|
||||||
selectedIndex = tab.rawValue
|
selectedIndex = tab.rawValue
|
||||||
|
completion?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func show(_ vc: UIViewController, sender: Any?) {
|
|
||||||
if let nav = selectedViewController as? UINavigationController {
|
|
||||||
nav.pushViewController(vc, animated: true)
|
|
||||||
} else {
|
|
||||||
present(vc, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
private func repositionFastSwitcherIndicator() {
|
|
||||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
|
|
||||||
let isPortrait = view.bounds.width < view.bounds.height
|
|
||||||
if traitCollection.horizontalSizeClass == .compact && isPortrait {
|
|
||||||
fastSwitcherConstraints = [
|
|
||||||
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
|
|
||||||
// tab bar button image width is 30
|
|
||||||
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
fastSwitcherConstraints = [
|
|
||||||
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
|
|
||||||
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
NSLayoutConstraint.activate(fastSwitcherConstraints)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private func findMyProfileTabBarButton() -> UIView? {
|
|
||||||
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
|
|
||||||
// sanity check that there is 1 button per VC
|
|
||||||
guard tabBarButtons.count == viewControllers!.count,
|
|
||||||
let myProfileButton = tabBarButtons.last else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return myProfileButton
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
|
|
||||||
fastAccountSwitcher.hide()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@objc func handleComposeKeyCommand() {
|
@objc func handleComposeKeyCommand() {
|
||||||
compose(editing: nil)
|
compose(editing: nil)
|
||||||
}
|
}
|
||||||
|
@ -177,22 +87,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
|
||||||
if viewController == composePlaceholder {
|
|
||||||
compose(editing: nil)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if selectedIndex != NSNotFound,
|
|
||||||
viewController == viewControllers![selectedIndex],
|
|
||||||
let nav = viewController as? UINavigationController,
|
|
||||||
nav.viewControllers.count == 1,
|
|
||||||
let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
|
|
||||||
scrollableVC.tabBarScrollToTop()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func setViewController(_ viewController: UIViewController, forTab tab: Tab) {
|
func setViewController(_ viewController: UIViewController, forTab tab: Tab) {
|
||||||
viewControllers![tab.rawValue] = viewController
|
viewControllers![tab.rawValue] = viewController
|
||||||
}
|
}
|
||||||
|
@ -227,7 +121,7 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTabController(tab: Tab) -> UIViewController? {
|
private func getTabController(tab: Tab) -> UIViewController? {
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -238,53 +132,21 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
extension MainTabBarViewController: UITabBarControllerDelegate {
|
||||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
if viewController == composePlaceholder {
|
||||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
compose(editing: nil)
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
|
||||||
|
|
||||||
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
|
||||||
|
|
||||||
// The safe area insets don't automatically propagate for some reason, so do it ourselves.
|
|
||||||
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
|
||||||
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
|
||||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
if selectedIndex != NSNotFound,
|
||||||
return myProfileButton.bounds.contains(locationInButton)
|
viewController == viewControllers![selectedIndex],
|
||||||
|
let nav = viewController as? UINavigationController,
|
||||||
|
nav.viewControllers.count == 1,
|
||||||
|
let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
|
||||||
|
scrollableVC.tabBarScrollToTop()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
#endif
|
|
||||||
|
|
||||||
extension MainTabBarViewController: TuskerNavigationDelegate {
|
|
||||||
var apiController: MastodonController! { mastodonController }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MainTabBarViewController: StateRestorableViewController {
|
|
||||||
func stateRestorationActivity() -> NSUserActivity? {
|
|
||||||
var activity: NSUserActivity?
|
|
||||||
if let presentedNav = presentedViewController as? UINavigationController,
|
|
||||||
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
|
|
||||||
let draft = compose.controller.draft
|
|
||||||
activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID)
|
|
||||||
} else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController {
|
|
||||||
activity = vc.stateRestorationActivity()
|
|
||||||
}
|
|
||||||
if activity == nil {
|
|
||||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
|
|
||||||
}
|
|
||||||
return activity
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,25 +154,24 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||||
switch route {
|
switch route {
|
||||||
case .timelines:
|
case .timelines:
|
||||||
select(tab: .timelines, dismissPresented: true)
|
select(tab: .timelines, dismissPresented: true, animated: animated, completion: completion)
|
||||||
case .notifications:
|
case .notifications:
|
||||||
select(tab: .notifications, dismissPresented: true)
|
select(tab: .notifications, dismissPresented: true, animated: animated, completion: completion)
|
||||||
case .myProfile:
|
case .myProfile:
|
||||||
select(tab: .myProfile, dismissPresented: true)
|
select(tab: .myProfile, dismissPresented: true, animated: animated, completion: completion)
|
||||||
case .explore:
|
case .explore:
|
||||||
select(tab: .explore, dismissPresented: true)
|
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
select(tab: .explore, dismissPresented: true)
|
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
|
||||||
getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
|
getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
|
||||||
case .list(id: let id):
|
case .list(id: let id):
|
||||||
select(tab: .explore, dismissPresented: true)
|
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
|
||||||
if let list = mastodonController.getCachedList(id: id) {
|
if let list = mastodonController.getCachedList(id: id) {
|
||||||
let nav = getNavigationController()
|
let nav = getNavigationController()
|
||||||
_ = nav.popToRootViewController(animated: animated)
|
_ = nav.popToRootViewController(animated: animated)
|
||||||
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
|
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
completion?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||||
|
@ -327,7 +188,7 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
select(tab: .explore, dismissPresented: true)
|
select(tab: .explore, dismissPresented: true, animated: false)
|
||||||
exploreNavController.popToRootViewController(animated: false)
|
exploreNavController.popToRootViewController(animated: false)
|
||||||
|
|
||||||
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time
|
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time
|
||||||
|
@ -348,24 +209,6 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
present(vc, animated: true, completion: completion)
|
present(vc, animated: true, completion: completion)
|
||||||
return vc
|
return vc
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
guard presentedViewController == nil else {
|
|
||||||
return .stop
|
|
||||||
}
|
|
||||||
guard let vc = viewController(for: currentTab) as? StatusBarTappableViewController else {
|
|
||||||
return .continue
|
|
||||||
}
|
|
||||||
return vc.handleStatusBarTapped(xPosition: xPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MainTabBarViewController: BackgroundableViewController {
|
|
||||||
func sceneDidEnterBackground() {
|
|
||||||
if let selectedVC = selectedViewController as? BackgroundableViewController {
|
|
||||||
selectedVC.sceneDidEnterBackground()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: AccountSwitchableViewController {
|
extension MainTabBarViewController: AccountSwitchableViewController {
|
||||||
|
|
|
@ -0,0 +1,866 @@
|
||||||
|
//
|
||||||
|
// NewMainTabBarViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/19/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import Pachyderm
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
||||||
|
|
||||||
|
private let composePlaceholder = UIViewController()
|
||||||
|
|
||||||
|
private var homeTab: UITab!
|
||||||
|
private var notificationsTab: UITab!
|
||||||
|
private var composeTab: UITab!
|
||||||
|
private var exploreTab: UITab!
|
||||||
|
private var bookmarksTab: UITab!
|
||||||
|
private var favoritesTab: UITab!
|
||||||
|
private var myProfileTab: UITab!
|
||||||
|
private var listsGroup: UITabGroup!
|
||||||
|
private var hashtagsGroup: UITabGroup!
|
||||||
|
private var instancesGroup: UITabGroup!
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private var navigationStacks = [String: [UIViewController]]()
|
||||||
|
private var isCompact: Bool?
|
||||||
|
@Box fileprivate var myProfileCell: UIView?
|
||||||
|
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
mode = .tabSidebar
|
||||||
|
delegate = self
|
||||||
|
sidebar.delegate = self
|
||||||
|
tabBar.isSpringLoaded = true
|
||||||
|
view.backgroundColor = .appBackground
|
||||||
|
|
||||||
|
let viewControllerProvider = { [unowned self] (tab: UITab) -> UIViewController in
|
||||||
|
self.makeViewController(for: tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider)
|
||||||
|
notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider)
|
||||||
|
composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider)
|
||||||
|
exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider)
|
||||||
|
bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider)
|
||||||
|
bookmarksTab.preferredPlacement = .optional
|
||||||
|
favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider)
|
||||||
|
favoritesTab.preferredPlacement = .optional
|
||||||
|
myProfileTab = MyProfileTab(mastodonController: mastodonController, viewControllerProvider: viewControllerProvider)
|
||||||
|
|
||||||
|
listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in
|
||||||
|
// this closure is necessary to prevent UIKit from crashing (FB14860961)
|
||||||
|
return AdaptableNavigationController()
|
||||||
|
}
|
||||||
|
listsGroup.preferredPlacement = .sidebarOnly
|
||||||
|
listsGroup.sidebarActions = [
|
||||||
|
UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
|
||||||
|
self.showAddList()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
reloadLists(mastodonController.lists)
|
||||||
|
|
||||||
|
hashtagsGroup = UITabGroup(title: "Hashtags", image: nil, identifier: Tab.hashtags.rawValue, children: []) { _ in
|
||||||
|
return AdaptableNavigationController()
|
||||||
|
}
|
||||||
|
hashtagsGroup.preferredPlacement = .sidebarOnly
|
||||||
|
hashtagsGroup.sidebarActions = [
|
||||||
|
UIAction(title: "Add Hashtag…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
|
||||||
|
self.showAddSavedHashtag()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
reloadHashtags()
|
||||||
|
|
||||||
|
instancesGroup = UITabGroup(title: "Instance Timelines", image: nil, identifier: Tab.instances.rawValue, children: []) { _ in
|
||||||
|
return AdaptableNavigationController()
|
||||||
|
}
|
||||||
|
instancesGroup.preferredPlacement = .sidebarOnly
|
||||||
|
instancesGroup.sidebarActions = [
|
||||||
|
UIAction(title: "Find an Instance…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
|
||||||
|
self.showAddSavedInstance()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
reloadSavedInstances()
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone || UIDevice.current.userInterfaceIdiom == .vision {
|
||||||
|
self.tabs = [
|
||||||
|
homeTab,
|
||||||
|
notificationsTab,
|
||||||
|
composeTab,
|
||||||
|
exploreTab,
|
||||||
|
myProfileTab,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
self.updatePadTabs()
|
||||||
|
registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: NewMainTabBarViewController, previousTraitCollection) in
|
||||||
|
self.updatePadTabs()
|
||||||
|
|
||||||
|
let vcToUpdate = self.selectedTab!.parent?.viewController ?? self.selectedTab!.viewController!
|
||||||
|
self.updateViewControllerSafeAreaInsets(vcToUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
mastodonController.$lists
|
||||||
|
.sink { [unowned self] in self.reloadLists($0) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
mastodonController.$followedHashtags
|
||||||
|
.map { _ in () }
|
||||||
|
.merge(with: NotificationCenter.default.publisher(for: .savedHashtagsChanged).map { _ in () })
|
||||||
|
.sink { [unowned self] in self.reloadHashtags() }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
setupFastAccountSwitcher()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePadTabs() {
|
||||||
|
let wasCompact = isCompact
|
||||||
|
|
||||||
|
if self.traitCollection.horizontalSizeClass == .compact {
|
||||||
|
isCompact = true
|
||||||
|
|
||||||
|
var exploreNavStack: [UIViewController]? = nil
|
||||||
|
if let parent = selectedTab?.parent,
|
||||||
|
parent === listsGroup || parent === hashtagsGroup || parent === instancesGroup {
|
||||||
|
let nav = parent.viewController as! any NavigationControllerProtocol
|
||||||
|
exploreNavStack = nav.viewControllers
|
||||||
|
nav.viewControllers = []
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tabs = [
|
||||||
|
homeTab,
|
||||||
|
notificationsTab,
|
||||||
|
composeTab,
|
||||||
|
exploreTab,
|
||||||
|
myProfileTab,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let exploreNavStack {
|
||||||
|
selectedTab = exploreTab
|
||||||
|
let nav = exploreTab.viewController as! any NavigationControllerProtocol
|
||||||
|
nav.viewControllers = exploreNavStack
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isCompact = false
|
||||||
|
|
||||||
|
var newTabAndNavigationStack: (UITab, [UIViewController])? = nil
|
||||||
|
if wasCompact == true,
|
||||||
|
selectedTab == exploreTab {
|
||||||
|
let nav = exploreTab.viewController as! any NavigationControllerProtocol
|
||||||
|
// skip over the ExploreViewController
|
||||||
|
if nav.viewControllers.count > 1 {
|
||||||
|
var newTab: UITab?
|
||||||
|
switch nav.viewControllers[1] {
|
||||||
|
case let listVC as ListTimelineViewController:
|
||||||
|
if let tab = listsGroup.tab(forIdentifier: ListTab.identifier(for: listVC.list)) {
|
||||||
|
newTab = tab
|
||||||
|
}
|
||||||
|
case let hashtagVC as HashtagTimelineViewController:
|
||||||
|
if let tab = hashtagsGroup.tab(forIdentifier: HashtagTab.identifier(for: hashtagVC.hashtagName)) {
|
||||||
|
newTab = tab
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if let newTab {
|
||||||
|
newTabAndNavigationStack = (newTab, Array(nav.viewControllers[1...]))
|
||||||
|
nav.viewControllers = [
|
||||||
|
nav.viewControllers[0], // leave the ExploreVC in place
|
||||||
|
InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tabs = [
|
||||||
|
homeTab,
|
||||||
|
notificationsTab,
|
||||||
|
exploreTab,
|
||||||
|
bookmarksTab,
|
||||||
|
favoritesTab,
|
||||||
|
myProfileTab,
|
||||||
|
composeTab,
|
||||||
|
listsGroup,
|
||||||
|
hashtagsGroup,
|
||||||
|
instancesGroup,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let (tab, navStack) = newTabAndNavigationStack {
|
||||||
|
let nav = tab.parent!.viewController as! any NavigationControllerProtocol
|
||||||
|
nav.viewControllers = navStack
|
||||||
|
// Setting the tab now seems to be clobbered by the UITabBarController itself updating in response
|
||||||
|
// to the size class change. So wait until it finishes to do so.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.selectedTab = tab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeViewController(for tab: UITab) -> UIViewController {
|
||||||
|
guard let tab = Tab(rawValue: tab.identifier) else {
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
let root: UIViewController
|
||||||
|
switch tab {
|
||||||
|
case .home:
|
||||||
|
root = TimelinesPageViewController(mastodonController: mastodonController)
|
||||||
|
case .notifications:
|
||||||
|
root = NotificationsPageViewController(mastodonController: mastodonController)
|
||||||
|
case .compose:
|
||||||
|
return composePlaceholder
|
||||||
|
case .explore:
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
root = ExploreViewController(mastodonController: mastodonController)
|
||||||
|
} else {
|
||||||
|
let nav = AdaptableNavigationController(viewControllersToPrependInCompact: [
|
||||||
|
ExploreViewController(mastodonController: mastodonController)
|
||||||
|
])
|
||||||
|
nav.viewControllers = [InlineTrendsViewController(mastodonController: mastodonController)]
|
||||||
|
return nav
|
||||||
|
}
|
||||||
|
case .bookmarks:
|
||||||
|
root = BookmarksViewController(mastodonController: mastodonController)
|
||||||
|
case .favorites:
|
||||||
|
root = FavoritesViewController(mastodonController: mastodonController)
|
||||||
|
case .myProfile:
|
||||||
|
root = MyProfileViewController(mastodonController: mastodonController)
|
||||||
|
case .lists, .hashtags, .instances:
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
return embedInNavigationController(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func embedInNavigationController(_ vc: UIViewController) -> UIViewController {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return EnhancedNavigationViewController(rootViewController: vc)
|
||||||
|
} else {
|
||||||
|
let nav = AdaptableNavigationController()
|
||||||
|
nav.viewControllers = [vc]
|
||||||
|
return nav
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if sidebarTapRecognizer == nil,
|
||||||
|
let sidebarView = findSidebarView() {
|
||||||
|
sidebarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
|
||||||
|
sidebarTapRecognizer!.cancelsTouchesInView = false
|
||||||
|
sidebarView.addGestureRecognizer(sidebarTapRecognizer!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadLists(_ lists: [List]) {
|
||||||
|
let viewControllerProvider = { [unowned self] (tab: UITab) in
|
||||||
|
let tab = tab as! ListTab
|
||||||
|
return ListTimelineViewController(for: tab.list, mastodonController: self.mastodonController)
|
||||||
|
}
|
||||||
|
listsGroup.children = lists.map { list in
|
||||||
|
ListTab(list: list, viewControllerProvider: viewControllerProvider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadHashtags() {
|
||||||
|
let viewControllerProvider = { [unowned self] (tab: UITab) in
|
||||||
|
let tab = tab as! HashtagTab
|
||||||
|
return HashtagTimelineViewController(forNamed: tab.hashtagName, mastodonController: self.mastodonController)
|
||||||
|
}
|
||||||
|
var seenTags: Set<String> = []
|
||||||
|
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) {
|
||||||
|
seenTags.insert(hashtag.name)
|
||||||
|
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||||
|
hashtagsGroup.children = tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func reloadSavedInstances() {
|
||||||
|
let viewControllerProvider = { [unowned self] (tab: UITab) in
|
||||||
|
let tab = tab as! InstanceTab
|
||||||
|
return InstanceTimelineViewController(for: tab.instance.url, parentMastodonController: self.mastodonController)
|
||||||
|
}
|
||||||
|
let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
|
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
||||||
|
let instances = (try? mastodonController.persistentContainer.viewContext.fetch(req).uniques(by: \.url)) ?? []
|
||||||
|
instancesGroup.children = instances.map {
|
||||||
|
InstanceTab(instance: $0, viewControllerProvider: viewControllerProvider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func sidebarTapped() {
|
||||||
|
#if !os(visionOS)
|
||||||
|
fastAccountSwitcher?.hide()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAddList() {
|
||||||
|
let service = CreateListService(mastodonController: mastodonController, present: {
|
||||||
|
self.present($0, animated: true)
|
||||||
|
}) { list in
|
||||||
|
let tab = self.listsGroup.tab(forIdentifier: ListTab.identifier(for: list))!
|
||||||
|
let listVC = tab.viewController as! ListTimelineViewController
|
||||||
|
listVC.presentEditOnAppear = true
|
||||||
|
self.selectedTab = tab
|
||||||
|
}
|
||||||
|
service.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAddSavedHashtag() {
|
||||||
|
let addController = AddSavedHashtagViewController(mastodonController: mastodonController)
|
||||||
|
let nav = EnhancedNavigationViewController(rootViewController: addController)
|
||||||
|
present(nav, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAddSavedInstance() {
|
||||||
|
let findController = FindInstanceViewController(parentMastodonController: mastodonController)
|
||||||
|
findController.instanceTimelineDelegate = self
|
||||||
|
let nav = EnhancedNavigationViewController(rootViewController: findController)
|
||||||
|
present(nav, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) {
|
||||||
|
guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// When in sidebar mode, for multi column mode, don't leave an inset for the floating tab bar, because it leaves a massive gap.
|
||||||
|
// The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size.
|
||||||
|
vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findSidebarView() -> UIView? {
|
||||||
|
var next = myProfileCell
|
||||||
|
while let cur = next {
|
||||||
|
if cur.superview?.superview === self.view {
|
||||||
|
return cur
|
||||||
|
} else {
|
||||||
|
next = cur.superview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
override func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||||
|
guard !sidebar.isHidden,
|
||||||
|
myProfileCell != nil else {
|
||||||
|
return super.fastAccountSwitcherItemOrientation(fastAccountSwitcher)
|
||||||
|
}
|
||||||
|
return .iconsLeading
|
||||||
|
}
|
||||||
|
|
||||||
|
override func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
|
guard !sidebar.isHidden,
|
||||||
|
let myProfileCell else {
|
||||||
|
super.fastAccountSwitcherAddToViewHierarchy(fastAccountSwitcher)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
|
|
||||||
|
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor),
|
||||||
|
|
||||||
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: selectedTab!.viewController!.view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
override func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
|
guard !sidebar.isHidden,
|
||||||
|
myProfileCell != nil else {
|
||||||
|
return super.fastAccountSwitcher(fastAccountSwitcher, triggerZoneContains: point)
|
||||||
|
}
|
||||||
|
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, *)
|
||||||
|
extension NewMainTabBarViewController {
|
||||||
|
enum Tab: String, Hashable, CaseIterable {
|
||||||
|
case home
|
||||||
|
case notifications
|
||||||
|
case compose
|
||||||
|
case explore
|
||||||
|
case bookmarks
|
||||||
|
case favorites
|
||||||
|
case myProfile
|
||||||
|
|
||||||
|
case lists
|
||||||
|
case hashtags
|
||||||
|
case instances
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
||||||
|
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
||||||
|
if tab.identifier == Tab.compose.rawValue {
|
||||||
|
let currentTab = selectedTab
|
||||||
|
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
|
||||||
|
// returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state)
|
||||||
|
// so return true, and then after the tab bar VC has finished updating, go back to currentTab
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.selectedTab = currentTab
|
||||||
|
}
|
||||||
|
compose(editing: nil)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabBarController(_ tabBarController: UITabBarController, didSelectTab newTab: UITab, previousTab: UITab?) {
|
||||||
|
self.updateViewControllerSafeAreaInsets(newTab.viewController!)
|
||||||
|
|
||||||
|
// All tabs in a tab group deliberately share the same view controller, so we have to do this ourselves.
|
||||||
|
// I think this is pretty unfortunate API design--half the time, the tab bar controller takes care of
|
||||||
|
// this, but the rest of the time it's up to you.
|
||||||
|
// The managingNavigationController API would theoretically solve this, but split-screen/multi-column
|
||||||
|
// nav can't straightforwardly be implemented as UINavigationController subclasses.
|
||||||
|
// Unfortunately this, in turn, means that when switching between tabs in the same group, we don't
|
||||||
|
// get the new transition animation.
|
||||||
|
// This would be much less complicated if the controller just used the individual VCs of items in a group.
|
||||||
|
if let group = newTab.parent,
|
||||||
|
group === listsGroup || group === hashtagsGroup || group === instancesGroup,
|
||||||
|
let nav = group.viewController as? any NavigationControllerProtocol {
|
||||||
|
updateViewControllerSafeAreaInsets(nav)
|
||||||
|
|
||||||
|
if let previousTab {
|
||||||
|
navigationStacks[previousTab.identifier] = nav.viewControllers
|
||||||
|
}
|
||||||
|
|
||||||
|
if let existing = navigationStacks[newTab.identifier] {
|
||||||
|
nav.viewControllers = existing
|
||||||
|
} else if let newVC = newTab.viewController {
|
||||||
|
nav.viewControllers = [newVC]
|
||||||
|
} else {
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fastAccountSwitcherIndicator: UIView = {
|
||||||
|
let indicator = FastAccountSwitcherIndicatorView()
|
||||||
|
// need to explicitly set the frame to get it vertically centered
|
||||||
|
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
||||||
|
return indicator
|
||||||
|
}()
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
|
||||||
|
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {
|
||||||
|
let vc = selectedTab!.parent?.viewController ?? selectedTab!.viewController!
|
||||||
|
animator.addAnimations {
|
||||||
|
self.updateViewControllerSafeAreaInsets(vc)
|
||||||
|
vc.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem {
|
||||||
|
let item = UITabSidebarItem(request: request)
|
||||||
|
if case .tab(let tab) = request.content,
|
||||||
|
tab.identifier == Tab.myProfile.rawValue,
|
||||||
|
var config = item.contentConfiguration as? UIListContentConfiguration {
|
||||||
|
config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset
|
||||||
|
config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset
|
||||||
|
config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize)
|
||||||
|
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize
|
||||||
|
|
||||||
|
#if os(visionOS)
|
||||||
|
item.contentConfiguration = config
|
||||||
|
#else
|
||||||
|
if UIDevice.current.userInterfaceIdiom != .mac {
|
||||||
|
item.accessories = [
|
||||||
|
.customView(configuration: .init(customView: fastAccountSwitcherIndicator, placement: .trailing()))
|
||||||
|
]
|
||||||
|
item.contentConfiguration = MyProfileContentConfiguration(wrapped: config, view: $myProfileCell) { [unowned self] in
|
||||||
|
$0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.contentConfiguration = config
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, contextMenuConfigurationFor tab: UITab) -> UIContextMenuConfiguration? {
|
||||||
|
guard let id = mastodonController.accountInfo?.id else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let activity: NSUserActivity
|
||||||
|
|
||||||
|
if let listTab = tab as? ListTab {
|
||||||
|
let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .list(id: listTab.list.id), accountID: id)
|
||||||
|
if let timelineActivity {
|
||||||
|
activity = timelineActivity
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if let hashtagTab = tab as? HashtagTab {
|
||||||
|
let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtagTab.hashtagName), accountID: id)
|
||||||
|
if let timelineActivity {
|
||||||
|
activity = timelineActivity
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if tab is InstanceTab {
|
||||||
|
// don't currently have a scene type for this
|
||||||
|
return nil
|
||||||
|
} else if let tabID = Tab(rawValue: tab.identifier) {
|
||||||
|
switch tabID {
|
||||||
|
case .home:
|
||||||
|
return nil
|
||||||
|
case .notifications:
|
||||||
|
activity = UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id)
|
||||||
|
case .explore:
|
||||||
|
activity = UserActivityManager.searchActivity(query: nil, accountID: id)
|
||||||
|
case .bookmarks:
|
||||||
|
activity = UserActivityManager.bookmarksActivity(accountID: id)
|
||||||
|
case .favorites:
|
||||||
|
// TODO
|
||||||
|
return nil
|
||||||
|
case .myProfile:
|
||||||
|
// no 'Open in New Window' activity for my profile, because the context menu clashes with the fast account switcher
|
||||||
|
return nil
|
||||||
|
case .compose:
|
||||||
|
activity = UserActivityManager.newPostActivity(accountID: id)
|
||||||
|
case .lists, .hashtags, .instances:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
|
|
||||||
|
return UIContextMenuConfiguration(actionProvider: { _ in
|
||||||
|
var actions: [UIAction] = [
|
||||||
|
UIWindowScene.ActivationAction({ action in
|
||||||
|
return UIWindowScene.ActivationConfiguration(userActivity: activity)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
if let listTab = tab as? ListTab {
|
||||||
|
actions.append(UIAction(title: "Delete List", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in
|
||||||
|
Task {
|
||||||
|
let service = DeleteListService(list: listTab.list, mastodonController: self.mastodonController) {
|
||||||
|
self.present($0, animated: true)
|
||||||
|
}
|
||||||
|
await service.run()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIMenu(children: actions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
extension NewMainTabBarViewController: TuskerRootViewController {
|
||||||
|
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||||
|
func doSelect() {
|
||||||
|
switch route {
|
||||||
|
case .timelines:
|
||||||
|
selectedTab = tab(forIdentifier: Tab.home.rawValue)
|
||||||
|
case .notifications:
|
||||||
|
selectedTab = tab(forIdentifier: Tab.notifications.rawValue)
|
||||||
|
case .myProfile:
|
||||||
|
selectedTab = tab(forIdentifier: Tab.myProfile.rawValue)
|
||||||
|
case .explore:
|
||||||
|
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||||
|
case .bookmarks:
|
||||||
|
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||||
|
let nav = getNavigationController()
|
||||||
|
nav.popToRootViewController(animated: animated)
|
||||||
|
nav.pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
|
||||||
|
case .list(let id):
|
||||||
|
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||||
|
if let list = mastodonController.getCachedList(id: id) {
|
||||||
|
let nav = getNavigationController()
|
||||||
|
nav.popToRootViewController(animated: animated)
|
||||||
|
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
if presentedViewController != nil {
|
||||||
|
dismiss(animated: animated) {
|
||||||
|
doSelect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doSelect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNavigationDelegate() -> (any TuskerNavigationDelegate)? {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNavigationController() -> any NavigationControllerProtocol {
|
||||||
|
return selectedViewController as! any NavigationControllerProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
func performSearch(query: String) {
|
||||||
|
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
|
||||||
|
guard let exploreNavController = selectedViewController as? any NavigationControllerProtocol,
|
||||||
|
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exploreNavController.popToRootViewController(animated: false)
|
||||||
|
|
||||||
|
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time
|
||||||
|
if exploreController.isViewLoaded {
|
||||||
|
exploreController.searchController.isActive = true
|
||||||
|
} else {
|
||||||
|
exploreController.searchControllerStatusOnAppearance = true
|
||||||
|
// we still need to load the view so that we can setup the search query
|
||||||
|
exploreController.loadViewIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
exploreController.searchController.searchBar.text = query
|
||||||
|
exploreController.resultsController.performSearch(query: query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
|
||||||
|
let vc = PreferencesNavigationController(mastodonController: mastodonController)
|
||||||
|
present(vc, animated: true, completion: completion)
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
extension NewMainTabBarViewController: AccountSwitchableViewController {
|
||||||
|
var isFastAccountSwitcherActive: Bool {
|
||||||
|
#if os(visionOS)
|
||||||
|
return false
|
||||||
|
#else
|
||||||
|
if let fastAccountSwitcher {
|
||||||
|
return !fastAccountSwitcher.view.isHidden
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
extension NewMainTabBarViewController: InstanceTimelineViewControllerDelegate {
|
||||||
|
func didSaveInstance(url: URL) {
|
||||||
|
dismiss(animated: true) {
|
||||||
|
let tab = self.instancesGroup.tab(forIdentifier: InstanceTab.identifier(for: url))!
|
||||||
|
self.selectedTab = tab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func didUnsaveInstance(url: URL) {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MyProfileContentConfiguration: UIContentConfiguration {
|
||||||
|
let wrapped: any UIContentConfiguration
|
||||||
|
@Box var view: UIView?
|
||||||
|
let configureView: (UIView) -> Void
|
||||||
|
|
||||||
|
init(wrapped: any UIContentConfiguration, view: Box<UIView?>, configureView: @escaping (UIView) -> Void) {
|
||||||
|
self.wrapped = wrapped
|
||||||
|
self._view = view
|
||||||
|
self.configureView = configureView
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeContentView() -> any UIView & UIContentView {
|
||||||
|
let view = wrapped.makeContentView()
|
||||||
|
self.view = view
|
||||||
|
configureView(view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: any UIConfigurationState) -> Self {
|
||||||
|
return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
private class MyProfileTab: UITab {
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private var avatarStyle: AvatarStyle?
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController, viewControllerProvider: @escaping (UITab) -> UIViewController) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
// try to add the avatar image synchronously if possible
|
||||||
|
var avatarImage: UIImage?
|
||||||
|
if !Preferences.shared.grayscaleImages,
|
||||||
|
let account = mastodonController.account,
|
||||||
|
let avatarURL = account.avatar,
|
||||||
|
let avatar = ImageCache.avatars.get(avatarURL) {
|
||||||
|
avatarImage = Self.renderAvatar(avatar.image)
|
||||||
|
self.avatarStyle = Preferences.shared.avatarStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = avatarImage ?? UIImage(systemName: "person")!
|
||||||
|
super.init(title: "My Profile", image: image, identifier: NewMainTabBarViewController.Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider)
|
||||||
|
|
||||||
|
if avatarImage == nil {
|
||||||
|
Task {
|
||||||
|
await updateAvatar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAvatar() async {
|
||||||
|
guard let account = try? await mastodonController.getOwnAccount(),
|
||||||
|
let avatarURL = account.avatar,
|
||||||
|
let image = await ImageCache.avatars.get(avatarURL).1 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybeGrayscale = await ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) ?? image
|
||||||
|
let rendered = Self.renderAvatar(maybeGrayscale)
|
||||||
|
|
||||||
|
self.avatarStyle = Preferences.shared.avatarStyle
|
||||||
|
self.image = rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func renderAvatar(_ image: UIImage) -> UIImage {
|
||||||
|
let size = MainSidebarMyProfileCollectionViewCell.avatarImageSize
|
||||||
|
let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * size
|
||||||
|
let rect = CGRect(x: 0, y: 0, width: size, height: size)
|
||||||
|
let renderer = UIGraphicsImageRenderer(bounds: rect)
|
||||||
|
let rendered = renderer.image { ctx in
|
||||||
|
UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip()
|
||||||
|
image.draw(in: rect)
|
||||||
|
}
|
||||||
|
return rendered.withRenderingMode(.alwaysOriginal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if avatarStyle != nil,
|
||||||
|
avatarStyle != Preferences.shared.avatarStyle {
|
||||||
|
Task {
|
||||||
|
await updateAvatar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
private class ListTab: UITab {
|
||||||
|
let list: List
|
||||||
|
|
||||||
|
init(list: List, viewControllerProvider: @escaping (UITab) -> UIViewController) {
|
||||||
|
self.list = list
|
||||||
|
super.init(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.identifier(for: list), viewControllerProvider: viewControllerProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func identifier(for list: List) -> String {
|
||||||
|
"list:\(list.id)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
private class HashtagTab: UITab {
|
||||||
|
let hashtagName: String
|
||||||
|
|
||||||
|
init(hashtagName: String, viewControllerProvider: @escaping (UITab) -> UIViewController) {
|
||||||
|
self.hashtagName = hashtagName
|
||||||
|
super.init(title: hashtagName, image: UIImage(systemName: "number"), identifier: Self.identifier(for: hashtagName), viewControllerProvider: viewControllerProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func identifier(for name: String) -> String {
|
||||||
|
"hashtag:\(name)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
private class InstanceTab: UITab {
|
||||||
|
let instance: SavedInstance
|
||||||
|
|
||||||
|
init(instance: SavedInstance, viewControllerProvider: @escaping (UITab) -> UIViewController) {
|
||||||
|
self.instance = instance
|
||||||
|
super.init(title: instance.url.host!, image: UIImage(systemName: "globe"), identifier: Self.identifier(for: instance), viewControllerProvider: viewControllerProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func identifier(for instance: SavedInstance) -> String {
|
||||||
|
"instance:\(instance.url.host!)"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func identifier(for instanceURL: URL) -> String {
|
||||||
|
"instance:\(instanceURL.host!)"
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,9 +11,8 @@ import ComposeUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
|
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
|
||||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool)
|
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?)
|
||||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
|
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
|
||||||
func getNavigationDelegate() -> TuskerNavigationDelegate?
|
func getNavigationDelegate() -> TuskerNavigationDelegate?
|
||||||
func getNavigationController() -> NavigationControllerProtocol
|
func getNavigationController() -> NavigationControllerProtocol
|
||||||
func performSearch(query: String)
|
func performSearch(query: String)
|
||||||
|
@ -21,6 +20,14 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll
|
||||||
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController?
|
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TuskerRootViewController {
|
||||||
|
func runNavigation(animated: Bool, _ builder: (_ navigation: TuskerNavigationSequence) -> Void) {
|
||||||
|
let sequence = TuskerNavigationSequence(root: self, animated: animated)
|
||||||
|
builder(sequence)
|
||||||
|
sequence.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum TuskerRoute {
|
enum TuskerRoute {
|
||||||
case timelines
|
case timelines
|
||||||
case notifications
|
case notifications
|
||||||
|
@ -30,6 +37,67 @@ enum TuskerRoute {
|
||||||
case list(id: String)
|
case list(id: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A class that manages running a sequence of navigation operations on a ``TuskerRootViewController``.
|
||||||
|
///
|
||||||
|
/// Use this type, rather than calling multiple methods on the root VC in a row, because it manages waiting until each previous step finishes.
|
||||||
|
@MainActor
|
||||||
|
final class TuskerNavigationSequence {
|
||||||
|
private let root: any TuskerRootViewController
|
||||||
|
private let animated: Bool
|
||||||
|
private var operations = [() -> Void]()
|
||||||
|
|
||||||
|
init(root: any TuskerRootViewController, animated: Bool) {
|
||||||
|
self.root = root
|
||||||
|
self.animated = animated
|
||||||
|
}
|
||||||
|
|
||||||
|
func select(route: TuskerRoute) {
|
||||||
|
operations.append {
|
||||||
|
self.root.select(route: route, animated: self.animated, completion: self.run)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func push(viewController: UIViewController) {
|
||||||
|
operations.append {
|
||||||
|
let nav = self.root.getNavigationController()
|
||||||
|
nav.pushViewController(viewController, animated: self.animated)
|
||||||
|
self.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func popToRoot() {
|
||||||
|
operations.append {
|
||||||
|
let nav = self.root.getNavigationController()
|
||||||
|
nav.popToRootViewController(animated: self.animated)
|
||||||
|
self.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func present(viewController: UIViewController) {
|
||||||
|
operations.append {
|
||||||
|
self.root.present(viewController, animated: self.animated, completion: self.run)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
|
||||||
|
operations.append {
|
||||||
|
block(self.root.getNavigationController().topViewController, self.run)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addOperation(_ operation: @escaping (_ completion: @escaping () -> Void) -> Void) {
|
||||||
|
operations.append {
|
||||||
|
operation(self.run)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
if !operations.isEmpty {
|
||||||
|
operations.removeFirst()()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol NavigationControllerProtocol: UIViewController {
|
protocol NavigationControllerProtocol: UIViewController {
|
||||||
var viewControllers: [UIViewController] { get set }
|
var viewControllers: [UIViewController] { get set }
|
||||||
|
|
|
@ -108,8 +108,8 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
|
@ -184,6 +184,7 @@ struct AdvancedPrefsView : View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Feature Flags")
|
Text("Feature Flags")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ struct PushInstanceSettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscription = try await PushManager.shared.createSubscription(account: account)
|
let subscription = try await PushManager.shared.createSubscription(account: account)
|
||||||
let mastodonController = await MastodonController.getForAccount(account)
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
do {
|
do {
|
||||||
let result = try await mastodonController.createPushSubscription(subscription: subscription)
|
let result = try await mastodonController.createPushSubscription(subscription: subscription)
|
||||||
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) created on \(account.instanceURL) with endpoint \(result.endpoint, privacy: .public)")
|
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) created on \(account.instanceURL) with endpoint \(result.endpoint, privacy: .public)")
|
||||||
|
@ -95,25 +95,25 @@ struct PushInstanceSettingsView: View {
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
// if creation failed, remove the subscription locally as well
|
// if creation failed, remove the subscription locally as well
|
||||||
await PushManager.shared.removeSubscription(account: account)
|
PushManager.shared.removeSubscription(account: account)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func disableNotifications() async throws {
|
private func disableNotifications() async throws {
|
||||||
let mastodonController = await MastodonController.getForAccount(account)
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
try await mastodonController.deletePushSubscription()
|
try await mastodonController.deletePushSubscription()
|
||||||
await PushManager.shared.removeSubscription(account: account)
|
PushManager.shared.removeSubscription(account: account)
|
||||||
subscription = nil
|
subscription = nil
|
||||||
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
|
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool {
|
private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool {
|
||||||
let mastodonController = await MastodonController.getForAccount(account)
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
do {
|
do {
|
||||||
let result = try await mastodonController.updatePushSubscription(alerts: alerts, policy: policy)
|
let result = try await mastodonController.updatePushSubscription(alerts: alerts, policy: policy)
|
||||||
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) updated on \(account.instanceURL)")
|
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) updated on \(account.instanceURL)")
|
||||||
await PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy)
|
PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy)
|
||||||
subscription?.alerts = alerts
|
subscription?.alerts = alerts
|
||||||
subscription?.policy = policy
|
subscription?.policy = policy
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -38,10 +38,12 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
|
||||||
header.translatesAutoresizingMaskIntoConstraints = false
|
header.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.embedSubview(header)
|
contentView.embedSubview(header)
|
||||||
self.state = .view(header)
|
self.state = .view(header)
|
||||||
case .view(_):
|
case .view(let existing):
|
||||||
|
if existing !== header {
|
||||||
fatalError("profile header collection view cell already has view")
|
fatalError("profile header collection view cell already has view")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func addConstraint(height: CGFloat) -> ProfileHeaderView? {
|
func addConstraint(height: CGFloat) -> ProfileHeaderView? {
|
||||||
switch state {
|
switch state {
|
||||||
|
@ -61,11 +63,18 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// overrides an internal method
|
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
|
||||||
// when the super impl is used, preferredLayoutAttributesFitting(_:) isn't called while the view is offscreen (i.e., window == nil)
|
switch state {
|
||||||
// and so the collection view imposes a height of 44pts which breaks the layout
|
case .unloaded:
|
||||||
@objc func _preferredLayoutAttributesFittingAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
|
return super.preferredLayoutAttributesFitting(layoutAttributes)
|
||||||
return preferredLayoutAttributesFitting(attributes)
|
case .placeholder(let heightConstraint):
|
||||||
|
layoutAttributes.size.height = heightConstraint.constant
|
||||||
|
return layoutAttributes
|
||||||
|
case .view(let profileHeaderView):
|
||||||
|
let size = profileHeaderView.systemLayoutSizeFitting(layoutAttributes.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||||
|
layoutAttributes.size = size
|
||||||
|
return layoutAttributes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
|
|
|
@ -17,7 +17,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
let filterer: Filterer
|
let filterer: Filterer
|
||||||
private(set) var accountID: String!
|
private(set) var accountID: String!
|
||||||
let kind: Kind
|
let kind: Kind
|
||||||
var initialHeaderMode: HeaderMode?
|
var headerViewMode: HeaderMode?
|
||||||
weak var profileHeaderDelegate: ProfileHeaderViewDelegate?
|
weak var profileHeaderDelegate: ProfileHeaderViewDelegate?
|
||||||
|
|
||||||
private(set) var controller: TimelineLikeController<TimelineItem>!
|
private(set) var controller: TimelineLikeController<TimelineItem>!
|
||||||
|
@ -26,11 +26,11 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
private(set) var collectionView: UICollectionView!
|
||||||
view as? UICollectionView
|
|
||||||
}
|
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
private(set) var headerCell: ProfileHeaderCollectionViewCell?
|
var headerCell: ProfileHeaderCollectionViewCell? {
|
||||||
|
collectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? ProfileHeaderCollectionViewCell
|
||||||
|
}
|
||||||
|
|
||||||
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
|
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
|
||||||
|
|
||||||
|
@ -54,7 +54,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
config.backgroundColor = .appBackground
|
config.backgroundColor = .appBackground
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
|
@ -101,10 +103,18 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
registerTimelineLikeCells()
|
registerTimelineLikeCells()
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
|
@ -113,10 +123,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
collectionView.refreshControl = UIRefreshControl()
|
collectionView.refreshControl = UIRefreshControl()
|
||||||
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
mastodonController.persistentContainer.accountSubject
|
mastodonController.persistentContainer.accountSubject
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -173,29 +179,29 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case .header(let id):
|
case .header(let id):
|
||||||
if let headerCell = self.headerCell {
|
|
||||||
headerCell.view?.updateUI(for: id)
|
|
||||||
return headerCell
|
|
||||||
} else {
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! ProfileHeaderCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! ProfileHeaderCollectionViewCell
|
||||||
switch self.initialHeaderMode {
|
switch self.headerViewMode {
|
||||||
case nil:
|
case nil:
|
||||||
fatalError("missing initialHeaderMode")
|
fatalError("missing headerViewMode")
|
||||||
case .createView:
|
case .createViewIfNeeded:
|
||||||
|
if let view = cell.view {
|
||||||
|
view.updateUI(for: id)
|
||||||
|
self.headerViewMode = .useExistingView(view)
|
||||||
|
} else {
|
||||||
let view = ProfileHeaderView.create()
|
let view = ProfileHeaderView.create()
|
||||||
view.delegate = self.profileHeaderDelegate
|
view.delegate = self.profileHeaderDelegate
|
||||||
view.updateUI(for: id)
|
view.updateUI(for: id)
|
||||||
view.pagesSegmentedControl.setSelectedOption(self.owner!.currentPage, animated: false)
|
view.pagesSegmentedControl.setSelectedOption(self.owner!.currentPage, animated: false)
|
||||||
cell.addHeader(view)
|
cell.addHeader(view)
|
||||||
|
self.headerViewMode = .useExistingView(view)
|
||||||
|
}
|
||||||
case .useExistingView(let view):
|
case .useExistingView(let view):
|
||||||
view.updateUI(for: id)
|
view.updateUI(for: id)
|
||||||
cell.addHeader(view)
|
cell.addHeader(view)
|
||||||
case .placeholder(height: let height):
|
case .placeholder(height: let height):
|
||||||
_ = cell.addConstraint(height: height)
|
_ = cell.addConstraint(height: height)
|
||||||
}
|
}
|
||||||
self.headerCell = cell
|
|
||||||
return cell
|
return cell
|
||||||
}
|
|
||||||
case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned):
|
case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned):
|
||||||
let (result, precomputedContent) = filterResult(state: filterState, statusID: id)
|
let (result, precomputedContent) = filterResult(state: filterState, statusID: id)
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -411,7 +417,9 @@ extension ProfileStatusesViewController {
|
||||||
case statuses, withReplies, onlyMedia
|
case statuses, withReplies, onlyMedia
|
||||||
}
|
}
|
||||||
enum HeaderMode {
|
enum HeaderMode {
|
||||||
case createView, useExistingView(ProfileHeaderView), placeholder(height: CGFloat)
|
case createViewIfNeeded
|
||||||
|
case useExistingView(ProfileHeaderView)
|
||||||
|
case placeholder(height: CGFloat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -178,7 +178,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
||||||
guard let currentIndex else {
|
guard let currentIndex else {
|
||||||
assert(!animated)
|
assert(!animated)
|
||||||
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
|
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
|
||||||
new.initialHeaderMode = .createView
|
new.headerViewMode = .createViewIfNeeded
|
||||||
new.view.translatesAutoresizingMaskIntoConstraints = false
|
new.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addChild(new)
|
addChild(new)
|
||||||
view.addSubview(new.view)
|
view.addSubview(new.view)
|
||||||
|
@ -213,20 +213,24 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
||||||
|
|
||||||
// old header cell must have the header view
|
// old header cell must have the header view
|
||||||
let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)!
|
let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)!
|
||||||
|
// Set the outgoing VC's header view mode to placeholder, so that it does not steal the header view back
|
||||||
|
// in case it updates the cell in the background.
|
||||||
|
old.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height)
|
||||||
|
|
||||||
if let newHeaderCell = new.headerCell {
|
if let newHeaderCell = new.headerCell {
|
||||||
_ = newHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)
|
_ = newHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)
|
||||||
} else {
|
} else {
|
||||||
new.initialHeaderMode = .placeholder(height: oldHeaderCell.bounds.height)
|
new.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable user interaction during animation, to avoid any potential weird race conditions
|
// disable user interaction during animation, to avoid any potential weird race conditions
|
||||||
headerView.isUserInteractionEnabled = false
|
view.isUserInteractionEnabled = false
|
||||||
|
|
||||||
headerView.layer.zPosition = 100
|
headerView.layer.zPosition = 100
|
||||||
view.addSubview(headerView)
|
view.addSubview(headerView)
|
||||||
let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
|
let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
|
||||||
let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
|
let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
|
||||||
let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y// - view.safeAreaInsets.top
|
let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
|
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
|
||||||
headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset),
|
headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset),
|
||||||
|
@ -269,23 +273,24 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
||||||
}
|
}
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
old.removeViewAndController()
|
old.removeViewAndController()
|
||||||
old.collectionView.transform = .identity
|
old.view.transform = .identity
|
||||||
|
|
||||||
new.collectionView.transform = .identity
|
new.view.transform = .identity
|
||||||
new.collectionView.contentOffset = origOldContentOffset
|
new.collectionView.contentOffset = origOldContentOffset
|
||||||
|
|
||||||
// reenable scroll indicators after the switching animation is done
|
// reenable scroll indicators after the switching animation is done
|
||||||
old.collectionView.showsVerticalScrollIndicator = true
|
old.collectionView.showsVerticalScrollIndicator = true
|
||||||
new.collectionView.showsVerticalScrollIndicator = true
|
new.collectionView.showsVerticalScrollIndicator = true
|
||||||
|
|
||||||
headerView.isUserInteractionEnabled = true
|
self.view.isUserInteractionEnabled = true
|
||||||
|
|
||||||
headerView.transform = .identity
|
headerView.transform = .identity
|
||||||
headerView.layer.zPosition = 0
|
headerView.layer.zPosition = 0
|
||||||
// move the header view into the new page controller's cell
|
// move the header view into the new page controller's cell
|
||||||
if let newHeaderCell = new.headerCell {
|
if let newHeaderCell = new.headerCell {
|
||||||
newHeaderCell.addHeader(headerView)
|
newHeaderCell.addHeader(headerView)
|
||||||
} else {
|
} else {
|
||||||
new.initialHeaderMode = .useExistingView(headerView)
|
new.headerViewMode = .useExistingView(headerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state = .idle
|
self.state = .idle
|
||||||
|
|
|
@ -36,7 +36,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
weak var delegate: SearchResultsViewControllerDelegate?
|
weak var delegate: SearchResultsViewControllerDelegate?
|
||||||
var tokenHandler: ((String, SearchOperatorType) -> Void)?
|
var tokenHandler: ((String, SearchOperatorType) -> Void)?
|
||||||
|
|
||||||
var collectionView: UICollectionView! { view as? UICollectionView }
|
private(set) var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
/// Types of results to search for.
|
/// Types of results to search for.
|
||||||
|
@ -62,7 +62,9 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)!
|
let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)!
|
||||||
switch sectionIdentifier {
|
switch sectionIdentifier {
|
||||||
|
@ -102,7 +104,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
return .list(using: config, layoutEnvironment: environment)
|
return .list(using: config, layoutEnvironment: environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
@ -110,12 +112,16 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
collectionView.keyboardDismissMode = .interactive
|
collectionView.keyboardDismissMode = .interactive
|
||||||
#endif
|
#endif
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
searchCancellable = searchSubject
|
searchCancellable = searchSubject
|
||||||
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
||||||
|
@ -295,7 +301,10 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) {
|
if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) {
|
||||||
snapshot.appendSections([.hashtags])
|
snapshot.appendSections([.hashtags])
|
||||||
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags)
|
// mastodon sometimes includes duplicate hashtags with the same name but different urls
|
||||||
|
// (e.g., containing %C3%B8 vs o)
|
||||||
|
let uniqueHashtags = results.hashtags.uniques(by: \.name)
|
||||||
|
snapshot.appendItems(uniqueHashtags.map { .hashtag($0) }, toSection: .hashtags)
|
||||||
}
|
}
|
||||||
if !results.statuses.isEmpty && resultTypes.contains(.statuses) {
|
if !results.statuses.isEmpty && resultTypes.contains(.statuses) {
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.statuses])
|
||||||
|
|