#106: Add Analytics UI

This commit is contained in:
Doug
2022-09-02 10:49:59 +01:00
committed by Doug
parent df68174a32
commit 8e46d0e3d6
26 changed files with 1437 additions and 85 deletions
@@ -0,0 +1,123 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.000000 -1.000000 cm
0.049479 0.742188 0.545395 scn
10.000000 23.000000 m
3.947715 23.000000 -1.000000 18.052284 -1.000000 12.000000 c
1.000000 12.000000 l
1.000000 16.947716 5.052285 21.000000 10.000000 21.000000 c
10.000000 23.000000 l
h
-1.000000 12.000000 m
-1.000000 5.947716 3.947715 1.000000 10.000000 1.000000 c
10.000000 3.000000 l
5.052285 3.000000 1.000000 7.052285 1.000000 12.000000 c
-1.000000 12.000000 l
h
10.000000 1.000000 m
16.052284 1.000000 21.000000 5.947716 21.000000 12.000000 c
19.000000 12.000000 l
19.000000 7.052285 14.947715 3.000000 10.000000 3.000000 c
10.000000 1.000000 l
h
21.000000 12.000000 m
21.000000 18.052284 16.052284 23.000000 10.000000 23.000000 c
10.000000 21.000000 l
14.947715 21.000000 19.000000 16.947716 19.000000 12.000000 c
21.000000 12.000000 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 5.545532 4.655060 cm
0.049479 0.742188 0.545395 scn
0.717378 6.153159 m
0.332610 6.549356 -0.300487 6.558620 -0.696684 6.173852 c
-1.092881 5.789084 -1.102146 5.155987 -0.717378 4.759790 c
0.717378 6.153159 l
h
3.257576 2.102139 m
2.540198 1.405455 l
2.728505 1.211555 2.987285 1.102139 3.257576 1.102139 c
3.527867 1.102139 3.786646 1.211555 3.974954 1.405455 c
3.257576 2.102139 l
h
11.626469 9.284243 m
12.011237 9.680439 12.001972 10.313537 11.605776 10.698304 c
11.209579 11.083073 10.576482 11.073808 10.191713 10.677610 c
11.626469 9.284243 l
h
-0.717378 4.759790 m
2.540198 1.405455 l
3.974954 2.798823 l
0.717378 6.153159 l
-0.717378 4.759790 l
h
3.974954 1.405455 m
11.626469 9.284243 l
10.191713 10.677610 l
2.540198 2.798823 l
3.974954 1.405455 l
h
f
n
Q
endstream
endobj
3 0 obj
1679
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 22.000000 22.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001769 00000 n
0000001792 00000 n
0000001965 00000 n
0000002039 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2098
%%EOF
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "AnalyticsTick.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,641 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << /ExtGState << /E2 << /ca 0.400000 >>
/E1 << /ca 0.400000 >>
>> >>
/BBox [ 0.000000 0.000000 119.000000 93.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 14.875000 -0.000015 cm
0.049479 0.742188 0.545395 scn
0.000000 44.521278 m
0.000000 69.109695 20.036579 89.042557 44.625000 89.042557 c
44.625000 89.042557 l
69.213417 89.042557 89.250000 69.109695 89.250000 44.521278 c
89.250000 44.521278 l
89.250000 19.932861 69.213417 0.000000 44.625000 0.000000 c
44.625000 0.000000 l
20.036579 0.000000 0.000000 19.932861 0.000000 44.521278 c
0.000000 44.521278 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 52.227402 46.594299 cm
1.000000 1.000000 1.000000 scn
0.000000 20.730219 m
0.000000 22.447567 1.395428 23.839752 3.116776 23.839752 c
14.592426 23.839752 23.895279 14.558517 23.895279 3.109533 c
23.895279 1.392185 22.499851 0.000000 20.778503 0.000000 c
19.057156 0.000000 17.661728 1.392185 17.661728 3.109533 c
17.661728 11.123821 11.149731 17.620686 3.116776 17.620686 c
1.395428 17.620686 0.000000 19.012871 0.000000 20.730219 c
h
f*
n
Q
q
-1.000000 0.000000 -0.000000 -1.000000 66.772202 42.448242 cm
1.000000 1.000000 1.000000 scn
0.000000 20.730206 m
0.000000 22.447552 1.395429 23.839737 3.116779 23.839737 c
14.592443 23.839737 23.895306 14.558502 23.895306 3.109520 c
23.895306 1.392172 22.499878 -0.000011 20.778526 -0.000011 c
19.057177 -0.000011 17.661749 1.392172 17.661749 3.109520 c
17.661749 11.123808 11.149744 17.620672 3.116779 17.620672 c
1.395429 17.620672 0.000000 19.012857 0.000000 20.730206 c
h
f*
n
Q
q
-0.000000 1.000000 -1.000000 -0.000000 57.366512 37.265598 cm
1.000000 1.000000 1.000000 scn
0.000000 20.722975 m
0.000000 22.444323 1.392186 23.839752 3.109534 23.839752 c
14.558520 23.839752 23.839758 14.536892 23.839758 3.061234 c
23.839758 1.339886 22.447573 -0.055544 20.730225 -0.055544 c
19.012875 -0.055544 17.620689 1.339886 17.620689 3.061234 c
17.620689 11.094195 11.123824 17.606197 3.109534 17.606197 c
1.392186 17.606197 0.000000 19.001625 0.000000 20.722975 c
h
f*
n
Q
q
-0.000000 -1.000000 1.000000 -0.000000 61.633194 51.777008 cm
1.000000 1.000000 1.000000 scn
0.000000 20.722975 m
0.000000 22.444324 1.392186 23.839752 3.109534 23.839752 c
14.558520 23.839752 23.839758 14.536893 23.839758 3.061237 c
23.839758 1.339888 22.447573 -0.055540 20.730225 -0.055540 c
19.012875 -0.055540 17.620689 1.339888 17.620689 3.061237 c
17.620689 11.094196 11.123824 17.606197 3.109534 17.606197 c
1.392186 17.606197 0.000000 19.001627 0.000000 20.722975 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 28.123089 16.695465 cm
1.000000 1.000000 1.000000 scn
10.448288 0.000008 m
11.057861 0.000008 11.560491 0.448122 11.646045 1.077618 c
12.576446 7.617959 13.464069 8.514189 19.795069 9.229038 c
20.436726 9.303724 20.917965 9.826525 20.917965 10.434681 c
20.917965 11.053506 20.447418 11.554968 19.805763 11.640323 c
13.506846 12.461866 12.694082 13.262072 11.646045 19.802414 c
11.539103 20.431910 11.057861 20.869354 10.448288 20.869354 c
9.849410 20.869354 9.346780 20.431910 9.250531 19.791744 c
8.330826 13.251402 7.443202 12.355172 1.112203 11.640323 c
0.470547 11.565637 0.000000 11.053506 0.000000 10.434681 c
0.000000 9.826525 0.459853 9.314394 1.112203 9.229038 c
7.411120 8.354148 8.191800 7.607290 9.250531 1.066948 c
9.368169 0.437452 9.860105 0.000008 10.448288 0.000008 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 28.123089 15.195465 cm
0.049479 0.742188 0.545395 scn
11.646045 2.577618 m
10.903506 2.683247 l
10.902877 2.678619 l
11.646045 2.577618 l
h
19.795069 10.729038 m
19.879219 9.983770 l
19.881781 9.984068 l
19.795069 10.729038 l
h
19.805763 13.140323 m
19.904661 13.883777 l
19.902761 13.884024 l
19.805763 13.140323 l
h
11.646045 21.302414 m
12.386630 21.421087 l
12.385450 21.428030 l
11.646045 21.302414 l
h
9.250531 21.291744 m
8.508834 21.403259 l
8.507838 21.396183 l
9.250531 21.291744 l
h
1.112203 13.140323 m
1.028052 13.885592 l
1.025491 13.885293 l
1.112203 13.140323 l
h
1.112203 10.729038 m
1.215387 11.471931 l
1.209505 11.472700 l
1.112203 10.729038 l
h
9.250531 2.566948 m
8.510169 2.447100 l
8.511623 2.438120 l
8.513294 2.429176 l
9.250531 2.566948 l
h
10.448288 0.750008 m
11.450765 0.750008 12.255557 1.493191 12.389213 2.476614 c
10.902877 2.678619 l
10.865425 2.403053 10.664957 2.250008 10.448288 2.250008 c
10.448288 0.750008 l
h
12.388570 2.471989 m
12.620602 4.103085 12.844038 5.334888 13.138243 6.288948 c
13.429995 7.235054 13.776924 7.858273 14.225985 8.307880 c
15.140809 9.223818 16.666159 9.620979 19.879219 9.983774 c
19.710920 11.474303 l
16.592981 11.122249 14.509018 10.713870 13.164680 9.367895 c
12.484159 8.686545 12.039045 7.814714 11.704848 6.730967 c
11.373105 5.655174 11.136688 4.322321 10.903521 2.683245 c
12.388570 2.471989 l
h
19.881781 9.984068 m
20.873766 10.099530 21.667965 10.918526 21.667965 11.934681 c
20.167965 11.934681 l
20.167965 11.734525 19.999683 11.507918 19.708359 11.474010 c
19.881781 9.984068 l
h
21.667965 11.934681 m
21.667965 12.966555 20.881077 13.753887 19.904659 13.883774 c
19.706867 12.396872 l
20.013762 12.356048 20.167965 12.140457 20.167965 11.934681 c
21.667965 11.934681 l
h
19.902761 13.884024 m
18.330524 14.089085 17.151228 14.287123 16.235826 14.561028 c
15.331247 14.831694 14.731587 15.163220 14.286853 15.607450 c
13.840208 16.053589 13.492528 16.670565 13.191763 17.611713 c
12.888441 18.560867 12.648228 19.788363 12.386598 21.421082 c
10.905493 21.183746 l
11.167881 19.546295 11.422276 18.221136 11.762950 17.155106 c
12.106180 16.081072 12.551881 15.220337 13.226795 14.546188 c
13.903622 13.870131 14.753702 13.438795 15.805837 13.123979 c
16.847151 12.812399 18.131544 12.602333 19.708765 12.396622 c
19.902761 13.884024 l
h
12.385450 21.428030 m
12.223795 22.379583 11.461336 23.119354 10.448288 23.119354 c
10.448288 21.619354 l
10.654386 21.619354 10.854410 21.484234 10.906639 21.176800 c
12.385450 21.428030 l
h
10.448288 23.119354 m
9.455997 23.119354 8.656921 22.387987 8.508867 21.403254 c
9.992196 21.180237 l
10.036638 21.475830 10.242823 21.619354 10.448288 21.619354 c
10.448288 23.119354 l
h
8.507838 21.396183 m
8.278477 19.765110 8.056973 18.533388 7.764218 17.579443 c
7.473907 16.633463 7.127907 16.010515 6.679636 15.561167 c
5.766263 14.645599 4.241331 14.248406 1.028053 13.885587 c
1.196352 12.395059 l
4.314074 12.747088 6.398453 13.155437 7.741569 14.501781 c
8.421542 15.183390 8.865580 16.055490 9.198210 17.139366 c
9.528394 18.215273 9.762733 19.548208 9.993224 21.187307 c
8.507838 21.396183 l
h
1.025491 13.885293 m
0.028613 13.769261 -0.750000 12.956783 -0.750000 11.934681 c
0.750000 11.934681 l
0.750000 12.150229 0.912482 12.362013 1.198914 12.395352 c
1.025491 13.885293 l
h
-0.750000 11.934681 m
-0.750000 10.922595 0.016880 10.115961 1.014900 9.985377 c
1.209505 11.472700 l
0.902826 11.512827 0.750000 11.730454 0.750000 11.934681 c
-0.750000 11.934681 l
h
1.009022 9.986170 m
2.582546 9.767614 3.760892 9.563175 4.675031 9.286510 c
5.578484 9.013078 6.174878 8.682938 6.616406 8.241908 c
7.059544 7.799271 7.404263 7.187467 7.703608 6.250257 c
8.005574 5.304842 8.245770 4.080437 8.510169 2.447100 c
9.990894 2.686794 l
9.725928 4.323629 9.471515 5.645210 9.132493 6.706642 c
8.790851 7.776280 8.348207 8.632186 7.676467 9.303166 c
7.003119 9.975756 6.157125 10.405144 5.109545 10.722197 c
4.072651 11.036015 2.791318 11.253017 1.215384 11.471908 c
1.009022 9.986170 l
h
8.513294 2.429176 m
8.688872 1.489628 9.455215 0.750008 10.448288 0.750008 c
10.448288 2.250008 l
10.264993 2.250008 10.047464 2.385277 9.987769 2.704720 c
8.513294 2.429176 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 72.573807 58.608093 cm
1.000000 1.000000 1.000000 scn
8.619835 -0.000011 m
9.122732 -0.000011 9.537402 0.373419 9.607985 0.897997 c
10.375565 6.348283 11.107853 7.095141 16.330927 7.690849 c
16.860292 7.753087 17.257317 8.188755 17.257317 8.695551 c
17.257317 9.211239 16.869114 9.629124 16.339748 9.700253 c
11.143145 10.384872 10.472614 11.051710 9.607985 16.501997 c
9.519756 17.026575 9.122732 17.391113 8.619835 17.391113 c
8.125761 17.391113 7.711091 17.026575 7.631686 16.493105 c
6.872929 11.042820 6.140640 10.295961 0.917567 9.700253 c
0.388201 9.638015 0.000000 9.211239 0.000000 8.695551 c
0.000000 8.188755 0.379379 7.761978 0.917567 7.690849 c
6.114172 6.961774 6.758233 6.339392 7.631686 0.889107 c
7.728737 0.364527 8.134583 -0.000011 8.619835 -0.000011 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 72.573807 57.608093 cm
0.049479 0.742188 0.545395 scn
9.607985 1.897997 m
9.112861 1.967726 l
9.112450 1.964672 l
9.607985 1.897997 l
h
16.330927 8.690849 m
16.387587 8.194067 l
16.389311 8.194269 l
16.330927 8.690849 l
h
16.339748 10.700253 m
16.406334 11.195801 l
16.405056 11.195970 l
16.339748 10.700253 l
h
9.607985 17.501997 m
10.101830 17.580339 l
10.101059 17.584925 l
9.607985 17.501997 l
h
7.631686 17.493105 m
7.137113 17.566721 l
7.136462 17.562048 l
7.631686 17.493105 l
h
0.917567 10.700253 m
0.860907 11.197035 l
0.859183 11.196833 l
0.917567 10.700253 l
h
0.917567 8.690849 m
0.987038 9.186015 l
0.983079 9.186539 l
0.917567 8.690849 l
h
7.631686 1.889107 m
7.137843 1.809963 l
7.140029 1.798147 l
7.631686 1.889107 l
h
8.619835 0.499989 m
9.388696 0.499989 10.001672 1.074373 10.103518 1.831324 c
9.112450 1.964672 l
9.073133 1.672462 8.856770 1.499989 8.619835 1.499989 c
8.619835 0.499989 l
h
10.103099 1.828268 m
10.294624 3.188222 10.480069 4.223436 10.725984 5.028956 c
10.970345 5.829387 11.264832 6.369568 11.654185 6.763333 c
12.442908 7.560994 13.744701 7.892640 16.387587 8.194070 c
16.274267 9.187629 l
13.694078 8.893350 12.018190 8.553713 10.943106 7.466446 c
10.400556 6.917747 10.041607 6.212054 9.769561 5.320940 c
9.499068 4.434915 9.305134 3.332915 9.112870 1.967726 c
10.103099 1.828268 l
h
16.389311 8.194269 m
17.155991 8.284410 17.757317 8.920855 17.757317 9.695551 c
16.757317 9.695551 l
16.757317 9.456655 16.564592 9.221766 16.272543 9.187428 c
16.389311 8.194269 l
h
17.757317 9.695551 m
17.757317 10.482604 17.162529 11.094193 16.406334 11.195800 c
16.273165 10.204706 l
16.575699 10.164056 16.757317 9.939874 16.757317 9.695551 c
17.757317 9.695551 l
h
16.405056 11.195970 m
15.107601 11.366901 14.126670 11.532858 13.361839 11.764020 c
12.604449 11.992933 12.090017 12.277006 11.704502 12.665974 c
11.317406 13.056538 11.022324 13.591100 10.770527 14.386979 c
10.517109 15.187984 10.317721 16.219311 10.101810 17.580338 c
9.114160 17.423656 l
9.330563 16.059540 9.539227 14.963654 9.817105 14.085340 c
10.096603 13.201900 10.456060 12.505035 10.994250 11.962027 c
11.534022 11.417421 12.215625 11.065775 13.072525 10.806786 c
13.921983 10.550046 14.973595 10.375915 16.274443 10.204536 c
16.405056 11.195970 l
h
10.101059 17.584925 m
9.977288 18.320837 9.395552 18.891113 8.619835 18.891113 c
8.619835 17.891113 l
8.849913 17.891113 9.062225 17.732313 9.114909 17.419067 c
10.101059 17.584925 l
h
8.619835 18.891113 m
7.859531 18.891113 7.250215 18.326427 7.137135 17.566717 c
8.126238 17.419493 l
8.171968 17.726725 8.391990 17.891113 8.619835 17.891113 c
8.619835 18.891113 l
h
7.136462 17.562048 m
6.947139 16.202108 6.763302 15.166948 6.518592 14.361504 c
6.275430 13.561155 5.981721 13.021152 5.592998 12.627558 c
4.805452 11.830145 3.503936 11.498478 0.860908 11.197033 c
0.974226 10.203474 l
3.554271 10.497736 5.230436 10.837353 6.304493 11.924868 c
6.846570 12.473737 7.204643 13.179608 7.475406 14.070805 c
7.744622 14.956905 7.936855 16.058958 8.126910 17.424164 c
7.136462 17.562048 l
h
0.859183 11.196833 m
0.089259 11.106312 -0.500000 10.475979 -0.500000 9.695551 c
0.500000 9.695551 l
0.500000 9.946500 0.687143 10.169718 0.975950 10.203673 c
0.859183 11.196833 l
h
-0.500000 9.695551 m
-0.500000 8.923503 0.079786 8.297226 0.852054 8.195160 c
0.983079 9.186539 l
0.678971 9.226731 0.500000 9.454006 0.500000 9.695551 c
-0.500000 9.695551 l
h
0.848098 8.195699 m
2.146421 8.013546 3.126409 7.842214 3.889960 7.608789 c
4.646159 7.377612 5.157835 7.094736 5.540681 6.708459 c
5.924912 6.320785 6.217544 5.790504 6.468155 4.997945 c
6.720429 4.200129 6.919805 3.171420 7.137986 1.809986 c
8.125387 1.968225 l
7.906841 3.331934 7.698164 4.424881 7.421624 5.299438 c
7.143422 6.179253 6.786478 6.872063 6.250936 7.412404 c
5.714010 7.954142 5.035716 8.304206 4.182313 8.565100 c
3.336262 8.823746 2.287016 9.003614 0.987036 9.186000 c
0.848098 8.195699 l
h
7.140029 1.798147 m
7.274719 1.070122 7.860904 0.499989 8.619835 0.499989 c
8.619835 1.499989 l
8.408263 1.499989 8.182755 1.658932 8.123343 1.980066 c
7.140029 1.798147 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 107.227104 75.129669 cm
0.049479 0.742188 0.545395 scn
5.619252 -0.000010 m
5.947090 -0.000010 6.217412 0.238985 6.263425 0.574716 c
6.763808 4.062898 7.241186 4.540888 10.646096 4.922141 c
10.991189 4.961974 11.250008 5.240800 11.250008 5.565150 c
11.250008 5.895190 10.996941 6.162637 10.651848 6.208159 c
7.264192 6.646316 6.827075 7.073092 6.263425 10.561275 c
6.205909 10.897006 5.947090 11.130310 5.619252 11.130310 c
5.297166 11.130310 5.026845 10.897006 4.975080 10.555585 c
4.480448 7.067402 4.003070 6.589413 0.598160 6.208159 c
0.253068 6.168327 0.000000 5.895190 0.000000 5.565150 c
0.000000 5.240800 0.247316 4.967664 0.598160 4.922141 c
3.985816 4.455533 4.405678 4.057208 4.975080 0.569025 c
5.038347 0.233294 5.302918 -0.000010 5.619252 -0.000010 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 103.008408 84.868683 cm
0.049479 0.742188 0.545395 scn
3.160831 -0.000001 m
3.345240 -0.000001 3.497297 0.134433 3.523178 0.323282 c
3.804644 2.285384 4.073170 2.554254 5.988433 2.768708 c
6.182547 2.791114 6.328133 2.947954 6.328133 3.130401 c
6.328133 3.316049 6.185782 3.466487 5.991668 3.492094 c
4.086111 3.738557 3.840232 3.978619 3.523178 5.940721 c
3.490826 6.129570 3.345240 6.260803 3.160831 6.260803 c
2.979658 6.260803 2.827601 6.129570 2.798484 5.937521 c
2.520254 3.975418 2.251728 3.706549 0.336465 3.492094 c
0.142351 3.469688 0.000000 3.316049 0.000000 3.130401 c
0.000000 2.947954 0.139115 2.794315 0.336465 2.768708 c
2.242023 2.506241 2.478195 2.282184 2.798484 0.320081 c
2.834072 0.131233 2.982893 -0.000001 3.160831 -0.000001 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 103.711479 69.564484 cm
0.049479 0.742188 0.545395 scn
3.863233 0.000004 m
4.088622 0.000004 4.274468 0.164312 4.306101 0.395127 c
4.650115 2.793253 4.978312 3.121871 7.319186 3.383983 c
7.556437 3.411368 7.734375 3.603061 7.734375 3.826052 c
7.734375 4.052955 7.560391 4.236824 7.323141 4.268121 c
4.994129 4.569354 4.693611 4.862762 4.306101 7.260888 c
4.266560 7.491703 4.088622 7.652100 3.863233 7.652100 c
3.641799 7.652100 3.455953 7.491703 3.420365 7.256976 c
3.080306 4.858850 2.752109 4.530232 0.411235 4.268121 c
0.173984 4.240736 0.000000 4.052955 0.000000 3.826052 c
0.000000 3.603061 0.170030 3.415280 0.411235 3.383983 c
2.740246 3.063190 3.028902 2.789341 3.420365 0.391215 c
3.463861 0.160400 3.645753 0.000004 3.863233 0.000004 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 -0.070938 78.607880 cm
0.049479 0.742188 0.545395 scn
5.268045 -0.000012 m
5.575393 -0.000012 5.828820 0.224045 5.871957 0.538792 c
6.341066 3.808963 6.788608 4.257079 9.980709 4.614503 c
10.304233 4.651846 10.546875 4.913247 10.546875 5.217325 c
10.546875 5.526737 10.309625 5.777468 9.986101 5.820146 c
6.810176 6.230918 6.400379 6.631021 5.871957 9.901192 c
5.818036 10.215940 5.575393 10.434662 5.268045 10.434662 c
4.966090 10.434662 4.712663 10.215940 4.664135 9.895857 c
4.200418 6.625686 3.752876 6.177571 0.560775 5.820146 c
0.237251 5.782803 0.000000 5.526737 0.000000 5.217325 c
0.000000 4.913247 0.231859 4.657181 0.560775 4.614503 c
3.736700 4.177058 4.130321 3.803629 4.664135 0.533457 c
4.723447 0.218710 4.971482 -0.000012 5.268045 -0.000012 c
h
f
n
Q
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 9.772858 88.346924 cm
0.049479 0.742188 0.545395 scn
2.458421 -0.000008 m
2.601850 -0.000008 2.720115 0.104552 2.740246 0.251434 c
2.959164 1.777514 3.168016 1.986634 4.657663 2.153433 c
4.808641 2.170859 4.921874 2.292846 4.921874 2.434749 c
4.921874 2.579142 4.811157 2.696150 4.660179 2.716066 c
3.178081 2.907759 2.986843 3.094474 2.740246 4.620554 c
2.715083 4.767436 2.601850 4.869507 2.458421 4.869507 c
2.317508 4.869507 2.199242 4.767436 2.176596 4.618064 c
1.960194 3.091985 1.751342 2.882864 0.261695 2.716066 c
0.110717 2.698639 0.000000 2.579142 0.000000 2.434749 c
0.000000 2.292846 0.108201 2.173349 0.261695 2.153433 c
1.743793 1.949291 1.927482 1.775024 2.176596 0.248944 c
2.204275 0.102062 2.320024 -0.000008 2.458421 -0.000008 c
h
f
n
Q
q
/E2 gs
1.000000 0.000000 -0.000000 1.000000 13.288479 82.086121 cm
0.049479 0.742188 0.545395 scn
2.458421 -0.000008 m
2.601850 -0.000008 2.720116 0.104552 2.740247 0.251434 c
2.959164 1.777514 3.168017 1.986634 4.657664 2.153433 c
4.808642 2.170859 4.921875 2.292846 4.921875 2.434749 c
4.921875 2.579142 4.811158 2.696150 4.660181 2.716066 c
3.178082 2.907759 2.986844 3.094474 2.740247 4.620554 c
2.715084 4.767436 2.601850 4.869507 2.458421 4.869507 c
2.317509 4.869507 2.199243 4.767436 2.176596 4.618064 c
1.960195 3.091985 1.751342 2.882864 0.261695 2.716066 c
0.110717 2.698639 0.000000 2.579142 0.000000 2.434749 c
0.000000 2.292846 0.108201 2.173349 0.261695 2.153433 c
1.743793 1.949291 1.927483 1.775024 2.176596 0.248944 c
2.204276 0.102062 2.320025 -0.000008 2.458421 -0.000008 c
h
f
n
Q
endstream
endobj
2 0 obj
17245
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 119.000000 93.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 93.000000 m
119.000000 93.000000 l
119.000000 0.000000 l
0.000000 0.000000 l
0.000000 93.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
234
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 119.000000 93.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000017630 00000 n
0000017654 00000 n
0000018137 00000 n
0000018159 00000 n
0000018457 00000 n
0000018559 00000 n
0000018580 00000 n
0000018754 00000 n
0000018828 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
18888
%%EOF
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "AnalyticsLogo.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+1
View File
@@ -286,6 +286,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private func presentSettingsScreen() {
let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter,
userSession: userSession,
bugReportService: bugReportService)
let coordinator = SettingsCoordinator(parameters: parameters)
coordinator.callback = { [weak self] action in
+2
View File
@@ -23,6 +23,8 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
internal enum Asset {
internal enum Images {
internal static let analyticsCheckmark = ImageAsset(name: "Images/AnalyticsCheckmark")
internal static let analyticsLogo = ImageAsset(name: "Images/AnalyticsLogo")
internal static let serverSelectionIcon = ImageAsset(name: "Images/Server Selection Icon")
internal static let splashScreenPage1 = ImageAsset(name: "Images/Splash Screen Page 1")
internal static let splashScreenPage2 = ImageAsset(name: "Images/Splash Screen Page 2")
@@ -0,0 +1,73 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct AnalyticsPromptCoordinatorParameters {
/// The user session to use if analytics are enabled.
let userSession: UserSessionProtocol
}
final class AnalyticsPromptCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: AnalyticsPromptCoordinatorParameters
private let analyticsPromptHostingController: UIViewController
private var analyticsPromptViewModel: AnalyticsPromptViewModel
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: (@MainActor () -> Void)?
// MARK: - Setup
init(parameters: AnalyticsPromptCoordinatorParameters) {
self.parameters = parameters
let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
let view = AnalyticsPrompt(context: viewModel.context)
analyticsPromptViewModel = viewModel
analyticsPromptHostingController = UIHostingController(rootView: view)
}
// MARK: - Public
func start() {
MXLog.debug("Did start.")
analyticsPromptViewModel.callback = { [weak self] result in
MXLog.debug("AnalyticsPromptViewModel did complete with result: \(result).")
guard let self = self else { return }
switch result {
case .enable:
Analytics.shared.optIn(with: self.parameters.userSession)
self.callback?()
case .disable:
Analytics.shared.optOut()
self.callback?()
}
}
}
func toPresentable() -> UIViewController { analyticsPromptHostingController }
}
@@ -0,0 +1,63 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum AnalyticsPromptViewAction {
/// Enable analytics.
case enable
/// Disable analytics.
case disable
}
enum AnalyticsPromptViewModelAction {
/// Enable analytics.
case enable
/// Disable analytics.
case disable
}
struct AnalyticsPromptViewState: BindableState {
/// Attributed strings created from localized HTML.
let strings = AnalyticsPromptStrings()
}
/// A collection of strings for the UI that need to be parsed from HTML
struct AnalyticsPromptStrings {
let optInContent: AttributedString
let point1 = AttributedStringBuilder().fromHTML(ElementL10n.analyticsOptInListItem1) ?? AttributedString(ElementL10n.analyticsOptInListItem1)
let point2 = AttributedStringBuilder().fromHTML(ElementL10n.analyticsOptInListItem2) ?? AttributedString(ElementL10n.analyticsOptInListItem2)
init() {
// Create the opt in content with a placeholder.
let linkPlaceholder = "{link}"
var optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleName, linkPlaceholder))
guard let range = optInContent.range(of: linkPlaceholder) else {
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleName,
ElementL10n.analyticsOptInContentLink))
MXLog.failure("Failed to add a link attribute to the opt in content.")
return
}
// Replace the placeholder with a link.
var link = AttributedString(ElementL10n.analyticsOptInContentLink)
link.link = BuildSettings.analyticsConfiguration.termsURL
optInContent.replaceSubrange(range, with: link)
self.optInContent = optInContent
}
}
@@ -0,0 +1,51 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState, AnalyticsPromptViewAction>
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let termsURL: URL
// MARK: Public
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)?
// MARK: - Setup
/// Initialize a view model with the specified prompt type and app display name.
init(termsURL: URL) {
self.termsURL = termsURL
super.init(initialViewState: AnalyticsPromptViewState())
}
// MARK: - Public
override func process(viewAction: AnalyticsPromptViewAction) async {
switch viewAction {
case .enable:
callback?(.enable)
case .disable:
callback?(.disable)
}
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@MainActor
protocol AnalyticsPromptViewModelProtocol {
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)? { get set }
var context: AnalyticsPromptViewModelType.Context { get }
}
@@ -0,0 +1,130 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
/// A prompt that asks the user whether they would like to enable Analytics or not.
struct AnalyticsPrompt: View {
// MARK: - Properties
// MARK: Private
private let horizontalPadding: CGFloat = 16
// MARK: Public
@ObservedObject var context: AnalyticsPromptViewModel.Context
// MARK: Views
var body: some View {
GeometryReader { geometry in
ScrollView {
Spacer()
.frame(height: UIConstants.spacerHeight(in: geometry))
mainContent
.readableFrame()
.padding(.horizontal, horizontalPadding)
.padding(.top, UIConstants.onboardingBreakerScreenTopPadding)
.padding(.bottom, 8)
}
.safeAreaInset(edge: .bottom) {
VStack {
buttons
.readableFrame()
.padding(.horizontal, horizontalPadding)
.padding(.bottom, UIConstants.actionButtonBottomPadding)
Spacer()
.frame(height: UIConstants.spacerHeight(in: geometry))
}
.padding(.top, 8)
.background(Color.element.background.ignoresSafeArea())
}
.background(Color.element.background.ignoresSafeArea())
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
/// The main content of the screen that is shown inside the scroll view.
private var mainContent: some View {
VStack {
Image(uiImage: Asset.Images.analyticsLogo.image)
.padding(.bottom, 25)
Text(ElementL10n.analyticsOptInTitle(ElementInfoPlist.cfBundleName))
.font(.element.title2B)
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)
.padding(.bottom, 2)
Text(context.viewState.strings.optInContent)
.font(.element.body)
.multilineTextAlignment(.center)
.foregroundColor(.element.secondaryContent)
Divider()
.background(Color.element.quinaryContent)
.padding(.vertical, 28)
checkmarkList
}
}
/// The list of re-assurances about analytics.
private var checkmarkList: some View {
VStack(alignment: .leading) {
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
AnalyticsPromptCheckmarkItem(string: ElementL10n.analyticsOptInListItem3)
}
.fixedSize(horizontal: false, vertical: true)
.font(.element.body)
.foregroundColor(.element.secondaryContent)
.frame(maxWidth: .infinity)
}
/// The stack of enable/disable buttons.
private var buttons: some View {
VStack {
Button { context.send(viewAction: .enable) } label: {
Text(ElementL10n.actionEnable)
.font(.element.bodySB)
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityIdentifier("enableButton")
Button { context.send(viewAction: .disable) } label: {
Text(ElementL10n.actionNotNow)
.font(.element.bodySB)
.padding(12)
}
.accessibilityIdentifier("disableButton")
}
}
}
// MARK: - Previews
struct AnalyticsPrompt_Previews: PreviewProvider {
static let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
static var previews: some View {
AnalyticsPrompt(context: viewModel.context)
.tint(.element.accent)
}
}
@@ -0,0 +1,60 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct AnalyticsPromptCheckmarkItem: View {
// MARK: - Properties
// MARK: Private
private let attributedString: AttributedString
// MARK: - Setup
init(attributedString: AttributedString) {
self.attributedString = attributedString
}
init(string: String) {
attributedString = AttributedString(string)
}
// MARK: - Views
var body: some View {
Label { Text(attributedString) } icon: {
Image(uiImage: Asset.Images.analyticsCheckmark.image)
.foregroundColor(.element.accent)
}
}
}
// MARK: - Previews
struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider {
static let strings = AnalyticsPromptStrings()
static var previews: some View {
VStack(alignment: .leading) {
AnalyticsPromptCheckmarkItem(attributedString: strings.point1)
AnalyticsPromptCheckmarkItem(attributedString: strings.point2)
AnalyticsPromptCheckmarkItem(attributedString: AttributedString("This is a short string."))
AnalyticsPromptCheckmarkItem(attributedString: AttributedString("This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct."))
}
.padding()
}
}
@@ -51,6 +51,7 @@ class AuthenticationCoordinator: Coordinator, Presentable {
// MARK: - Private
/// Shows the splash screen as the root view in the navigation stack.
private func showSplashScreen() {
let coordinator = SplashScreenCoordinator()
@@ -129,6 +130,23 @@ class AuthenticationCoordinator: Coordinator, Presentable {
}
}
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
let parameters = AnalyticsPromptCoordinatorParameters(userSession: userSession)
let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
coordinator.callback = { [weak self] in
guard let self = self else { return }
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
}
coordinator.start()
add(childCoordinator: coordinator)
navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
/// Show a blocking activity indicator.
private func startLoading() {
activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
@@ -25,6 +25,9 @@ struct UIConstants {
/// The padding used between the footer and the bottom of the view.
static let actionButtonBottomPadding: CGFloat = 24
/// The padding used to the top of the view for breaker screens that don't have a navigation bar.
static let onboardingBreakerScreenTopPadding: CGFloat = 80
/// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`.
static func spacerHeight(in geometry: GeometryProxy) -> CGFloat {
max(0, (geometry.size.height - maxContentHeight) / 2)
@@ -18,6 +18,7 @@ import SwiftUI
struct SettingsCoordinatorParameters {
let navigationRouter: NavigationRouterType
let userSession: UserSessionProtocol
let bugReportService: BugReportServiceProtocol
}
@@ -60,6 +61,8 @@ final class SettingsCoordinator: Coordinator, Presentable {
guard let self = self else { return }
MXLog.debug("SettingsViewModel did complete with result: \(result).")
switch result {
case .toggleAnalytics:
self.toggleAnalytics()
case .reportBug:
self.presentBugReportScreen()
case .crash:
@@ -82,17 +85,12 @@ final class SettingsCoordinator: Coordinator, Presentable {
// MARK: - Private
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
private func toggleAnalytics() {
if ElementSettings.shared.enableAnalytics {
Analytics.shared.optOut()
} else {
Analytics.shared.optIn(with: parameters.userSession)
}
}
private func presentBugReportScreen() {
@@ -114,6 +112,19 @@ final class SettingsCoordinator: Coordinator, Presentable {
self.remove(childCoordinator: coordinator)
}
}
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}
/// Show success indicator
private func showSuccess(label: String) {
@@ -21,6 +21,7 @@ import Foundation
// MARK: View model
enum SettingsViewModelAction {
case toggleAnalytics
case reportBug
case crash
case logout
@@ -32,9 +33,12 @@ struct SettingsViewState: BindableState {
var bindings: SettingsViewStateBindings
}
struct SettingsViewStateBindings { }
struct SettingsViewStateBindings {
var enableAnalytics = ElementSettings.shared.enableAnalytics
}
enum SettingsViewAction {
case toggleAnalytics
case reportBug
case crash
case logout
@@ -40,6 +40,8 @@ class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
override func process(viewAction: SettingsViewAction) async {
switch viewAction {
case .toggleAnalytics:
callback?(.toggleAnalytics)
case .reportBug:
callback?(.reportBug)
case .crash:
@@ -31,43 +31,14 @@ struct SettingsScreen: View {
var body: some View {
Form {
Section {
Button { context.send(viewAction: .reportBug) } label: {
Text(ElementL10n.sendBugReport)
}
.foregroundColor(Color.element.primaryContent)
.accessibilityIdentifier("reportBugButton")
if BuildSettings.settingsCrashButtonVisible {
Button("Crash the app",
role: .destructive) { context.send(viewAction: .crash)
}
.accessibilityIdentifier("crashButton")
}
}
.listRowBackground(rowBackgroundColor)
analyticsSection
.listRowBackground(rowBackgroundColor)
userInterfaceSection
.listRowBackground(rowBackgroundColor)
Section {
Button { showingLogoutConfirmation = true } label: {
Text(ElementL10n.actionSignOut)
}
.frame(maxWidth: .infinity)
.foregroundColor(Color.element.primaryContent)
.accessibilityIdentifier("logoutButton")
.confirmationDialog(ElementL10n.actionSignOutConfirmationSimple,
isPresented: $showingLogoutConfirmation,
titleVisibility: .visible) {
Button(ElementL10n.actionSignOut,
role: .destructive) { context.send(viewAction: .logout)
}
}
} footer: {
versionText
}
.listRowBackground(rowBackgroundColor)
logoutSection
.listRowBackground(rowBackgroundColor)
}
.introspectTableView { tableView in
tableView.backgroundColor = .clear
@@ -87,11 +58,28 @@ struct SettingsScreen: View {
private var rowBackgroundColor: Color {
colorScheme == .light ? .element.background : .element.system
}
private var analyticsSection: some View {
Section(ElementL10n.settingsAnalytics) {
Button { context.send(viewAction: .reportBug) } label: {
Text(ElementL10n.sendBugReport)
}
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("reportBugButton")
if BuildSettings.settingsCrashButtonVisible {
Button("Crash the app",
role: .destructive) { context.send(viewAction: .crash)
}
.accessibilityIdentifier("crashButton")
}
}
}
@ViewBuilder
private var userInterfaceSection: some View {
if BuildSettings.settingsShowTimelineStyle {
Section(header: Text(ElementL10n.settingsUserInterface)) {
Section(ElementL10n.settingsUserInterface) {
Picker(ElementL10n.settingsTimelineStyle, selection: $settings.timelineStyle) {
ForEach(TimelineStyle.allCases, id: \.self) { style in
Text(style.description)
@@ -100,7 +88,26 @@ struct SettingsScreen: View {
}
.accessibilityIdentifier("timelineStylePicker")
}
.listRowBackground(rowBackgroundColor)
}
}
private var logoutSection: some View {
Section {
Button { showingLogoutConfirmation = true } label: {
Text(ElementL10n.actionSignOut)
}
.frame(maxWidth: .infinity)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("logoutButton")
.confirmationDialog(ElementL10n.actionSignOutConfirmationSimple,
isPresented: $showingLogoutConfirmation,
titleVisibility: .visible) {
Button(ElementL10n.actionSignOut,
role: .destructive) { context.send(viewAction: .logout)
}
}
} footer: {
versionText
}
}
}
@@ -56,14 +56,14 @@ class Analytics {
// MARK: - Public
/// Opts in to analytics tracking with the supplied session.
/// - Parameter session: An optional session to use to when reading/generating the analytics ID.
/// Opts in to analytics tracking with the supplied user session.
/// - Parameter userSession: The user session to use to when reading/generating the analytics ID.
/// The session will be ignored if not running.
func optIn(with session: UserSessionProtocol) {
func optIn(with userSession: UserSessionProtocol) {
ElementSettings.shared.enableAnalytics = true
startIfEnabled()
Task { await useAnalyticsSettings(from: session) }
Task { await useAnalyticsSettings(from: userSession) }
}
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
@@ -95,16 +95,16 @@ class Analytics {
// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
}
/// Use the analytics settings from the supplied session to configure analytics.
/// Use the analytics settings from the supplied user session to configure analytics.
/// For now this is only used for (pseudonymous) identification.
/// - Parameter session: The session to read analytics settings from.
func useAnalyticsSettings(from session: UserSessionProtocol) async {
/// - Parameter userSession: The user session to read analytics settings from.
func useAnalyticsSettings(from userSession: UserSessionProtocol) async {
guard
ElementSettings.shared.enableAnalytics,
!ElementSettings.shared.isIdentifiedForAnalytics
else { return }
let service = AnalyticsService(session: session)
let service = AnalyticsService(userSession: userSession)
self.service = service
switch await service.settings() {
@@ -17,7 +17,7 @@
import Foundation
enum AnalyticsServiceError: Error {
/// The session supplied to the service does not have a state of `MXSessionStateRunning`.
/// The user session supplied to the service does not have a state of `MXSessionStateRunning`.
case sessionIsNotRunning
/// The service failed to get or update the analytics settings event from the user's account data.
case accountDataFailure
@@ -25,12 +25,12 @@ enum AnalyticsServiceError: Error {
/// A service responsible for handling the `im.vector.analytics` event from the user's account data.
class AnalyticsService {
let session: UserSessionProtocol
let userSession: UserSessionProtocol
/// Creates an analytics service with the supplied session.
/// - Parameter session: The session to use when reading analytics settings from account data.
init(session: UserSessionProtocol) {
self.session = session
/// Creates an analytics service with the supplied user session.
/// - Parameter userSession: The user session to use when reading analytics settings from account data.
init(userSession: UserSessionProtocol) {
self.userSession = userSession
}
/// The analytics settings for the current user. Calling this method will check whether the settings already
@@ -41,12 +41,12 @@ class AnalyticsService {
func settings() async -> Result<AnalyticsSettings, AnalyticsServiceError> {
// Only use the session if it is running otherwise we could wipe out an existing analytics ID.
fatalWithoutUnreachableCodeWarning()
// guard session.state == .running else {
// guard userSession.state == .running else {
// MXLog.warning("Aborting attempt to read analytics settings. The session may not be up-to-date.")
// return .failure(.sessionIsNotRunning)
// }
let result: Result<AnalyticsSettings?, ClientProxyError> = await session.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
let result: Result<AnalyticsSettings?, ClientProxyError> = await userSession.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
switch result {
case .failure:
return .failure(.accountDataFailure)
@@ -57,7 +57,7 @@ class AnalyticsService {
}
let newSettings = AnalyticsSettings.new(currentEvent: settings)
switch await session.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
switch await userSession.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
case .failure:
MXLog.warning("Failed to update analytics settings.")
return .failure(.accountDataFailure)
@@ -21,6 +21,7 @@ enum UITestScreenIdentifier: String {
case serverSelection
case serverSelectionNonModal
case authenticationFlow
case analyticsPrompt
case simpleRegular
case simpleUpgrade
case settings
@@ -76,6 +76,9 @@ class MockScreen: Identifiable {
case .serverSelectionNonModal:
return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
hasModalPresentation: false))
case .analyticsPrompt:
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
mediaProvider: MockMediaProvider())))
case .authenticationFlow:
return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
navigationRouter: navigationRouter)
@@ -85,6 +88,8 @@ class MockScreen: Identifiable {
return TemplateCoordinator(parameters: .init(promptType: .upgrade))
case .settings:
return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter,
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
mediaProvider: MockMediaProvider()),
bugReportService: MockBugReportService()))
case .bugReport:
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),