diff --git a/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme index af399e45d..81b1b3191 100644 --- a/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme +++ b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme @@ -4,8 +4,7 @@ version = "1.7"> + buildImplicitDependencies = "YES"> - - - - - - - - + + + + - - - - diff --git a/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/Contents.json new file mode 100644 index 000000000..11cb61a99 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "location-pointer-full.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "location-pointer-full-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/location-pointer-full-dark.pdf b/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/location-pointer-full-dark.pdf new file mode 100644 index 000000000..7715aee97 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/location-pointer-full-dark.pdf @@ -0,0 +1,189 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.062745 0.074510 0.090196 scn +0.000000 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 9.000000 9.000000 cm +0.921569 0.933333 0.949020 scn +11.000000 15.000000 m +8.790000 15.000000 7.000000 13.210000 7.000000 11.000000 c +7.000000 8.790000 8.790000 7.000000 11.000000 7.000000 c +13.210000 7.000000 15.000000 8.790000 15.000000 11.000000 c +15.000000 13.210000 13.210000 15.000000 11.000000 15.000000 c +h +19.940001 12.000000 m +19.480001 16.170000 16.170000 19.480000 12.000000 19.940001 c +12.000000 21.000000 l +12.000000 21.549999 11.550000 22.000000 11.000000 22.000000 c +10.450000 22.000000 10.000000 21.549999 10.000000 21.000000 c +10.000000 19.940001 l +5.830000 19.480000 2.520000 16.170000 2.060000 12.000000 c +1.000000 12.000000 l +0.450000 12.000000 0.000000 11.550000 0.000000 11.000000 c +0.000000 10.450000 0.450000 10.000000 1.000000 10.000000 c +2.060000 10.000000 l +2.520000 5.830000 5.830000 2.519999 10.000000 2.059999 c +10.000000 1.000000 l +10.000000 0.450001 10.450000 0.000000 11.000000 0.000000 c +11.550000 0.000000 12.000000 0.450001 12.000000 1.000000 c +12.000000 2.059999 l +16.170000 2.519999 19.480001 5.830000 19.940001 10.000000 c +21.000000 10.000000 l +21.549999 10.000000 22.000000 10.450000 22.000000 11.000000 c +22.000000 11.550000 21.549999 12.000000 21.000000 12.000000 c +19.940001 12.000000 l +19.940001 12.000000 l +h +11.000000 4.000000 m +7.130000 4.000000 4.000000 7.130000 4.000000 11.000000 c +4.000000 14.870000 7.130000 18.000000 11.000000 18.000000 c +14.870000 18.000000 18.000000 14.870000 18.000000 11.000000 c +18.000000 7.130000 14.870000 4.000000 11.000000 4.000000 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 2028 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.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 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 468 +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 40.000000 40.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 +0000002286 00000 n +0000002309 00000 n +0000003025 00000 n +0000003047 00000 n +0000003345 00000 n +0000003447 00000 n +0000003468 00000 n +0000003641 00000 n +0000003715 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +3775 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/location-pointer-full.pdf b/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/location-pointer-full.pdf new file mode 100644 index 000000000..76a7e9159 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/location/location-pointer-full.imageset/location-pointer-full.pdf @@ -0,0 +1,189 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 1.000000 1.000000 scn +0.000000 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 9.000000 9.000000 cm +0.105882 0.113725 0.133333 scn +11.000000 15.000000 m +8.790000 15.000000 7.000000 13.210000 7.000000 11.000000 c +7.000000 8.790000 8.790000 7.000000 11.000000 7.000000 c +13.210000 7.000000 15.000000 8.790000 15.000000 11.000000 c +15.000000 13.210000 13.210000 15.000000 11.000000 15.000000 c +h +19.940001 12.000000 m +19.480001 16.170000 16.170000 19.480000 12.000000 19.940001 c +12.000000 21.000000 l +12.000000 21.549999 11.550000 22.000000 11.000000 22.000000 c +10.450000 22.000000 10.000000 21.549999 10.000000 21.000000 c +10.000000 19.940001 l +5.830000 19.480000 2.520000 16.170000 2.060000 12.000000 c +1.000000 12.000000 l +0.450000 12.000000 0.000000 11.550000 0.000000 11.000000 c +0.000000 10.450000 0.450000 10.000000 1.000000 10.000000 c +2.060000 10.000000 l +2.520000 5.830000 5.830000 2.519999 10.000000 2.059999 c +10.000000 1.000000 l +10.000000 0.450001 10.450000 0.000000 11.000000 0.000000 c +11.550000 0.000000 12.000000 0.450001 12.000000 1.000000 c +12.000000 2.059999 l +16.170000 2.519999 19.480001 5.830000 19.940001 10.000000 c +21.000000 10.000000 l +21.549999 10.000000 22.000000 10.450000 22.000000 11.000000 c +22.000000 11.550000 21.549999 12.000000 21.000000 12.000000 c +19.940001 12.000000 l +19.940001 12.000000 l +h +11.000000 4.000000 m +7.130000 4.000000 4.000000 7.130000 4.000000 11.000000 c +4.000000 14.870000 7.130000 18.000000 11.000000 18.000000 c +14.870000 18.000000 18.000000 14.870000 18.000000 11.000000 c +18.000000 7.130000 14.870000 4.000000 11.000000 4.000000 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 2028 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.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 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 468 +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 40.000000 40.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 +0000002286 00000 n +0000002309 00000 n +0000003025 00000 n +0000003047 00000 n +0000003345 00000 n +0000003447 00000 n +0000003468 00000 n +0000003641 00000 n +0000003715 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +3775 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/Contents.json new file mode 100644 index 000000000..10d9bad0c --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "location-pointer.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "location-pointer-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/location-pointer-dark.pdf b/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/location-pointer-dark.pdf new file mode 100644 index 000000000..6190511ba --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/location-pointer-dark.pdf @@ -0,0 +1,218 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.062745 0.074510 0.090196 scn +0.000000 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 9.049805 9.049805 cm +0.921569 0.933333 0.949020 scn +10.950000 0.000391 m +10.666667 0.000391 10.429167 0.096224 10.237500 0.287889 c +10.045834 0.479557 9.950000 0.717056 9.950000 1.000391 c +9.950000 2.000391 l +7.866667 2.233725 6.079167 3.096224 4.587500 4.587891 c +3.095833 6.079557 2.233333 7.867057 2.000000 9.950391 c +1.000000 9.950391 l +0.716667 9.950391 0.479167 10.046224 0.287500 10.237890 c +0.095833 10.429557 0.000000 10.667057 0.000000 10.950391 c +0.000000 11.233724 0.095833 11.471224 0.287500 11.662890 c +0.479167 11.854557 0.716667 11.950391 1.000000 11.950391 c +2.000000 11.950391 l +2.233333 14.033724 3.095833 15.821224 4.587500 17.312891 c +6.079167 18.804558 7.866667 19.667057 9.950000 19.900391 c +9.950000 20.900391 l +9.950000 21.183723 10.045834 21.421225 10.237500 21.612890 c +10.429167 21.804558 10.666667 21.900391 10.950000 21.900391 c +11.233334 21.900391 11.470834 21.804558 11.662500 21.612890 c +11.854167 21.421225 11.950000 21.183723 11.950000 20.900391 c +11.950000 19.900391 l +14.033334 19.667057 15.820833 18.804558 17.312500 17.312891 c +18.804167 15.821224 19.666666 14.033724 19.900000 11.950391 c +20.900000 11.950391 l +21.183334 11.950391 21.420834 11.854557 21.612501 11.662890 c +21.804167 11.471224 21.900000 11.233724 21.900000 10.950391 c +21.900000 10.667057 21.804167 10.429557 21.612501 10.237890 c +21.420834 10.046224 21.183334 9.950391 20.900000 9.950391 c +19.900000 9.950391 l +19.666666 7.867057 18.804167 6.079557 17.312500 4.587891 c +15.820833 3.096224 14.033334 2.233725 11.950000 2.000391 c +11.950000 1.000391 l +11.950000 0.717056 11.854167 0.479557 11.662500 0.287889 c +11.470834 0.096224 11.233334 0.000391 10.950000 0.000391 c +h +10.950000 3.950390 m +12.883333 3.950390 14.533334 4.633724 15.900001 6.000390 c +17.266666 7.367057 17.950001 9.017057 17.950001 10.950391 c +17.950001 12.883724 17.266666 14.533724 15.900001 15.900391 c +14.533334 17.267057 12.883333 17.950390 10.950000 17.950390 c +9.016666 17.950390 7.366667 17.267057 6.000000 15.900391 c +4.633333 14.533724 3.950000 12.883724 3.950000 10.950391 c +3.950000 9.017057 4.633333 7.367057 6.000000 6.000390 c +7.366667 4.633724 9.016666 3.950390 10.950000 3.950390 c +h +10.950000 6.950391 m +9.850000 6.950391 8.908334 7.342057 8.125000 8.125390 c +7.341667 8.908724 6.950000 9.850390 6.950000 10.950391 c +6.950000 12.050390 7.341667 12.992057 8.125000 13.775391 c +8.908334 14.558723 9.850000 14.950390 10.950000 14.950390 c +12.050000 14.950390 12.991667 14.558723 13.775001 13.775391 c +14.558333 12.992057 14.950000 12.050390 14.950000 10.950391 c +14.950000 9.850390 14.558333 8.908724 13.775001 8.125390 c +12.991667 7.342057 12.050000 6.950391 10.950000 6.950391 c +h +10.950000 8.950391 m +11.500000 8.950391 11.970834 9.146224 12.362500 9.537890 c +12.754167 9.929557 12.950000 10.400391 12.950000 10.950391 c +12.950000 11.500390 12.754167 11.971224 12.362500 12.362890 c +11.970834 12.754558 11.500000 12.950391 10.950000 12.950391 c +10.400001 12.950391 9.929167 12.754558 9.537500 12.362890 c +9.145833 11.971224 8.950000 11.500390 8.950000 10.950391 c +8.950000 10.400391 9.145833 9.929557 9.537500 9.537890 c +9.929167 9.146224 10.400001 8.950391 10.950000 8.950391 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 3685 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.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 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 468 +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 40.000000 40.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 +0000003943 00000 n +0000003966 00000 n +0000004682 00000 n +0000004704 00000 n +0000005002 00000 n +0000005104 00000 n +0000005125 00000 n +0000005298 00000 n +0000005372 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +5432 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/location-pointer.pdf b/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/location-pointer.pdf new file mode 100644 index 000000000..9613d1ff8 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/location/location-pointer.imageset/location-pointer.pdf @@ -0,0 +1,218 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 1.000000 1.000000 scn +0.000000 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 9.049805 9.049927 cm +0.105882 0.113725 0.133333 scn +10.950000 0.000025 m +10.666666 0.000025 10.429167 0.095856 10.237500 0.287523 c +10.045834 0.479191 9.950000 0.716692 9.950000 1.000025 c +9.950000 2.000025 l +7.866666 2.233358 6.079167 3.095858 4.587500 4.587524 c +3.095833 6.079191 2.233333 7.866692 2.000000 9.950025 c +1.000000 9.950025 l +0.716667 9.950025 0.479167 10.045857 0.287500 10.237524 c +0.095833 10.429191 0.000000 10.666691 0.000000 10.950025 c +0.000000 11.233358 0.095833 11.470858 0.287500 11.662524 c +0.479167 11.854191 0.716667 11.950025 1.000000 11.950025 c +2.000000 11.950025 l +2.233333 14.033358 3.095833 15.820858 4.587500 17.312525 c +6.079167 18.804192 7.866666 19.666691 9.950000 19.900024 c +9.950000 20.900024 l +9.950000 21.183357 10.045834 21.420858 10.237500 21.612524 c +10.429167 21.804192 10.666666 21.900024 10.950000 21.900024 c +11.233334 21.900024 11.470834 21.804192 11.662500 21.612524 c +11.854167 21.420858 11.950000 21.183357 11.950000 20.900024 c +11.950000 19.900024 l +14.033333 19.666691 15.820833 18.804192 17.312500 17.312525 c +18.804167 15.820858 19.666666 14.033358 19.900000 11.950025 c +20.900000 11.950025 l +21.183332 11.950025 21.420834 11.854191 21.612501 11.662524 c +21.804169 11.470858 21.900000 11.233358 21.900000 10.950025 c +21.900000 10.666691 21.804169 10.429191 21.612501 10.237524 c +21.420834 10.045857 21.183332 9.950025 20.900000 9.950025 c +19.900000 9.950025 l +19.666666 7.866692 18.804167 6.079191 17.312500 4.587524 c +15.820833 3.095858 14.033333 2.233358 11.950000 2.000025 c +11.950000 1.000025 l +11.950000 0.716692 11.854167 0.479191 11.662500 0.287523 c +11.470834 0.095856 11.233334 0.000025 10.950000 0.000025 c +h +10.950000 3.950024 m +12.883333 3.950024 14.533334 4.633358 15.900001 6.000024 c +17.266666 7.366691 17.950001 9.016691 17.950001 10.950025 c +17.950001 12.883358 17.266666 14.533358 15.900001 15.900024 c +14.533334 17.266691 12.883333 17.950024 10.950000 17.950024 c +9.016666 17.950024 7.366667 17.266691 6.000000 15.900024 c +4.633333 14.533358 3.950000 12.883358 3.950000 10.950025 c +3.950000 9.016691 4.633333 7.366691 6.000000 6.000024 c +7.366667 4.633358 9.016666 3.950024 10.950000 3.950024 c +h +10.950000 6.950025 m +9.849999 6.950025 8.908334 7.341690 8.125000 8.125024 c +7.341667 8.908358 6.950000 9.850024 6.950000 10.950025 c +6.950000 12.050025 7.341667 12.991691 8.125000 13.775024 c +8.908334 14.558357 9.849999 14.950024 10.950000 14.950024 c +12.050000 14.950024 12.991667 14.558357 13.775001 13.775024 c +14.558334 12.991691 14.950000 12.050025 14.950000 10.950025 c +14.950000 9.850024 14.558334 8.908358 13.775001 8.125024 c +12.991667 7.341690 12.050000 6.950025 10.950000 6.950025 c +h +10.950000 8.950025 m +11.500000 8.950025 11.970834 9.145858 12.362500 9.537524 c +12.754167 9.929191 12.950000 10.400024 12.950000 10.950025 c +12.950000 11.500025 12.754167 11.970858 12.362500 12.362524 c +11.970834 12.754190 11.500000 12.950025 10.950000 12.950025 c +10.400000 12.950025 9.929167 12.754190 9.537500 12.362524 c +9.145834 11.970858 8.950000 11.500025 8.950000 10.950025 c +8.950000 10.400024 9.145834 9.929191 9.537500 9.537524 c +9.929167 9.145858 10.400000 8.950025 10.950000 8.950025 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 3685 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.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 34.000000 m +0.000000 37.313709 2.686292 40.000000 6.000000 40.000000 c +34.000000 40.000000 l +37.313709 40.000000 40.000000 37.313709 40.000000 34.000000 c +40.000000 6.000000 l +40.000000 2.686291 37.313709 0.000000 34.000000 0.000000 c +5.999999 0.000000 l +2.686291 0.000000 0.000000 2.686291 0.000000 6.000000 c +0.000000 34.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 468 +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 40.000000 40.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 +0000003943 00000 n +0000003966 00000 n +0000004682 00000 n +0000004704 00000 n +0000005002 00000 n +0000005104 00000 n +0000005125 00000 n +0000005298 00000 n +0000005372 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +5432 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 3a2c19419..c692f2a1a 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -139,10 +139,10 @@ "emoji_picker_category_places" = "Travel & Places"; "emoji_picker_category_symbols" = "Symbols"; "error_failed_creating_the_permalink" = "Failed creating the permalink"; -"error_failed_loading_map" = "Element could not load the map. Please try again later."; +"error_failed_loading_map" = "%1$@ could not load the map. Please try again later."; "error_failed_loading_messages" = "Failed loading messages"; -"error_failed_locating_user" = "Element could not access your location. Please try again later."; -"error_missing_location_auth" = "Element does not have permission to access your location. You can enable access in Settings > Location"; +"error_failed_locating_user" = "%1$@ could not access your location. Please try again later."; +"error_missing_location_auth" = "%1$@ does not have permission to access your location. You can enable access in Settings > Location"; "error_no_compatible_app_found" = "No compatible app was found to handle this action."; "error_some_messages_have_not_been_sent" = "Some messages have not been sent"; "error_unknown" = "Sorry, an error occurred"; @@ -308,6 +308,8 @@ "screen_room_reactions_show_more" = "Show more"; "screen_room_retry_send_menu_send_again_action" = "Send again"; "screen_room_retry_send_menu_title" = "Your message failed to send"; +"screen_room_timeline_add_reaction" = "Add emoji"; +"screen_room_timeline_less_reactions" = "Show less"; "screen_roomlist_a11y_create_message" = "Create a new conversation or room"; "screen_roomlist_main_space_title" = "All Chats"; "screen_server_confirmation_change_server" = "Change account provider"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict index e6cefe155..f2e3f0a51 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict @@ -146,5 +146,19 @@ %1$d people + screen_room_timeline_more_reactions + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + %1$d more + + \ No newline at end of file diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index b1e40e88c..591e6cf71 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -515,7 +515,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { _ = await self.roomProxy?.sendLocation(body: geoURI.bodyMessage, geoURI: geoURI, description: nil, - zoomLevel: nil, + zoomLevel: 15, assetType: isUserLocation ? .sender : .pin) self.navigationSplitCoordinator.setSheetCoordinator(nil) } diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index b802fa44f..6dfa4250e 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -39,6 +39,8 @@ internal enum Asset { internal static let launchLogo = ImageAsset(name: "images/launch-logo") internal static let locationMarker = ImageAsset(name: "images/location-marker") internal static let locationPin = ImageAsset(name: "images/location-pin") + internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full") + internal static let locationPointer = ImageAsset(name: "images/location-pointer") internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message") internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient") } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index b79a15afd..b7ce519be 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -308,14 +308,20 @@ public enum L10n { public static var emojiPickerCategorySymbols: String { return L10n.tr("Localizable", "emoji_picker_category_symbols") } /// Failed creating the permalink public static var errorFailedCreatingThePermalink: String { return L10n.tr("Localizable", "error_failed_creating_the_permalink") } - /// Element could not load the map. Please try again later. - public static var errorFailedLoadingMap: String { return L10n.tr("Localizable", "error_failed_loading_map") } + /// %1$@ could not load the map. Please try again later. + public static func errorFailedLoadingMap(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_failed_loading_map", String(describing: p1)) + } /// Failed loading messages public static var errorFailedLoadingMessages: String { return L10n.tr("Localizable", "error_failed_loading_messages") } - /// Element could not access your location. Please try again later. - public static var errorFailedLocatingUser: String { return L10n.tr("Localizable", "error_failed_locating_user") } - /// Element does not have permission to access your location. You can enable access in Settings > Location - public static var errorMissingLocationAuth: String { return L10n.tr("Localizable", "error_missing_location_auth") } + /// %1$@ could not access your location. Please try again later. + public static func errorFailedLocatingUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_failed_locating_user", String(describing: p1)) + } + /// %1$@ does not have permission to access your location. You can enable access in Settings > Location + public static func errorMissingLocationAuth(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_missing_location_auth", String(describing: p1)) + } /// No compatible app was found to handle this action. public static var errorNoCompatibleAppFound: String { return L10n.tr("Localizable", "error_no_compatible_app_found") } /// Some messages have not been sent @@ -780,6 +786,14 @@ public enum L10n { public static var screenRoomRetrySendMenuSendAgainAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_send_again_action") } /// Your message failed to send public static var screenRoomRetrySendMenuTitle: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_title") } + /// Add emoji + public static var screenRoomTimelineAddReaction: String { return L10n.tr("Localizable", "screen_room_timeline_add_reaction") } + /// Show less + public static var screenRoomTimelineLessReactions: String { return L10n.tr("Localizable", "screen_room_timeline_less_reactions") } + /// Plural format key: "%#@COUNT@" + public static func screenRoomTimelineMoreReactions(_ p1: Int) -> String { + return L10n.tr("Localizable", "screen_room_timeline_more_reactions", p1) + } /// Create a new conversation or room public static var screenRoomlistA11yCreateMessage: String { return L10n.tr("Localizable", "screen_roomlist_a11y_create_message") } /// All Chats diff --git a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift index ee0e2947c..41a039f8b 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift @@ -20,15 +20,20 @@ import SwiftUI struct MapLibreMapView: UIViewRepresentable { struct Options { - /// The initial zoom level + /// the final zoom level used when the first user location emit let zoomLevel: Double + /// The initial zoom level used when the map it firstly loaded and the user location is not yet available, in case of annotations this property is not being used + let initialZoomLevel: Double + /// The initial map center - let mapCenter: CLLocationCoordinate2D? + let mapCenter: CLLocationCoordinate2D + /// Map annotations let annotations: [LocationAnnotation] - init(zoomLevel: Double, mapCenter: CLLocationCoordinate2D? = nil, annotations: [LocationAnnotation] = []) { + init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [LocationAnnotation] = []) { self.zoomLevel = zoomLevel + self.initialZoomLevel = initialZoomLevel self.mapCenter = mapCenter self.annotations = annotations } @@ -43,14 +48,16 @@ struct MapLibreMapView: UIViewRepresentable { let options: Options /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user - var showsUserLocationMode: ShowUserLocationMode = .hide + @Binding var showsUserLocationMode: ShowUserLocationMode /// Bind view errors if any - let error: Binding + @Binding var error: MapLibreError? /// Coordinate of the center of the map @Binding var mapCenterCoordinate: CLLocationCoordinate2D? + @Binding var isLocationAuthorized: Bool? + /// Called when the user pan on the map var userDidPan: (() -> Void)? @@ -59,23 +66,12 @@ struct MapLibreMapView: UIViewRepresentable { func makeUIView(context: Context) -> MGLMapView { let mapView = makeMapView() mapView.delegate = context.coordinator - let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.didPan)) - panGesture.delegate = context.coordinator - mapView.addGestureRecognizer(panGesture) setupMap(mapView: mapView, with: options) return mapView } func updateUIView(_ mapView: MGLMapView, context: Context) { - mapView.removeAllAnnotations() - mapView.addAnnotations(options.annotations) - - if colorScheme == .dark { - mapView.styleURL = builder.dynamicMapURL(for: .dark) - } else { - mapView.styleURL = builder.dynamicMapURL(for: .light) - } - + mapView.styleURL = builder.dynamicMapURL(for: .init(colorScheme)) showUserLocation(in: mapView) } @@ -86,32 +82,36 @@ struct MapLibreMapView: UIViewRepresentable { // MARK: - Private private func setupMap(mapView: MGLMapView, with options: Options) { - mapView.zoomLevel = options.zoomLevel - if let mapCenter = options.mapCenter { - mapView.centerCoordinate = mapCenter - } + mapView.addAnnotations(options.annotations) + mapView.zoomLevel = options.annotations.isEmpty ? options.initialZoomLevel : options.zoomLevel + mapView.centerCoordinate = options.mapCenter } private func makeMapView() -> MGLMapView { let mapView = MGLMapView(frame: .zero, styleURL: colorScheme == .dark ? builder.dynamicMapURL(for: .dark) : builder.dynamicMapURL(for: .light)) - - showUserLocation(in: mapView) - mapView.attributionButton.isHidden = true - + mapView.logoViewPosition = .topLeft + mapView.attributionButtonPosition = .topLeft + mapView.attributionButtonMargins = .init(x: mapView.logoView.frame.maxX + 8, y: mapView.logoView.center.y / 2) return mapView } private func showUserLocation(in mapView: MGLMapView) { - switch showsUserLocationMode { - case .showAndFollow: - mapView.showsUserLocation = true + switch (showsUserLocationMode, options.annotations) { + case (.showAndFollow, _): mapView.userTrackingMode = .follow - case .show: + case (.show, let annotations) where !annotations.isEmpty: + /** in the show mode, if there are annotations, we check the authorizationStatus, + if it's not determined, we wont prompt the user with a request for permissions, + because he should be able to see the annotations without sharing his location informations + **/ + guard mapView.locationManager.authorizationStatus != .notDetermined else { return } + fallthrough + case (.show, _): mapView.showsUserLocation = true - mapView.userTrackingMode = .none - case .hide: + mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil) + case (.hide, _): mapView.showsUserLocation = false - mapView.userTrackingMode = .none + mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil) } } } @@ -119,11 +119,13 @@ struct MapLibreMapView: UIViewRepresentable { // MARK: - Coordinator extension MapLibreMapView { - class Coordinator: NSObject, MGLMapViewDelegate, UIGestureRecognizerDelegate { + class Coordinator: NSObject, MGLMapViewDelegate { // MARK: - Properties var mapLibreView: MapLibreMapView + private var previousUserLocation: MGLUserLocation? + // MARK: - Setup init(_ mapLibreView: MapLibreMapView) { @@ -140,20 +142,30 @@ extension MapLibreMapView { } func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { - mapLibreView.error.wrappedValue = .failedLoadingMap + mapLibreView.error = .failedLoadingMap } - func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { } + func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { + guard let userLocation else { return } + + if previousUserLocation == nil, mapLibreView.options.annotations.isEmpty { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.options.zoomLevel, animated: true) + } + } + + previousUserLocation = userLocation + } func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) { - guard mapView.showsUserLocation else { - return - } - switch manager.authorizationStatus { case .denied, .restricted: - mapLibreView.error.wrappedValue = .invalidLocationAuthorization - default: + mapLibreView.isLocationAuthorized = false + case .authorizedAlways, .authorizedWhenInUse: + mapLibreView.isLocationAuthorized = true + case .notDetermined: + mapLibreView.isLocationAuthorized = nil + @unknown default: break } } @@ -171,15 +183,25 @@ extension MapLibreMapView { false } - // MARK: UIGestureRecognizer - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - gestureRecognizer is UIPanGestureRecognizer - } - - @objc - func didPan() { - mapLibreView.userDidPan?() + func mapView(_ mapView: MGLMapView, shouldChangeFrom oldCamera: MGLMapCamera, to newCamera: MGLMapCamera, reason: MGLCameraChangeReason) -> Bool { + // we send the userDidPan event only for the reasons that actually will change the map center, and not zoom only / rotations only events. + switch reason { + case .gesturePan, + .gesturePinch, + .gestureRotate: + mapLibreView.userDidPan?() + case .gestureOneFingerZoom, + .gestureTilt, + .gestureZoomIn, + .gestureZoomOut, + .programmatic, + .resetNorth, + .transitionCancelled: + break + default: + break + } + return true } } } @@ -194,3 +216,16 @@ private extension MGLMapView { removeAnnotations(annotations) } } + +private extension MapTilerStyle { + init(_ colorScheme: ColorScheme) { + switch colorScheme { + case .light: + self = .light + case .dark: + self = .dark + @unknown default: + fatalError() + } + } +} diff --git a/ElementX/Sources/Other/MapLibre/MapLibreModels.swift b/ElementX/Sources/Other/MapLibre/MapLibreModels.swift index 538af291a..16f76a25a 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreModels.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreModels.swift @@ -36,7 +36,6 @@ enum MapTilerStyle { enum MapLibreError: Error { case failedLoadingMap case failedLocatingUser - case invalidLocationAuthorization } enum MapTilerAttributionPlacement: String { diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift index 609347e5b..35dd1291a 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift @@ -18,12 +18,13 @@ import CoreLocation import Foundation enum LocationSharingViewError: Error, Hashable { - case failedSharingLocation + case missingAuthorization case mapError(MapLibreError) } enum StaticLocationScreenViewModelAction { case close + case openSystemSettings case sendLocation(GeoURI, isUserLocation: Bool) } @@ -33,47 +34,36 @@ enum StaticLocationInteractionMode: Hashable { } struct StaticLocationScreenViewState: BindableState { - init(interactionMode: StaticLocationInteractionMode, isSharingUserLocation: Bool = false, showsUserLocationMode: ShowUserLocationMode = .hide) { + init(interactionMode: StaticLocationInteractionMode) { self.interactionMode = interactionMode - self.isSharingUserLocation = isSharingUserLocation - self.showsUserLocationMode = showsUserLocationMode - switch interactionMode { case .picker: - bindings = .init() - case .viewOnly(let geoURI, _): - bindings = .init(mapCenterLocation: .init(latitude: geoURI.latitude, longitude: geoURI.longitude)) + bindings.showsUserLocationMode = .showAndFollow + case .viewOnly: + bindings.showsUserLocationMode = .show } } let interactionMode: StaticLocationInteractionMode /// Indicates whether the user is sharing his current location - var isSharingUserLocation: Bool - /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user - var showsUserLocationMode: ShowUserLocationMode - - var bindings = StaticLocationScreenBindings() - - var showBottomToolbar: Bool { - interactionMode == .picker + var isSharingUserLocation: Bool { + bindings.isLocationAuthorized == true && bindings.showsUserLocationMode == .showAndFollow } - - var mapAnnotationCoordinate: CLLocationCoordinate2D? { + + var bindings = StaticLocationScreenBindings(showsUserLocationMode: .hide) + + var initialMapCenter: CLLocationCoordinate2D { switch interactionMode { case .picker: - return nil + // middle point in Europe, to be used if the users location is not yet known + return .init(latitude: 49.843, longitude: 9.902056) case .viewOnly(let geoURI, _): return .init(latitude: geoURI.latitude, longitude: geoURI.longitude) } } var isLocationPickerMode: Bool { - switch interactionMode { - case .picker: - return true - case .viewOnly: - return false - } + interactionMode == .picker } var navigationTitle: String { @@ -95,9 +85,13 @@ struct StaticLocationScreenViewState: BindableState { } var zoomLevel: Double { + 15.0 + } + + var initialZoomLevel: Double { switch interactionMode { case .picker: - return 5.0 + return 2.7 case .viewOnly: return 15.0 } @@ -115,6 +109,10 @@ struct StaticLocationScreenViewState: BindableState { struct StaticLocationScreenBindings { var mapCenterLocation: CLLocationCoordinate2D? + + var showsUserLocationMode: ShowUserLocationMode + + var isLocationAuthorized: Bool? /// Information describing the currently displayed alert. var mapError: MapLibreError? { @@ -125,7 +123,7 @@ struct StaticLocationScreenBindings { return nil } set { - alertInfo = newValue.map { AlertInfo(id: .mapError($0)) } + alertInfo = newValue.map { AlertInfo(locationSharingViewError: .mapError($0)) } } } @@ -138,5 +136,33 @@ struct StaticLocationScreenBindings { enum StaticLocationScreenViewAction { case close case selectLocation + case centerToUser case userDidPan } + +extension AlertInfo where T == LocationSharingViewError { + init(locationSharingViewError error: LocationSharingViewError, + primaryButton: AlertButton = AlertButton(title: L10n.actionOk, action: nil), + secondaryButton: AlertButton? = nil) { + switch error { + case .missingAuthorization: + self.init(id: error, + title: "", + message: L10n.errorMissingLocationAuth(InfoPlistReader.main.bundleDisplayName), + primaryButton: primaryButton, + secondaryButton: secondaryButton) + case .mapError(.failedLoadingMap): + self.init(id: error, + title: "", + message: L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName), + primaryButton: primaryButton, + secondaryButton: secondaryButton) + case .mapError(.failedLocatingUser): + self.init(id: error, + title: "", + message: L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName), + primaryButton: primaryButton, + secondaryButton: secondaryButton) + } + } +} diff --git a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift index abb36812f..59b6e4b73 100644 --- a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift @@ -51,6 +51,12 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol { switch action { case .close: actionsSubject.send(.close) + case .openSystemSettings: + guard let url = URL(string: UIApplication.openSettingsURLString), + UIApplication.shared.canOpenURL(url) else { + return + } + UIApplication.shared.open(url) case .sendLocation(let geoURI, let isUserLocation): actionsSubject.send(.selectedLocation(geoURI, isUserLocation: isUserLocation)) } diff --git a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift index f6dab2e5f..bef056726 100644 --- a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift @@ -38,8 +38,17 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo guard let coordinate = state.bindings.mapCenterLocation else { return } actionsSubject.send(.sendLocation(.init(coordinate: coordinate), isUserLocation: state.isSharingUserLocation)) case .userDidPan: - state.showsUserLocationMode = .hide - state.isSharingUserLocation = false + state.bindings.showsUserLocationMode = .show + case .centerToUser: + switch state.bindings.isLocationAuthorized { + case .some(true), .none: + state.bindings.showsUserLocationMode = .showAndFollow + case .some(false): + let action: () -> Void = { [weak self] in self?.actionsSubject.send(.openSystemSettings) } + state.bindings.alertInfo = .init(locationSharingViewError: .missingAuthorization, + primaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.commonSettings, action: action)) + } } } } diff --git a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift index f3321c5d5..2c2d2973e 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift @@ -44,17 +44,20 @@ struct StaticLocationScreen: View { ZStack(alignment: .center) { MapLibreMapView(builder: builder, options: mapOptions, - showsUserLocationMode: .hide, + showsUserLocationMode: $context.showsUserLocationMode, error: $context.mapError, mapCenterCoordinate: $context.mapCenterLocation, - userDidPan: { - context.send(viewAction: .userDidPan) - }) + isLocationAuthorized: $context.isLocationAuthorized) { + context.send(viewAction: .userDidPan) + } + .ignoresSafeArea(.all, edges: mapSafeAreaEdges) if context.viewState.isLocationPickerMode { LocationMarkerView() } } - .ignoresSafeArea(.all, edges: mapSafeAreaEdges) + .overlay(alignment: .bottomTrailing) { + centerToUserLocationButton + } } // MARK: - Private @@ -72,7 +75,7 @@ struct StaticLocationScreen: View { } } - if context.viewState.showBottomToolbar { + if context.viewState.isLocationPickerMode { ToolbarItemGroup(placement: .bottomBar) { selectLocationButton Spacer() @@ -81,19 +84,22 @@ struct StaticLocationScreen: View { } private var mapOptions: MapLibreMapView.Options { - guard let coordinate = context.viewState.mapAnnotationCoordinate else { - return .init(zoomLevel: context.viewState.zoomLevel) + var annotations: [LocationAnnotation] = [] + if context.viewState.isLocationPickerMode == false { + let annotation = LocationAnnotation(coordinate: context.viewState.initialMapCenter, anchorPoint: .bottomCenter) { + LocationMarkerView() + } + annotations.append(annotation) } return .init(zoomLevel: context.viewState.zoomLevel, - mapCenter: coordinate, - annotations: [LocationAnnotation(coordinate: coordinate, anchorPoint: .bottomCenter) { - LocationMarkerView() - }]) + initialZoomLevel: context.viewState.initialZoomLevel, + mapCenter: context.viewState.initialMapCenter, + annotations: annotations) } private var mapSafeAreaEdges: Edge.Set { - context.viewState.showBottomToolbar ? .horizontal : [.horizontal, .bottom] + context.viewState.isLocationPickerMode ? .horizontal : [.horizontal, .bottom] } @ScaledMetric private var shareMarkerSize: CGFloat = 28 @@ -111,6 +117,15 @@ struct StaticLocationScreen: View { } } + private var centerToUserLocationButton: some View { + Button { + context.send(viewAction: .centerToUser) + } label: { + Image(asset: context.viewState.isSharingUserLocation ? Asset.Images.locationPointerFull : Asset.Images.locationPointer) + } + .padding(16) + } + private var closeButton: some View { Button(L10n.actionCancel) { context.send(viewAction: .close) @@ -127,14 +142,13 @@ struct StaticLocationScreen: View { @ViewBuilder private var shareSheet: some View { - if let location = context.viewState.mapAnnotationCoordinate { - let locationDescription = context.viewState.locationDescription - AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, locationDescription: locationDescription)], - applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, locationDescription: locationDescription) }) - .edgesIgnoringSafeArea(.bottom) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.hidden) - } + let location = context.viewState.initialMapCenter + let locationDescription = context.viewState.locationDescription + AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, locationDescription: locationDescription)], + applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, locationDescription: locationDescription) }) + .edgesIgnoringSafeArea(.bottom) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.hidden) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 68e6466e4..c7a937c4f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -238,7 +238,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return .none } } - + + // swiftlint:disable:next cyclomatic_complexity function_body_length private func updateTimelineItems() { var newTimelineItems = [RoomTimelineItemProtocol]() var canBackPaginate = true diff --git a/ElementX/SupportingFiles/Info.plist b/ElementX/SupportingFiles/Info.plist index dfc49dad1..9c7933379 100644 --- a/ElementX/SupportingFiles/Info.plist +++ b/ElementX/SupportingFiles/Info.plist @@ -28,6 +28,8 @@ NSCameraUsageDescription To take pictures or videos and send them as a message $(APP_DISPLAY_NAME) needs access to the camera. + NSLocationWhenInUseUsageDescription + When you share your location to people, $(APP_DISPLAY_NAME) needs access to show them a map. NSMicrophoneUsageDescription To take videos with audio and send them as a message $(APP_DISPLAY_NAME) needs access to the microphone. NSPhotoLibraryAddUsageDescription diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 8ffa832d8..4dde9748c 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -69,6 +69,7 @@ targets: NSCameraUsageDescription: To take pictures or videos and send them as a message $(APP_DISPLAY_NAME) needs access to the camera. NSMicrophoneUsageDescription: To take videos with audio and send them as a message $(APP_DISPLAY_NAME) needs access to the microphone. NSPhotoLibraryAddUsageDescription: Allows saving photos and videos to your library. + NSLocationWhenInUseUsageDescription: When you share your location to people, $(APP_DISPLAY_NAME) needs access to show them a map. UIBackgroundModes: [ fetch ] diff --git a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift index 057ec0db0..381a487b0 100644 --- a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift +++ b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift @@ -32,13 +32,48 @@ class StaticLocationScreenViewModelTests: XCTestCase { override func setUpWithError() throws { let viewModel = StaticLocationScreenViewModel(interactionMode: .picker) - viewModel.state.isSharingUserLocation = true + viewModel.state.bindings.isLocationAuthorized = true self.viewModel = viewModel } func testUserDidPan() async throws { XCTAssertTrue(context.viewState.isSharingUserLocation) + XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) context.send(viewAction: .userDidPan) XCTAssertFalse(context.viewState.isSharingUserLocation) + XCTAssertEqual(context.showsUserLocationMode, .show) + } + + func testCenterOnUser() async throws { + XCTAssertTrue(context.viewState.isSharingUserLocation) + context.showsUserLocationMode = .show + XCTAssertFalse(context.viewState.isSharingUserLocation) + context.send(viewAction: .centerToUser) + XCTAssertTrue(context.viewState.isSharingUserLocation) + XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) + } + + func testCenterOnUserWithoutAuth() async throws { + context.showsUserLocationMode = .hide + context.isLocationAuthorized = nil + context.send(viewAction: .centerToUser) + XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) + } + + func testCenterOnUserWithDeniedAuth() async throws { + context.isLocationAuthorized = false + context.showsUserLocationMode = .hide + context.send(viewAction: .centerToUser) + XCTAssertNotEqual(context.showsUserLocationMode, .showAndFollow) + XCTAssertNotNil(context.alertInfo) + } + + func testErrorMapping() async throws { + let mapError = AlertInfo(locationSharingViewError: .mapError(.failedLoadingMap)) + XCTAssertEqual(mapError.message, L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName)) + let locationError = AlertInfo(locationSharingViewError: .mapError(.failedLocatingUser)) + XCTAssertEqual(locationError.message, L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName)) + let authorizationError = AlertInfo(locationSharingViewError: .missingAuthorization) + XCTAssertEqual(authorizationError.message, L10n.errorMissingLocationAuth(InfoPlistReader.main.bundleDisplayName)) } } diff --git a/changelog.d/1272.feature b/changelog.d/1272.feature new file mode 100644 index 000000000..026ab6ad8 --- /dev/null +++ b/changelog.d/1272.feature @@ -0,0 +1 @@ +Send current user location \ No newline at end of file