diff options
59 files changed, 1811 insertions, 133 deletions
diff --git a/assets/icons/bubbles_stroke2_corner2_rounded.svg b/assets/icons/bubbles_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..f09fcaa33 --- /dev/null +++ b/assets/icons/bubbles_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/car_stroke2_corner2_rounded.svg b/assets/icons/car_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..ca29cefd1 --- /dev/null +++ b/assets/icons/car_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.018 6a1 1 0 0 0-.808.412l-.81-.588.809.588L3 10.825V17a1 1 0 1 0 2 0 1 1 0 0 1 1-1h12a1 1 0 0 1 1 1 1 1 0 1 0 2 0v-5.998l-3.22-4.577A1 1 0 0 0 16.962 6H7.018ZM23 11.686V17a3 3 0 0 1-5.83 1H6.83A3.001 3.001 0 0 1 1 17v-5.5a1 1 0 1 1 0-2h.49l3.102-4.265A3 3 0 0 1 7.018 4h9.944a3 3 0 0 1 2.453 1.274l3.104 4.412H23a1 1 0 1 1 0 2ZM5 13a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1Zm10 0a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/circleQuestion_stroke2_corner2_rounded.svg b/assets/icons/circleQuestion_stroke2_corner2_rounded.svg index a534f9871..c025a1450 100644 --- a/assets/icons/circleQuestion_stroke2_corner2_rounded.svg +++ b/assets/icons/circleQuestion_stroke2_corner2_rounded.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/><path fill="#000" fill-rule="evenodd" d="M12 9a1 1 0 0 0-.879.522 1 1 0 0 1-1.754-.96A3 3 0 0 1 12 7c1.515 0 2.567 1.006 2.866 2.189.302 1.189-.156 2.574-1.524 3.258A.62.62 0 0 0 13 13a1 1 0 1 1-2 0c0-.992.56-1.898 1.447-2.342.455-.227.572-.618.48-.978C12.836 9.314 12.529 9 12 9Z" clip-rule="evenodd"/><path fill="#000" d="M13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm10-3a1 1 0 0 0-.879.522 1 1 0 0 1-1.754-.96A3 3 0 0 1 12 7c1.515 0 2.567 1.006 2.866 2.189.302 1.189-.156 2.574-1.524 3.258A.62.62 0 0 0 13 13a1 1 0 1 1-2 0c0-.992.56-1.898 1.447-2.342.455-.227.572-.618.48-.978C12.836 9.314 12.529 9 12 9Zm1 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg> diff --git a/assets/icons/codeBrackets_stroke2_corner2_rounded.svg b/assets/icons/codeBrackets_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..d40e7b003 --- /dev/null +++ b/assets/icons/codeBrackets_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M14.243 3.03a1 1 0 0 1 .727 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414l-2.586 2.586a1 1 0 0 0 0 1.414l2.586 2.586a1 1 0 1 1-1.414 1.414l-2.586-2.586a3 3 0 0 1 0-4.242l2.586-2.586a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l2.586 2.586a3 3 0 0 1 0 4.242l-2.586 2.586a1 1 0 1 1-1.414-1.414l2.586-2.586a1 1 0 0 0 0-1.414l-2.586-2.586a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/codeLines_stroke2_corner2_rounded.svg b/assets/icons/codeLines_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..65a7b1e85 --- /dev/null +++ b/assets/icons/codeLines_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2 5a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm15 0a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2h-3a1 1 0 0 1-1-1ZM2 12a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm10 0a1 1 0 0 1 1-1h8a1 1 0 1 1 0 2h-8a1 1 0 0 1-1-1ZM2 19a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm12 0a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/earth_stroke2_corner2_rounded.svg b/assets/icons/earth_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..b8922629a --- /dev/null +++ b/assets/icons/earth_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.67 2.679a2.95 2.95 0 0 1-2.14 2.142l-2.173.547a.32.32 0 0 0-.205.164 2.316 2.316 0 0 1-3.457.81L4.4 9.493Zm.883-1.84 2.171 1.63a.315.315 0 0 0 .471-.11c.303-.6.851-1.04 1.503-1.204l2.174-.546a.95.95 0 0 0 .687-.688l.97.242-.97-.242.67-2.678a7.99 7.99 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.048.543a2.2 2.2 0 0 1 1.69-.636l.827.053c.52.033 1.023.204 1.456.495l1.37.921a2.453 2.453 0 0 1-1.367 4.489h-.98a2.95 2.95 0 0 1-2.45-1.312L9.77 15.32a2.2 2.2 0 0 1 .278-2.776Zm1.563 1.36a.197.197 0 0 0-.177.306l.823 1.235c.176.263.471.42.787.42h.98a.453.453 0 0 0 .252-.828l-1.37-.921a.95.95 0 0 0-.468-.159l-.827-.053Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/freeze_stroke2_corner2_rounded.svg b/assets/icons/freeze_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..109145478 --- /dev/null +++ b/assets/icons/freeze_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.789 2.293a1 1 0 0 1 1.414 0l1.793 1.793 1.793-1.793a1 1 0 1 1 1.414 1.414l-2.207 2.207v4.355l3.772-2.178.808-3.015a1 1 0 1 1 1.931.518l-.656 2.449 2.45.656a1 1 0 1 1-.518 1.932l-3.015-.808L13.998 12l3.77 2.177 3.015-.808a1 1 0 1 1 .517 1.932l-2.449.656.657 2.45a1 1 0 1 1-1.932.517l-.808-3.015-3.772-2.178v4.355l2.207 2.207a1 1 0 0 1-1.414 1.414l-1.793-1.793-1.793 1.793a1 1 0 0 1-1.414-1.414l2.207-2.207v-4.353l-3.77 2.176-.807 3.015a1 1 0 0 1-1.932-.518l.656-2.449-2.449-.656a1 1 0 1 1 .518-1.932l3.015.808L9.997 12l-3.77-2.177-3.015.808a1 1 0 0 1-.518-1.932l2.45-.656-.657-2.45a1 1 0 0 1 1.932-.517l.808 3.015 3.77 2.176V5.914L8.788 3.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/haptic_stroke2_corner2_rounded.svg b/assets/icons/haptic_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..7a93daf66 --- /dev/null +++ b/assets/icons/haptic_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M9 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H9ZM6 6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6Zm4 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm-5.793.293a1 1 0 0 1 0 1.414L3.155 9.76a.546.546 0 0 0-.05.713 2.55 2.55 0 0 1 0 3.056.546.546 0 0 0 .05.713l1.052 1.052a1 1 0 1 1-1.414 1.414L1.74 15.655a2.546 2.546 0 0 1-.237-3.327.55.55 0 0 0 0-.655 2.546 2.546 0 0 1 .237-3.328l1.052-1.052a1 1 0 0 1 1.414 0Zm15.586 0a1 1 0 0 1 1.414 0l1.052 1.052c.896.896.997 2.314.237 3.327a.55.55 0 0 0 0 .656 2.546 2.546 0 0 1-.237 3.327l-1.052 1.052a1 1 0 0 1-1.414-1.414l1.052-1.052a.546.546 0 0 0 .05-.713 2.55 2.55 0 0 1 0-3.056.546.546 0 0 0-.05-.713l-1.052-1.052a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/home_stroke2_corner2_rounded.svg b/assets/icons/home_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..5665b24b8 --- /dev/null +++ b/assets/icons/home_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.659 3.905a1 1 0 0 0-1.318 0l-6 5.25A1 1 0 0 0 5 9.907V18a1 1 0 0 0 1 1h3v-3a3 3 0 0 1 6 0v3h3a1 1 0 0 0 1-1V9.907a1 1 0 0 0-.341-.752l-6-5.25ZM10.024 2.4a3 3 0 0 1 3.952 0l6 5.25A3 3 0 0 1 21 9.907V18a3 3 0 0 1-3 3h-3a2 2 0 0 1-2-2v-3a1 1 0 0 0-2 0v3a2 2 0 0 1-2 2H6a3 3 0 0 1-3-3V9.907A3 3 0 0 1 4.024 7.65l6-5.25Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/key_stroke2_corner2_rounded.svg b/assets/icons/key_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..2beb2b4ae --- /dev/null +++ b/assets/icons/key_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 12a4 4 0 0 1 7.212-2.385c.363.488.963.885 1.696.885h8.111l1.2 1.5-1.2 1.5h-1.783l-1.789-.894a1 1 0 0 0-.894 0l-1.79.894h-1.855c-.733 0-1.333.397-1.696.885A4 4 0 0 1 3 12Zm4-6a6 6 0 1 0 4.817 9.579.3.3 0 0 1 .076-.072l.017-.007H14a1 1 0 0 0 .447-.106L16 14.618l1.553.776c.139.07.292.106.447.106h2.02a2 2 0 0 0 1.561-.75l1.2-1.5a2 2 0 0 0 0-2.5l-1.2-1.5a2 2 0 0 0-1.562-.75h-8.11l-.016-.007a.3.3 0 0 1-.077-.071A6 6 0 0 0 7 6Zm0 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/macintosh_stroke2_corner2_rounded.svg b/assets/icons/macintosh_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..543531972 --- /dev/null +++ b/assets/icons/macintosh_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 5a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v11a3 3 0 0 1-1 2.236V20a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-1.764c-.614-.55-1-1.348-1-2.236V5Zm3 14v1h10v-1H7ZM7 4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H7Zm0 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V6Zm2 1v4h6V7H9Zm4 8a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/newspaper_stroke2_corner2_rounded.svg b/assets/icons/newspaper_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..3eb823cf2 --- /dev/null +++ b/assets/icons/newspaper_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M1 6.5A2.5 2.5 0 0 1 3.5 4H9a4 4 0 0 1 3 1.354A4 4 0 0 1 15 4h5.5A2.5 2.5 0 0 1 23 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-5.223c-.52 0-1 .125-1.4.372-.421.26-.761.633-.983 1.075a1 1 0 0 1-1.788 0 2.66 2.66 0 0 0-.983-1.075c-.4-.247-.88-.372-1.4-.372H3.5A2.5 2.5 0 0 1 1 17.5v-11ZM11 8a2 2 0 0 0-2-2H3.5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h5.223c.776 0 1.564.173 2.277.569V8Zm2 10.569A4.7 4.7 0 0 1 15.277 18H20.5a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5H15a2 2 0 0 0-2 2v10.569Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/pencilLine_stroke2_corner2_rounded.svg b/assets/icons/pencilLine_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..92f1fed1c --- /dev/null +++ b/assets/icons/pencilLine_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.379 2.707a3 3 0 0 1 4.242 0l1.672 1.672a3 3 0 0 1 0 4.242l-12.5 12.5A3 3 0 0 1 6.672 22H3a1 1 0 0 1-1-1v-3.672a3 3 0 0 1 .879-2.121l12.5-12.5Zm2.828 1.414a1 1 0 0 0-1.414 0l-12.5 12.5a1 1 0 0 0-.293.707V20h2.672a1 1 0 0 0 .707-.293l12.5-12.5.707.707-.707-.707a1 1 0 0 0 0-1.414L18.207 4.12ZM13 21a1 1 0 0 1 1-1h7a1 1 0 0 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/personGroup_stroke2_corner2_rounded.svg b/assets/icons/personGroup_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..fad8a8e7f --- /dev/null +++ b/assets/icons/personGroup_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm7.301 9.7c-.836-2.6-2.88-3.503-4.575-3.111a1 1 0 0 1-.451-1.949c2.815-.651 5.81.966 6.93 4.448a2.49 2.49 0 0 1-.506 2.43A2.92 2.92 0 0 1 20 20h-2a1 1 0 1 1 0-2h2a.92.92 0 0 0 .69-.295.49.49 0 0 0 .112-.505ZM8 14c-1.865 0-3.878 1.274-4.681 4.151a.57.57 0 0 0 .132.55c.15.171.4.299.695.299h7.708a.93.93 0 0 0 .695-.299.57.57 0 0 0 .132-.55C11.878 15.274 9.865 14 8 14Zm0-2c2.87 0 5.594 1.98 6.607 5.613.53 1.9-1.09 3.387-2.753 3.387H4.146c-1.663 0-3.283-1.487-2.753-3.387C2.406 13.981 5.129 12 8 12Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/raisingHand4finger_stroke2_corner2_rounded.svg b/assets/icons/raisingHand4finger_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..f6beb5647 --- /dev/null +++ b/assets/icons/raisingHand4finger_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.5 4a.5.5 0 0 0-.5.5V10a1 1 0 1 1-2 0V5.5a.5.5 0 0 0-1 0V11a1 1 0 1 1-2 0V7.5a.5.5 0 0 0-1 0v6a6.5 6.5 0 1 0 13 0V9a1.996 1.996 0 0 0-2 2v.838c0 .826-.529 1.559-1.312 1.82A2.47 2.47 0 0 0 14 16a1 1 0 1 1-2 0 4.47 4.47 0 0 1 3-4.22V11c0-1.014.379-1.941 1-2.646V5.5a.5.5 0 0 0-1 0V10a1 1 0 1 1-2 0V4.5a.5.5 0 0 0-.5-.5Zm2.112-.838A2.5 2.5 0 0 1 18 5.5v1.626q.481-.124 1-.126a2 2 0 0 1 2 2v4.5a8.5 8.5 0 0 1-17 0v-6a2.5 2.5 0 0 1 3.039-2.442 2.5 2.5 0 0 1 3.349-1.896 2.498 2.498 0 0 1 4.224 0Z" clip-rule="evenodd"/></svg> diff --git a/assets/icons/wrench_stroke2_corner2_rounded.svg b/assets/icons/wrench_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..601614c88 --- /dev/null +++ b/assets/icons/wrench_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M14.5 4a5.5 5.5 0 0 0-5.078 7.616 1 1 0 0 1-.216 1.092L4.37 17.543a1 1 0 0 0 0 1.414l.672.672a1 1 0 0 0 1.414 0l4.835-4.835a1 1 0 0 1 1.092-.216A5.5 5.5 0 0 0 20 9.414l-1.293 1.293a3.828 3.828 0 1 1-5.414-5.414L14.585 4H14.5ZM7 9.5a7.5 7.5 0 0 1 9.969-7.084 1 1 0 0 1 .378 1.651l-2.64 2.64a1.829 1.829 0 0 0 2.586 2.586l2.64-2.64a1 1 0 0 1 1.65.378 7.5 7.5 0 0 1-9.328 9.627l-4.384 4.385a3 3 0 0 1-4.242 0l-.672-.672a3 3 0 0 1 0-4.242l4.385-4.385A7.5 7.5 0 0 1 7 9.5Z" clip-rule="evenodd"/></svg> diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 12b7b8960..d0d6b4c49 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -253,6 +253,10 @@ func serve(cctx *cli.Context) error { e.GET("/settings/external-embeds", server.WebGeneric) e.GET("/settings/accessibility", server.WebGeneric) e.GET("/settings/appearance", server.WebGeneric) + e.GET("/settings/account", server.WebGeneric) + e.GET("/settings/privacy-and-security", server.WebGeneric) + e.GET("/settings/content-and-media", server.WebGeneric) + e.GET("/settings/about", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/debug-mod", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 81d08c7da..436f2488a 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -95,6 +95,10 @@ import {Wizard} from '#/screens/StarterPack/Wizard' import {useTheme} from '#/alf' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {AboutSettingsScreen} from './screens/Settings/AboutSettings' +import {AccountSettingsScreen} from './screens/Settings/AccountSettings' +import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' +import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' const navigationRef = createNavigationContainerRef<AllNavigatorParams>() @@ -322,7 +326,39 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { name="AppearanceSettings" getComponent={() => AppearanceSettingsScreen} options={{ - title: title(msg`Appearance Settings`), + title: title(msg`Appearance`), + requireAuth: true, + }} + /> + <Stack.Screen + name="AccountSettings" + getComponent={() => AccountSettingsScreen} + options={{ + title: title(msg`Account`), + requireAuth: true, + }} + /> + <Stack.Screen + name="PrivacyAndSecuritySettings" + getComponent={() => PrivacyAndSecuritySettingsScreen} + options={{ + title: title(msg`Privacy and Security`), + requireAuth: true, + }} + /> + <Stack.Screen + name="ContentAndMediaSettings" + getComponent={() => ContentAndMediaSettingsScreen} + options={{ + title: title(msg`Content and Media`), + requireAuth: true, + }} + /> + <Stack.Screen + name="AboutSettings" + getComponent={() => AboutSettingsScreen} + options={{ + title: title(msg`About`), requireAuth: true, }} /> diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index ca4c14dc6..434420a97 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -102,6 +102,9 @@ export const atoms = { /* * Flex */ + gap_0: { + gap: 0, + }, gap_2xs: { gap: tokens.space._2xs, }, diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 57e373164..ea11e2217 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,16 +1,22 @@ -import React from 'react' +import React, {useContext, useMemo} from 'react' import {View, ViewStyle} from 'react-native' import {StyleProp} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {ScrollView} from '#/view/com/util/Views' +import {CenteredView} from '#/view/com/util/Views' import {atoms as a} from '#/alf' // Every screen should have a Layout component wrapping it. // This component provides a default padding for the top of the screen. // This allows certain screens to avoid the top padding if they want to. -// -// In a future PR I will add a unified header component to this file and -// things like a preconfigured scrollview. + +const LayoutContext = React.createContext({ + withinScreen: false, + topPaddingDisabled: false, + withinScrollView: false, +}) /** * Every screen should have a Layout.Screen component wrapping it. @@ -18,7 +24,7 @@ import {atoms as a} from '#/alf' * and height/minHeight */ let Screen = ({ - disableTopPadding, + disableTopPadding = false, style, ...props }: React.ComponentProps<typeof View> & { @@ -26,16 +32,69 @@ let Screen = ({ style?: StyleProp<ViewStyle> }): React.ReactNode => { const {top} = useSafeAreaInsets() + const context = useMemo( + () => ({ + withinScreen: true, + topPaddingDisabled: disableTopPadding, + withinScrollView: false, + }), + [disableTopPadding], + ) return ( - <View - style={[ - {paddingTop: disableTopPadding ? 0 : top}, - a.util_screen_outer, - style, - ]} - {...props} - /> + <LayoutContext.Provider value={context}> + <View + style={[ + {paddingTop: disableTopPadding ? 0 : top}, + a.util_screen_outer, + style, + ]} + {...props} + /> + </LayoutContext.Provider> ) } Screen = React.memo(Screen) export {Screen} + +let Header = ( + props: React.ComponentProps<typeof ViewHeader>, +): React.ReactNode => { + const {withinScrollView} = useContext(LayoutContext) + if (!withinScrollView) { + return ( + <CenteredView topBorder={false} sideBorders> + <ViewHeader showOnDesktop showBorder {...props} /> + </CenteredView> + ) + } else { + return <ViewHeader showOnDesktop showBorder {...props} /> + } +} +Header = React.memo(Header) +export {Header} + +let Content = ({ + style, + contentContainerStyle, + ...props +}: React.ComponentProps<typeof ScrollView> & { + style?: StyleProp<ViewStyle> + contentContainerStyle?: StyleProp<ViewStyle> +}): React.ReactNode => { + const context = useContext(LayoutContext) + const newContext = useMemo( + () => ({...context, withinScrollView: true}), + [context], + ) + return ( + <LayoutContext.Provider value={newContext}> + <ScrollView + style={[a.flex_1, style]} + contentContainerStyle={[{paddingBottom: 100}, contentContainerStyle]} + {...props} + /> + </LayoutContext.Provider> + ) +} +Content = React.memo(Content) +export {Content} diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx index 81d0c6740..8f47d05b0 100644 --- a/src/components/dialogs/BirthDateSettings.tsx +++ b/src/components/dialogs/BirthDateSettings.tsx @@ -29,7 +29,7 @@ export function BirthDateSettingsDialog({ const {isLoading, error, data: preferences} = usePreferencesQuery() return ( - <Dialog.Outer control={control}> + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`My Birthday`)}> <View style={[a.gap_sm, a.pb_lg]}> diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx index ea870e2da..daad01d2a 100644 --- a/src/components/dialogs/SwitchAccount.tsx +++ b/src/components/dialogs/SwitchAccount.tsx @@ -56,6 +56,8 @@ export function SwitchAccountDialog({ pendingDid={pendingDid} /> </View> + + <Dialog.Close /> </Dialog.ScrollableInner> </Dialog.Outer> ) diff --git a/src/components/icons/Bubble.tsx b/src/components/icons/Bubble.tsx index ff6da2531..3654fcf78 100644 --- a/src/components/icons/Bubble.tsx +++ b/src/components/icons/Bubble.tsx @@ -11,3 +11,7 @@ export const Bubble_Stroke2_Corner2_Rounded = createSinglePathSVG({ export const Bubble_Stroke2_Corner3_Rounded = createSinglePathSVG({ path: 'M2.002 7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2a4 4 0 0 1-4-4V7Zm4-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h6a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-12Z', }) + +export const Bubbles_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z', +}) diff --git a/src/components/icons/Car.tsx b/src/components/icons/Car.tsx new file mode 100644 index 000000000..25859176a --- /dev/null +++ b/src/components/icons/Car.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Car_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M7.018 6a1 1 0 0 0-.808.412L5.4 5.824l.809.588L3 10.825V17a1 1 0 1 0 2 0 1 1 0 0 1 1-1h12a1 1 0 0 1 1 1 1 1 0 1 0 2 0v-5.998l-3.22-4.577A1 1 0 0 0 16.962 6H7.018ZM23 11.686V17a3 3 0 0 1-5.83 1H6.83A3.001 3.001 0 0 1 1 17v-5.5a1 1 0 1 1 0-2h.49l3.102-4.265A3 3 0 0 1 7.018 4h9.944a3 3 0 0 1 2.453 1.274l3.104 4.412H23a1 1 0 1 1 0 2ZM5 13a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1Zm10 0a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z', +}) diff --git a/src/components/icons/CircleQuestion.tsx b/src/components/icons/CircleQuestion.tsx index 4eb369379..6242a2b51 100644 --- a/src/components/icons/CircleQuestion.tsx +++ b/src/components/icons/CircleQuestion.tsx @@ -1,5 +1,5 @@ import {createSinglePathSVG} from './TEMPLATE' export const CircleQuestion_Stroke2_Corner2_Rounded = createSinglePathSVG({ - path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/><path fill="#000" fill-rule="evenodd" d="M12 9a1 1 0 0 0-.879.522 1 1 0 0 1-1.754-.96A3 3 0 0 1 12 7c1.515 0 2.567 1.006 2.866 2.189.302 1.189-.156 2.574-1.524 3.258A.62.62 0 0 0 13 13a1 1 0 1 1-2 0c0-.992.56-1.898 1.447-2.342.455-.227.572-.618.48-.978C12.836 9.314 12.529 9 12 9Z', + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z M12 9a1 1 0 0 0-.879.522 1 1 0 0 1-1.754-.96A3 3 0 0 1 12 7c1.515 0 2.567 1.006 2.866 2.189.302 1.189-.156 2.574-1.524 3.258A.62.62 0 0 0 13 13a1 1 0 1 1-2 0c0-.992.56-1.898 1.447-2.342.455-.227.572-.618.48-.978C12.836 9.314 12.529 9 12 9Z M13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z', }) diff --git a/src/components/icons/CodeBrackets.tsx b/src/components/icons/CodeBrackets.tsx index 59d5fca90..98c679f04 100644 --- a/src/components/icons/CodeBrackets.tsx +++ b/src/components/icons/CodeBrackets.tsx @@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE' export const CodeBrackets_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M14.242 3.03a1 1 0 0 1 .728 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414L3.414 12l3.293 3.293a1 1 0 1 1-1.414 1.414l-4-4a1 1 0 0 1 0-1.414l4-4a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 1 1-1.414-1.414L20.586 12l-3.293-3.293a1 1 0 0 1 0-1.414Z', }) + +export const CodeBrackets_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M14.243 3.03a1 1 0 0 1 .727 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414l-2.586 2.586a1 1 0 0 0 0 1.414l2.586 2.586a1 1 0 1 1-1.414 1.414l-2.586-2.586a3 3 0 0 1 0-4.242l2.586-2.586a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l2.586 2.586a3 3 0 0 1 0 4.242l-2.586 2.586a1 1 0 1 1-1.414-1.414l2.586-2.586a1 1 0 0 0 0-1.414l-2.586-2.586a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/CodeLines.tsx b/src/components/icons/CodeLines.tsx new file mode 100644 index 000000000..833dc2316 --- /dev/null +++ b/src/components/icons/CodeLines.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CodeLines_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M2 5a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm15 0a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2h-3a1 1 0 0 1-1-1ZM2 12a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm10 0a1 1 0 0 1 1-1h8a1 1 0 1 1 0 2h-8a1 1 0 0 1-1-1ZM2 19a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm12 0a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1Z', +}) diff --git a/src/components/icons/Freeze.tsx b/src/components/icons/Freeze.tsx new file mode 100644 index 000000000..628157b4f --- /dev/null +++ b/src/components/icons/Freeze.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Freeze_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M8.789 2.293a1 1 0 0 1 1.414 0l1.793 1.793 1.793-1.793a1 1 0 1 1 1.414 1.414l-2.207 2.207v4.355l3.772-2.178.808-3.015a1 1 0 1 1 1.931.518l-.656 2.449 2.45.656a1 1 0 1 1-.518 1.932l-3.015-.808L13.998 12l3.77 2.177 3.015-.808a1 1 0 1 1 .517 1.932l-2.449.656.657 2.45a1 1 0 1 1-1.932.517l-.808-3.015-3.772-2.178v4.355l2.207 2.207a1 1 0 0 1-1.414 1.414l-1.793-1.793-1.793 1.793a1 1 0 0 1-1.414-1.414l2.207-2.207v-4.353l-3.77 2.176-.807 3.015a1 1 0 0 1-1.932-.518l.656-2.449-2.449-.656a1 1 0 1 1 .518-1.932l3.015.808L9.997 12l-3.77-2.177-3.015.808a1 1 0 0 1-.518-1.932l2.45-.656-.657-2.45a1 1 0 0 1 1.932-.517l.808 3.015 3.77 2.176V5.914L8.788 3.707a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/Globe.tsx b/src/components/icons/Globe.tsx index 53ef84eec..837c7c1a1 100644 --- a/src/components/icons/Globe.tsx +++ b/src/components/icons/Globe.tsx @@ -7,3 +7,7 @@ export const Globe_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const Earth_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.953 3.81a1 1 0 0 1-.726.727l-3.444.866-.772 1.533a1 1 0 0 1-1.493.35L4.4 9.493Zm.883-1.84L7.756 9.51l.44-.874a1 1 0 0 1 .649-.52l3.306-.832.807-3.227a7.993 7.993 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.43.162a1 1 0 0 1 .77-.29l1.89.121a1 1 0 0 1 .494.168l2.869 1.928a1 1 0 0 1 .336 1.277l-.973 1.946a1 1 0 0 1-.894.553h-2.92a1 1 0 0 1-.831-.445L9.225 14.5a1 1 0 0 1 .126-1.262l1.08-1.076Zm.915 1.913.177-.177 1.171.074 1.914 1.286-.303.607h-1.766l-1.194-1.79Z', }) + +export const Earth_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.67 2.679a2.95 2.95 0 0 1-2.14 2.142l-2.173.547a.32.32 0 0 0-.205.164 2.316 2.316 0 0 1-3.457.81L4.4 9.493Zm.883-1.84 2.171 1.63a.315.315 0 0 0 .471-.11c.303-.6.851-1.04 1.503-1.204l2.174-.546a.95.95 0 0 0 .687-.688l.97.242-.97-.242.67-2.678a7.993 7.993 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.048.543a2.2 2.2 0 0 1 1.69-.636l.827.053c.52.033 1.023.204 1.456.495l1.37.921a2.453 2.453 0 0 1-1.367 4.489h-.98a2.95 2.95 0 0 1-2.45-1.312L9.77 15.32a2.2 2.2 0 0 1 .278-2.776Zm1.563 1.36a.197.197 0 0 0-.177.306l.823 1.235c.176.263.471.42.787.42h.98a.453.453 0 0 0 .252-.828l-1.37-.921a.95.95 0 0 0-.468-.159l-.827-.053Z', +}) diff --git a/src/components/icons/Haptic.tsx b/src/components/icons/Haptic.tsx new file mode 100644 index 000000000..09726e2f1 --- /dev/null +++ b/src/components/icons/Haptic.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Haptic_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M9 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H9ZM6 6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6Zm4 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm-5.793.293a1 1 0 0 1 0 1.414L3.155 9.76a.546.546 0 0 0-.05.713c.678.906.678 2.15 0 3.056a.546.546 0 0 0 .05.713l1.052 1.052a1 1 0 1 1-1.414 1.414L1.74 15.655a2.546 2.546 0 0 1-.237-3.327.55.55 0 0 0 0-.655 2.546 2.546 0 0 1 .237-3.328l1.052-1.052a1 1 0 0 1 1.414 0Zm15.586 0a1 1 0 0 1 1.414 0l1.052 1.052c.896.896.997 2.314.237 3.327a.55.55 0 0 0 0 .656 2.546 2.546 0 0 1-.237 3.327l-1.052 1.052a1 1 0 0 1-1.414-1.414l1.052-1.052a.546.546 0 0 0 .05-.713 2.55 2.55 0 0 1 0-3.056.546.546 0 0 0-.05-.713l-1.052-1.052a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/Home.tsx b/src/components/icons/Home.tsx index e150b7b81..a5e796e6c 100644 --- a/src/components/icons/Home.tsx +++ b/src/components/icons/Home.tsx @@ -1,7 +1,11 @@ import {createSinglePathSVG} from './TEMPLATE' export const Home_Stroke2_Corner0_Rounded = createSinglePathSVG({ - path: 'M11.46 1.362a2 2 0 0 1 1.08 0c.249.07.448.188.611.301.146.102.306.232.467.363l6.421 5.218.046.036c.169.137.38.308.54.53a2 2 0 0 1 .304.64c.073.264.072.536.071.753v9.229c0 .252 0 .498-.017.706a2.023 2.023 0 0 1-.201.77 2 2 0 0 1-.874.874 2.02 2.02 0 0 1-.77.201c-.208.017-.454.017-.706.017H5.568c-.252 0-.498 0-.706-.017a2.02 2.02 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C3 18.93 3 18.684 3 18.432V9.203c0-.217-.002-.49.07-.754a2 2 0 0 1 .304-.638c.16-.223.372-.394.541-.53l.045-.037 6.422-5.218c.161-.13.321-.26.467-.362.163-.114.362-.232.612-.302Zm.532 1.943c-.077.054-.18.136-.37.29l-6.4 5.2a6.315 6.315 0 0 0-.215.18c-.002 0-.003.002-.004.003v.004C5 9.036 5 9.112 5 9.262V18.4a8.18 8.18 0 0 0 .011.588l.014.002c.116.01.278.01.575.01H8v-5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v5h2.4a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V9.262c0-.15 0-.226-.003-.28v-.004l-.003-.003a6.448 6.448 0 0 0-.216-.18l-6.4-5.2a7.373 7.373 0 0 0-.37-.29L12 3.299l-.008.006ZM14 19v-5h-4v5h4Z', + path: 'M11.37 1.724a1 1 0 0 1 1.26 0l8 6.5A1 1 0 0 1 21 9v11a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-5h-2v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 .37-.776l8-6.5ZM5 9.476V19h4v-5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5h4V9.476l-7-5.688-7 5.688Z', +}) + +export const Home_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12.659 3.905a1 1 0 0 0-1.318 0l-6 5.25A1 1 0 0 0 5 9.907V18a1 1 0 0 0 1 1h3v-3a3 3 0 0 1 6 0v3h3a1 1 0 0 0 1-1V9.907a1 1 0 0 0-.341-.752l-6-5.25ZM10.024 2.4a3 3 0 0 1 3.952 0l6 5.25A3 3 0 0 1 21 9.907V18a3 3 0 0 1-3 3h-3a2 2 0 0 1-2-2v-3a1 1 0 0 0-2 0v3a2 2 0 0 1-2 2H6a3 3 0 0 1-3-3V9.907A3 3 0 0 1 4.024 7.65l6-5.25Z', }) export const Home_Filled_Corner0_Rounded = createSinglePathSVG({ diff --git a/src/components/icons/Key.tsx b/src/components/icons/Key.tsx new file mode 100644 index 000000000..a7abd6b9a --- /dev/null +++ b/src/components/icons/Key.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Key_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M3 12a4 4 0 0 1 7.212-2.385c.363.488.963.885 1.696.885h8.111l1.2 1.5-1.2 1.5h-1.783l-1.789-.894a1 1 0 0 0-.894 0l-1.79.894h-1.855c-.733 0-1.333.397-1.696.885A4 4 0 0 1 3 12Zm4-6a6 6 0 1 0 4.817 9.579.3.3 0 0 1 .076-.072l.017-.007H14a1 1 0 0 0 .447-.106L16 14.618l1.553.776c.139.07.292.106.447.106h2.02a2 2 0 0 0 1.561-.75l1.2-1.5a2 2 0 0 0 0-2.5l-1.2-1.5a2 2 0 0 0-1.562-.75h-8.11l-.016-.007a.3.3 0 0 1-.077-.071A6 6 0 0 0 7 6Zm0 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', +}) diff --git a/src/components/icons/Macintosh.tsx b/src/components/icons/Macintosh.tsx new file mode 100644 index 000000000..8cc598126 --- /dev/null +++ b/src/components/icons/Macintosh.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Macintosh_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M4 5a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v11c0 .889-.386 1.687-1 2.236V20a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-1.764c-.614-.55-1-1.348-1-2.236V5Zm3 14v1h10v-1H7ZM7 4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H7Zm0 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V6Zm2 1v4h6V7H9Zm4 8a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z', +}) diff --git a/src/components/icons/Newspaper.tsx b/src/components/icons/Newspaper.tsx new file mode 100644 index 000000000..bd4b85590 --- /dev/null +++ b/src/components/icons/Newspaper.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Newspaper_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M1 6.5A2.5 2.5 0 0 1 3.5 4H9a4 4 0 0 1 3 1.354A4 4 0 0 1 15 4h5.5A2.5 2.5 0 0 1 23 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-5.223c-.52 0-1 .125-1.4.372-.421.26-.761.633-.983 1.075a1 1 0 0 1-1.788 0 2.66 2.66 0 0 0-.983-1.075c-.4-.247-.88-.372-1.4-.372H3.5A2.5 2.5 0 0 1 1 17.5v-11ZM11 8a2 2 0 0 0-2-2H3.5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h5.223c.776 0 1.564.173 2.277.569V8Zm2 10.569A4.7 4.7 0 0 1 15.277 18H20.5a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5H15a2 2 0 0 0-2 2v10.569Z', +}) diff --git a/src/components/icons/Pencil.tsx b/src/components/icons/Pencil.tsx index 51fd8ba79..a99f8d614 100644 --- a/src/components/icons/Pencil.tsx +++ b/src/components/icons/Pencil.tsx @@ -7,3 +7,7 @@ export const Pencil_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const PencilLine_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', }) + +export const PencilLine_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M15.379 2.707a3 3 0 0 1 4.242 0l1.672 1.672a3 3 0 0 1 0 4.242l-12.5 12.5A3 3 0 0 1 6.672 22H3a1 1 0 0 1-1-1v-3.672a3 3 0 0 1 .879-2.121l12.5-12.5Zm2.828 1.414a1 1 0 0 0-1.414 0l-12.5 12.5a1 1 0 0 0-.293.707V20h2.672a1 1 0 0 0 .707-.293l12.5-12.5.707.707-.707-.707a1 1 0 0 0 0-1.414L18.207 4.12ZM13 21a1 1 0 0 1 1-1h7a1 1 0 0 1 0 2h-7a1 1 0 0 1-1-1Z', +}) diff --git a/src/components/icons/Person.tsx b/src/components/icons/Person.tsx index 4fcc83891..31d7078d9 100644 --- a/src/components/icons/Person.tsx +++ b/src/components/icons/Person.tsx @@ -23,3 +23,7 @@ export const PersonPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const PersonPlus_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z', }) + +export const PersonGroup_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm7.301 9.7c-.836-2.6-2.88-3.503-4.575-3.111a1 1 0 0 1-.451-1.949c2.815-.651 5.81.966 6.93 4.448a2.49 2.49 0 0 1-.506 2.43A2.92 2.92 0 0 1 20 20h-2a1 1 0 1 1 0-2h2a.92.92 0 0 0 .69-.295.49.49 0 0 0 .112-.505ZM8 14c-1.865 0-3.878 1.274-4.681 4.151a.57.57 0 0 0 .132.55c.15.171.4.299.695.299h7.708a.93.93 0 0 0 .695-.299.57.57 0 0 0 .132-.55C11.878 15.274 9.865 14 8 14Zm0-2c2.87 0 5.594 1.98 6.607 5.613.53 1.9-1.09 3.387-2.753 3.387H4.146c-1.663 0-3.283-1.487-2.753-3.387C2.406 13.981 5.129 12 8 12Z', +}) diff --git a/src/components/icons/RaisingHand.tsx b/src/components/icons/RaisingHand.tsx index cd023cb7e..d9d73957d 100644 --- a/src/components/icons/RaisingHand.tsx +++ b/src/components/icons/RaisingHand.tsx @@ -1,5 +1,9 @@ import {createSinglePathSVG} from './TEMPLATE' -export const RaisingHande4Finger_Stroke2_Corner0_Rounded = createSinglePathSVG({ +export const RaisingHand4Finger_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M10.25 4a.75.75 0 0 0-.75.75V11a1 1 0 1 1-2 0V6.75a.75.75 0 0 0-1.5 0V14a6 6 0 0 0 12 0V9a2 2 0 0 0-2 2v1.5a1 1 0 0 1-.684.949l-.628.21A2.469 2.469 0 0 0 13 16a1 1 0 1 1-2 0 4.469 4.469 0 0 1 3-4.22V11c0-.703.181-1.364.5-1.938V5.75a.75.75 0 0 0-1.5 0V9a1 1 0 1 1-2 0V4.75a.75.75 0 0 0-.75-.75Zm2.316-.733A2.75 2.75 0 0 1 16.5 5.75v1.54c.463-.187.97-.29 1.5-.29h1a1 1 0 0 1 1 1v6a8 8 0 1 1-16 0V6.75a2.75 2.75 0 0 1 3.571-2.625 2.751 2.751 0 0 1 4.995-.858Z', }) + +export const RaisingHand4Finger_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12.5 4a.5.5 0 0 0-.5.5V10a1 1 0 1 1-2 0V5.5a.5.5 0 0 0-1 0V11a1 1 0 1 1-2 0V7.5a.5.5 0 0 0-1 0v6a6.5 6.5 0 1 0 13 0V9c-.513 0-.979.192-1.333.509-.41.368-.667.899-.667 1.491v.838c0 .826-.529 1.559-1.312 1.82A2.47 2.47 0 0 0 14 16a1 1 0 1 1-2 0 4.47 4.47 0 0 1 3-4.22V11c0-1.014.379-1.941 1-2.646V5.5a.5.5 0 0 0-1 0V10a1 1 0 1 1-2 0V4.5a.5.5 0 0 0-.5-.5Zm2.112-.838A2.5 2.5 0 0 1 18 5.5v1.626q.481-.124 1-.126a2 2 0 0 1 2 2v4.5a8.5 8.5 0 0 1-17 0v-6a2.5 2.5 0 0 1 3.039-2.442 2.5 2.5 0 0 1 3.349-1.896 2.498 2.498 0 0 1 4.224 0Z', +}) diff --git a/src/components/icons/Wrench.tsx b/src/components/icons/Wrench.tsx new file mode 100644 index 000000000..4b4c86ec4 --- /dev/null +++ b/src/components/icons/Wrench.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Wrench_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M14.5 4a5.5 5.5 0 0 0-5.078 7.616 1 1 0 0 1-.216 1.092L4.37 17.543a1 1 0 0 0 0 1.414l.672.672a1 1 0 0 0 1.414 0l4.835-4.835a1 1 0 0 1 1.092-.216A5.5 5.5 0 0 0 20 9.414l-1.293 1.293a3.828 3.828 0 1 1-5.414-5.414L14.585 4 14.5 4ZM7 9.5a7.5 7.5 0 0 1 9.969-7.084 1 1 0 0 1 .378 1.651l-2.64 2.64a1.829 1.829 0 0 0 2.586 2.586l2.64-2.64a1 1 0 0 1 1.65.378 7.5 7.5 0 0 1-9.328 9.627l-4.384 4.385a3 3 0 0 1-4.242 0l-.672-.672a3 3 0 0 1 0-4.242l4.385-4.385A7.5 7.5 0 0 1 7 9.5Z', +}) diff --git a/src/lib/app-info.web.ts b/src/lib/app-info.web.ts index 7227e2863..742ccfe97 100644 --- a/src/lib/app-info.web.ts +++ b/src/lib/app-info.web.ts @@ -3,6 +3,7 @@ import {version} from '../../package.json' export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development' export const IS_TESTFLIGHT = false +export const IS_INTERNAL = IS_DEV // This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings // along with the other version info. Useful for debugging/reporting. diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 426665d07..e699a47d3 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -11,7 +11,6 @@ export type CommonNavigatorParams = { ModerationMutedAccounts: undefined ModerationBlockedAccounts: undefined Settings: undefined - LanguageSettings: undefined Profile: {name: string; hideBackButton?: boolean} ProfileFollowers: {name: string} ProfileFollows: {name: string} @@ -33,6 +32,7 @@ export type CommonNavigatorParams = { TermsOfService: undefined CommunityGuidelines: undefined CopyrightPolicy: undefined + LanguageSettings: undefined AppPasswords: undefined SavedFeeds: undefined PreferencesFollowingFeed: undefined @@ -40,6 +40,10 @@ export type CommonNavigatorParams = { PreferencesExternalEmbeds: undefined AccessibilitySettings: undefined AppearanceSettings: undefined + AccountSettings: undefined + PrivacyAndSecuritySettings: undefined + ContentAndMediaSettings: undefined + AboutSettings: undefined Search: {q?: string} Hashtag: {tag: string; author?: string} MessagesConversation: {conversation: string; embed?: string} diff --git a/src/routes.ts b/src/routes.ts index 2ae4126ac..58e9a3abc 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,4 +1,4 @@ -import {Router} from 'lib/routes/router' +import {Router} from '#/lib/routes/router' export const router = new Router({ Home: '/', @@ -7,12 +7,13 @@ export const router = new Router({ Notifications: '/notifications', NotificationsSettings: '/notifications/settings', Settings: '/settings', - LanguageSettings: '/settings/language', Lists: '/lists', + // moderation Moderation: '/moderation', ModerationModlists: '/moderation/modlists', ModerationMutedAccounts: '/moderation/muted-accounts', ModerationBlockedAccounts: '/moderation/blocked-accounts', + // profiles, threads, lists Profile: ['/profile/:name', '/profile/:name/rss'], ProfileFollowers: '/profile/:name/followers', ProfileFollows: '/profile/:name/follows', @@ -25,9 +26,12 @@ export const router = new Router({ ProfileFeed: '/profile/:name/feed/:rkey', ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by', ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by', + // debug Debug: '/sys/debug', DebugMod: '/sys/debug-mod', Log: '/sys/log', + // settings + LanguageSettings: '/settings/language', AppPasswords: '/settings/app-passwords', PreferencesFollowingFeed: '/settings/following-feed', PreferencesThreads: '/settings/threads', @@ -35,15 +39,24 @@ export const router = new Router({ AccessibilitySettings: '/settings/accessibility', AppearanceSettings: '/settings/appearance', SavedFeeds: '/settings/saved-feeds', + // new settings + AccountSettings: '/settings/account', + PrivacyAndSecuritySettings: '/settings/privacy-and-security', + ContentAndMediaSettings: '/settings/content-and-media', + AboutSettings: '/settings/about', + // support Support: '/support', PrivacyPolicy: '/support/privacy', TermsOfService: '/support/tos', CommunityGuidelines: '/support/community-guidelines', CopyrightPolicy: '/support/copyright', + // hashtags Hashtag: '/hashtag/:tag', + // DMs Messages: '/messages', MessagesSettings: '/messages/settings', MessagesConversation: '/messages/:conversation', + // starter packs Start: '/start/:name/:rkey', StarterPackEdit: '/starter-pack/edit/:rkey', StarterPack: '/starter-pack/:name/:rkey', diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 222774e05..d5a2daffd 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -7,6 +7,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' +import {IS_INTERNAL} from '#/lib/app-info' import {getLabelingServiceTitle} from '#/lib/moderation' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {logger} from '#/logger' @@ -469,18 +470,22 @@ export function ModerationScreenInner({ </View> )} - <Text - style={[ - a.text_md, - a.font_bold, - a.pt_2xl, - a.pb_md, - t.atoms.text_contrast_high, - ]}> - <Trans>Logged-out visibility</Trans> - </Text> + {!IS_INTERNAL && ( + <> + <Text + style={[ + a.text_md, + a.font_bold, + a.pt_2xl, + a.pb_md, + t.atoms.text_contrast_high, + ]}> + <Trans>Logged-out visibility</Trans> + </Text> - <PwiOptOut /> + <PwiOptOut /> + </> + )} <View style={{height: 200}} /> </ScrollView> diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx new file mode 100644 index 000000000..3c445b966 --- /dev/null +++ b/src/screens/Settings/AboutSettings.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import {Platform} from 'react-native' +import {setStringAsync} from 'expo-clipboard' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info' +import {STATUS_PAGE_URL} from '#/lib/constants' +import {CommonNavigatorParams} from '#/lib/routes/types' +import * as Toast from '#/view/com/util/Toast' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines' +import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' +import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/icons/Newspaper' +import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench' +import * as Layout from '#/components/Layout' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'AboutSettings'> +export function AboutSettingsScreen({}: Props) { + const {_} = useLingui() + + return ( + <Layout.Screen> + <Layout.Header title={_(msg`About`)} /> + <Layout.Content> + <SettingsList.Container> + <SettingsList.LinkItem + to="https://bsky.social/about/support/tos" + label={_(msg`Terms of Service`)}> + <SettingsList.ItemIcon icon={NewspaperIcon} /> + <SettingsList.ItemText> + <Trans>Terms of Service</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="https://bsky.social/about/support/privacy-policy" + label={_(msg`Privacy Policy`)}> + <SettingsList.ItemIcon icon={NewspaperIcon} /> + <SettingsList.ItemText> + <Trans>Privacy Policy</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to={STATUS_PAGE_URL} + label={_(msg`Status Page`)}> + <SettingsList.ItemIcon icon={GlobeIcon} /> + <SettingsList.ItemText> + <Trans>Status Page</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.Divider /> + <SettingsList.LinkItem to="/sys/log" label={_(msg`System log`)}> + <SettingsList.ItemIcon icon={CodeLinesIcon} /> + <SettingsList.ItemText> + <Trans>System log</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.PressableItem + label={_(msg`Version ${appVersion}`)} + accessibilityHint={_(msg`Copy build version to clipboard`)} + onPress={() => { + setStringAsync( + `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}`, + ) + Toast.show(_(msg`Copied build version to clipboard`)) + }}> + <SettingsList.ItemIcon icon={WrenchIcon} /> + <SettingsList.ItemText> + <Trans>Version {appVersion}</Trans> + </SettingsList.ItemText> + <SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText> + </SettingsList.PressableItem> + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/AccessibilitySettings.tsx b/src/screens/Settings/AccessibilitySettings.tsx new file mode 100644 index 000000000..dfe2c14a5 --- /dev/null +++ b/src/screens/Settings/AccessibilitySettings.tsx @@ -0,0 +1,113 @@ +import React from 'react' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {CommonNavigatorParams} from '#/lib/routes/types' +import {isNative} from '#/platform/detection' +import { + useHapticsDisabled, + useRequireAltTextEnabled, + useSetHapticsDisabled, + useSetRequireAltTextEnabled, +} from '#/state/preferences' +import { + useLargeAltBadgeEnabled, + useSetLargeAltBadgeEnabled, +} from '#/state/preferences/large-alt-badge' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import * as Toggle from '#/components/forms/Toggle' +import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' +import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' +import * as Layout from '#/components/Layout' +import {InlineLinkText} from '#/components/Link' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'AccessibilitySettings' +> +export function AccessibilitySettingsScreen({}: Props) { + const {_} = useLingui() + + const requireAltTextEnabled = useRequireAltTextEnabled() + const setRequireAltTextEnabled = useSetRequireAltTextEnabled() + const hapticsDisabled = useHapticsDisabled() + const setHapticsDisabled = useSetHapticsDisabled() + const largeAltBadgeEnabled = useLargeAltBadgeEnabled() + const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() + + return ( + <Layout.Screen> + <Layout.Header title={_(msg`Accessibility`)} /> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> + <SettingsList.ItemIcon icon={AccessibilityIcon} /> + <SettingsList.ItemText> + <Trans>Alt text</Trans> + </SettingsList.ItemText> + <Toggle.Item + name="require_alt_text" + label={_(msg`Require alt text before posting`)} + value={requireAltTextEnabled ?? false} + onChange={value => setRequireAltTextEnabled(value)} + style={[a.w_full]}> + <Toggle.LabelText style={[a.flex_1]}> + <Trans>Require alt text before posting</Trans> + </Toggle.LabelText> + <Toggle.Platform /> + </Toggle.Item> + <Toggle.Item + name="large_alt_badge" + label={_(msg`Display larger alt text badges`)} + value={!!largeAltBadgeEnabled} + onChange={value => setLargeAltBadgeEnabled(value)} + style={[a.w_full]}> + <Toggle.LabelText style={[a.flex_1]}> + <Trans>Display larger alt text badges</Trans> + </Toggle.LabelText> + <Toggle.Platform /> + </Toggle.Item> + </SettingsList.Group> + {isNative && ( + <> + <SettingsList.Divider /> + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> + <SettingsList.ItemIcon icon={HapticIcon} /> + <SettingsList.ItemText> + <Trans>Haptics</Trans> + </SettingsList.ItemText> + <Toggle.Item + name="haptics" + label={_(msg`Disable haptic feedback`)} + value={hapticsDisabled ?? false} + onChange={value => setHapticsDisabled(value)} + style={[a.w_full]}> + <Toggle.LabelText style={[a.flex_1]}> + <Trans>Disable haptic feedback</Trans> + </Toggle.LabelText> + <Toggle.Platform /> + </Toggle.Item> + </SettingsList.Group> + </> + )} + <SettingsList.Item> + <Admonition type="info" style={[a.flex_1]}> + <Trans> + Autoplay options have moved to the{' '} + <InlineLinkText + to="/settings/content-and-media" + label={_(msg`Content and media`)}> + Content and Media settings + </InlineLinkText> + . + </Trans> + </Admonition> + </SettingsList.Item> + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx new file mode 100644 index 000000000..19101d2f4 --- /dev/null +++ b/src/screens/Settings/AccountSettings.tsx @@ -0,0 +1,180 @@ +import React from 'react' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {useQueryClient} from '@tanstack/react-query' + +import {CommonNavigatorParams} from '#/lib/routes/types' +import {useModalControls} from '#/state/modals' +import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a, useTheme} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' +import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' +import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car' +import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' +import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' +import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze' +import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' +import {PencilLine_Stroke2_Corner2_Rounded as PencilIcon} from '#/components/icons/Pencil' +import {Trash_Stroke2_Corner2_Rounded} from '#/components/icons/Trash' +import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' +import * as Layout from '#/components/Layout' +import {DeactivateAccountDialog} from './components/DeactivateAccountDialog' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'> +export function AccountSettingsScreen({}: Props) { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const queryClient = useQueryClient() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) + const {openModal} = useModalControls() + const birthdayControl = useDialogControl() + const exportCarControl = useDialogControl() + const deactivateAccountControl = useDialogControl() + + return ( + <Layout.Screen> + <Layout.Header title={_(msg`Account`)} /> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item> + <SettingsList.ItemIcon icon={EnvelopeIcon} /> + <SettingsList.ItemText> + <Trans>Email</Trans> + </SettingsList.ItemText> + {currentAccount && ( + <> + <SettingsList.BadgeText> + {currentAccount.email || <Trans>(no email)</Trans>} + </SettingsList.BadgeText> + {currentAccount.emailConfirmed ? ( + <CheckIcon color={t.palette.positive_500} size="sm" /> + ) : ( + <SettingsList.BadgeButton + label={_(msg`Verify`)} + onPress={() => {}} + /> + )} + </> + )} + </SettingsList.Item> + <SettingsList.PressableItem + label={_(msg`Change email`)} + onPress={() => openModal({name: 'change-email'})}> + <SettingsList.ItemIcon icon={PencilIcon} /> + <SettingsList.ItemText> + <Trans>Change email</Trans> + </SettingsList.ItemText> + <SettingsList.Chevron /> + </SettingsList.PressableItem> + <SettingsList.LinkItem + to="/settings/privacy-and-security" + label={_(msg`Protect your account`)} + style={[ + a.my_xs, + a.mx_lg, + a.rounded_md, + {backgroundColor: t.palette.primary_50}, + ]} + chevronColor={t.palette.primary_500} + hoverStyle={[{backgroundColor: t.palette.primary_100}]} + contentContainerStyle={[a.rounded_md, a.px_lg]}> + <SettingsList.ItemIcon + icon={VerifiedIcon} + color={t.palette.primary_500} + /> + <SettingsList.ItemText + style={[{color: t.palette.primary_500}, a.font_bold]}> + <Trans>Protect your account</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.Divider /> + <SettingsList.Item> + <SettingsList.ItemIcon icon={BirthdayCakeIcon} /> + <SettingsList.ItemText> + <Trans>Birthday</Trans> + </SettingsList.ItemText> + <SettingsList.BadgeButton + label={_(msg`Edit`)} + onPress={() => birthdayControl.open()} + /> + </SettingsList.Item> + <SettingsList.PressableItem + label={_(msg`Password`)} + onPress={() => openModal({name: 'change-password'})}> + <SettingsList.ItemIcon icon={LockIcon} /> + <SettingsList.ItemText> + <Trans>Password</Trans> + </SettingsList.ItemText> + <SettingsList.Chevron /> + </SettingsList.PressableItem> + <SettingsList.PressableItem + label={_(msg`Handle`)} + onPress={() => + openModal({ + name: 'change-handle', + onChanged() { + if (currentAccount) { + // refresh my profile + queryClient.invalidateQueries({ + queryKey: RQKEY_PROFILE(currentAccount.did), + }) + } + }, + }) + }> + <SettingsList.ItemIcon icon={AtIcon} /> + <SettingsList.ItemText> + <Trans>Handle</Trans> + </SettingsList.ItemText> + {profile && ( + <SettingsList.BadgeText>@{profile.handle}</SettingsList.BadgeText> + )} + <SettingsList.Chevron /> + </SettingsList.PressableItem> + <SettingsList.Divider /> + <SettingsList.PressableItem + label={_(msg`Export my data`)} + onPress={() => exportCarControl.open()}> + <SettingsList.ItemIcon icon={CarIcon} /> + <SettingsList.ItemText> + <Trans>Export my data</Trans> + </SettingsList.ItemText> + <SettingsList.Chevron /> + </SettingsList.PressableItem> + <SettingsList.PressableItem + label={_(msg`Deactivate account`)} + onPress={() => deactivateAccountControl.open()} + destructive> + <SettingsList.ItemIcon icon={FreezeIcon} /> + <SettingsList.ItemText> + <Trans>Deactivate account</Trans> + </SettingsList.ItemText> + <SettingsList.Chevron /> + </SettingsList.PressableItem> + <SettingsList.PressableItem + label={_(msg`Delete account`)} + onPress={() => openModal({name: 'delete-account'})} + destructive> + <SettingsList.ItemIcon icon={Trash_Stroke2_Corner2_Rounded} /> + <SettingsList.ItemText> + <Trans>Delete account</Trans> + </SettingsList.ItemText> + <SettingsList.Chevron /> + </SettingsList.PressableItem> + </SettingsList.Container> + </Layout.Content> + + <BirthDateSettingsDialog control={birthdayControl} /> + <ExportCarDialog control={exportCarControl} /> + <DeactivateAccountDialog control={deactivateAccountControl} /> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index c317c930f..d0beb7d50 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -1,18 +1,15 @@ import React, {useCallback} from 'react' -import {View} from 'react-native' import Animated, { - FadeInDown, - FadeOutDown, + FadeInUp, + FadeOutUp, LayoutAnimationConfig, + LinearTransition, } from 'react-native-reanimated' -import {msg, Trans} from '@lingui/macro' +import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {useSetThemePrefs, useThemePrefs} from '#/state/shell' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {ScrollView} from '#/view/com/util/Views' import {atoms as a, native, useAlf, useTheme} from '#/alf' import * as ToggleButton from '#/components/forms/ToggleButton' import {Props as SVGIconProps} from '#/components/icons/common' @@ -22,12 +19,11 @@ import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/T import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' +import * as SettingsList from './components/SettingsList' type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> export function AppearanceSettingsScreen({}: Props) { - const t = useTheme() const {_} = useLingui() - const {isTabletOrMobile} = useWebMediaQueries() const {fonts} = useAlf() const {colorMode, darkTheme} = useThemePrefs() @@ -77,66 +73,54 @@ export function AppearanceSettingsScreen({}: Props) { return ( <LayoutAnimationConfig skipExiting skipEntering> <Layout.Screen testID="preferencesThreadsScreen"> - <ScrollView - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}} - contentContainerStyle={{paddingBottom: 75}}> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[t.atoms.border_contrast_medium, a.border_b]}> - <View style={a.flex_1}> - <Text style={[a.text_2xl, a.font_bold]}> - <Trans>Appearance</Trans> - </Text> - </View> - </SimpleViewHeader> - - <View style={[a.gap_3xl, a.pt_xl, a.px_xl]}> - <View style={[a.gap_lg]}> - <AppearanceToggleButtonGroup - title={_(msg`Color mode`)} - icon={PhoneIcon} - items={[ - { - label: _(msg`System`), - name: 'system', - }, - { - label: _(msg`Light`), - name: 'light', - }, - { - label: _(msg`Dark`), - name: 'dark', - }, - ]} - values={[colorMode]} - onChange={onChangeAppearance} - /> + <Layout.Header title={_(msg`Appearance`)} /> + <Layout.Content> + <SettingsList.Container> + <AppearanceToggleButtonGroup + title={_(msg`Color mode`)} + icon={PhoneIcon} + items={[ + { + label: _(msg`System`), + name: 'system', + }, + { + label: _(msg`Light`), + name: 'light', + }, + { + label: _(msg`Dark`), + name: 'dark', + }, + ]} + values={[colorMode]} + onChange={onChangeAppearance} + /> - {colorMode !== 'light' && ( - <Animated.View - entering={native(FadeInDown)} - exiting={native(FadeOutDown)}> - <AppearanceToggleButtonGroup - title={_(msg`Dark theme`)} - icon={MoonIcon} - items={[ - { - label: _(msg`Dim`), - name: 'dim', - }, - { - label: _(msg`Dark`), - name: 'dark', - }, - ]} - values={[darkTheme ?? 'dim']} - onChange={onChangeDarkTheme} - /> - </Animated.View> - )} + {colorMode !== 'light' && ( + <Animated.View + entering={native(FadeInUp)} + exiting={native(FadeOutUp)}> + <AppearanceToggleButtonGroup + title={_(msg`Dark theme`)} + icon={MoonIcon} + items={[ + { + label: _(msg`Dim`), + name: 'dim', + }, + { + label: _(msg`Dark`), + name: 'dark', + }, + ]} + values={[darkTheme ?? 'dim']} + onChange={onChangeDarkTheme} + /> + </Animated.View> + )} + <Animated.View layout={native(LinearTransition)}> <AppearanceToggleButtonGroup title={_(msg`Font`)} description={_( @@ -177,9 +161,9 @@ export function AppearanceSettingsScreen({}: Props) { values={[fonts.scale]} onChange={onChangeFontScale} /> - </View> - </View> - </ScrollView> + </Animated.View> + </SettingsList.Container> + </Layout.Content> </Layout.Screen> </LayoutAnimationConfig> ) @@ -205,29 +189,32 @@ export function AppearanceToggleButtonGroup({ }) { const t = useTheme() return ( - <View style={[a.gap_sm]}> - <View style={[a.gap_xs]}> - <View style={[a.flex_row, a.align_center, a.gap_md]}> - <Icon style={t.atoms.text} /> - <Text style={[a.text_md, a.font_bold]}>{title}</Text> - </View> + <> + <SettingsList.Group contentContainerStyle={[a.gap_sm]} iconInset={false}> + <SettingsList.ItemIcon icon={Icon} /> + <SettingsList.ItemText>{title}</SettingsList.ItemText> {description && ( <Text - style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> + style={[ + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + a.w_full, + ]}> {description} </Text> )} - </View> - <ToggleButton.Group label={title} values={values} onChange={onChange}> - {items.map(item => ( - <ToggleButton.Button - key={item.name} - label={item.label} - name={item.name}> - <ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText> - </ToggleButton.Button> - ))} - </ToggleButton.Group> - </View> + <ToggleButton.Group label={title} values={values} onChange={onChange}> + {items.map(item => ( + <ToggleButton.Button + key={item.name} + label={item.label} + name={item.name}> + <ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText> + </ToggleButton.Button> + ))} + </ToggleButton.Group> + </SettingsList.Group> + </> ) } diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx new file mode 100644 index 000000000..79c8a48f3 --- /dev/null +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {CommonNavigatorParams} from '#/lib/routes/types' +import {isNative} from '#/platform/detection' +import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' +import { + useInAppBrowser, + useSetInAppBrowser, +} from '#/state/preferences/in-app-browser' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import * as Toggle from '#/components/forms/Toggle' +import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' +import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag' +import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home' +import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh' +import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' +import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' +import * as Layout from '#/components/Layout' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'ContentAndMediaSettings' +> +export function ContentAndMediaSettingsScreen({}: Props) { + const {_} = useLingui() + const autoplayDisabledPref = useAutoplayDisabled() + const setAutoplayDisabledPref = useSetAutoplayDisabled() + const inAppBrowserPref = useInAppBrowser() + const setUseInAppBrowser = useSetInAppBrowser() + + return ( + <Layout.Screen> + <Layout.Header title={_(msg`Content and Media`)} /> + <Layout.Content> + <SettingsList.Container> + <SettingsList.LinkItem + to="/settings/saved-feeds" + label={_(msg`Manage saved feeds`)}> + <SettingsList.ItemIcon icon={HashtagIcon} /> + <SettingsList.ItemText> + <Trans>Manage saved feeds</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/threads" + label={_(msg`Thread preferences`)}> + <SettingsList.ItemIcon icon={BubblesIcon} /> + <SettingsList.ItemText> + <Trans>Thread preferences</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/following-feed" + label={_(msg`Following feed preferences`)}> + <SettingsList.ItemIcon icon={HomeIcon} /> + <SettingsList.ItemText> + <Trans>Following feed preferences</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/external-embeds" + label={_(msg`External media`)}> + <SettingsList.ItemIcon icon={MacintoshIcon} /> + <SettingsList.ItemText> + <Trans>External media</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.Divider /> + {isNative && ( + <Toggle.Item + name="use_in_app_browser" + label={_(msg`Use in-app browser to open links`)} + value={inAppBrowserPref ?? false} + onChange={value => setUseInAppBrowser(value)}> + <SettingsList.Item> + <SettingsList.ItemIcon icon={WindowIcon} /> + <SettingsList.ItemText> + <Trans>Use in-app browser to open links</Trans> + </SettingsList.ItemText> + <Toggle.Platform /> + </SettingsList.Item> + </Toggle.Item> + )} + <Toggle.Item + name="disable_autoplay" + label={_(msg`Disable autoplay for videos and GIFs`)} + value={autoplayDisabledPref} + onChange={value => setAutoplayDisabledPref(value)}> + <SettingsList.Item> + <SettingsList.ItemIcon icon={PlayIcon} /> + <SettingsList.ItemText> + <Trans>Disable autoplay for videos and GIFs</Trans> + </SettingsList.ItemText> + <Toggle.Platform /> + </SettingsList.Item> + </Toggle.Item> + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/PrivacyAndSecuritySettings.tsx b/src/screens/Settings/PrivacyAndSecuritySettings.tsx new file mode 100644 index 000000000..da462c90d --- /dev/null +++ b/src/screens/Settings/PrivacyAndSecuritySettings.tsx @@ -0,0 +1,91 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {CommonNavigatorParams} from '#/lib/routes/types' +import {useAppPasswordsQuery} from '#/state/queries/app-passwords' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import * as Admonition from '#/components/Admonition' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' +import {Key_Stroke2_Corner2_Rounded as KeyIcon} from '#/components/icons/Key' +import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' +import * as Layout from '#/components/Layout' +import {InlineLinkText} from '#/components/Link' +import {Email2FAToggle} from './components/Email2FAToggle' +import {PwiOptOut} from './components/PwiOptOut' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'PrivacyAndSecuritySettings' +> +export function PrivacyAndSecuritySettingsScreen({}: Props) { + const {_} = useLingui() + const {data: appPasswords} = useAppPasswordsQuery() + return ( + <Layout.Screen> + <Layout.Header title={_(msg`Privacy and Security`)} /> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item> + <SettingsList.ItemIcon icon={VerifiedIcon} /> + <SettingsList.ItemText> + <Trans>Two-factor authentication (2FA)</Trans> + </SettingsList.ItemText> + <Email2FAToggle /> + </SettingsList.Item> + <SettingsList.LinkItem + to="/settings/app-passwords" + label={_(msg`App passwords`)}> + <SettingsList.ItemIcon icon={KeyIcon} /> + <SettingsList.ItemText> + <Trans>App passwords</Trans> + </SettingsList.ItemText> + {appPasswords && appPasswords.length > 0 && ( + <SettingsList.BadgeText> + {appPasswords.length} + </SettingsList.BadgeText> + )} + </SettingsList.LinkItem> + <SettingsList.Divider /> + <SettingsList.Group> + <SettingsList.ItemIcon icon={EyeSlashIcon} /> + <SettingsList.ItemText> + <Trans>Logged-out visibility</Trans> + </SettingsList.ItemText> + <PwiOptOut /> + </SettingsList.Group> + <SettingsList.Item> + <Admonition.Outer type="tip" style={[a.flex_1]}> + <Admonition.Row> + <Admonition.Icon /> + <View style={[a.flex_1, a.gap_sm]}> + <Admonition.Text> + <Trans> + Note: Bluesky is an open and public network. This setting + only limits the visibility of your content on the Bluesky + app and website, and other apps may not respect this + setting. Your content may still be shown to logged-out + users by other apps and websites. + </Trans> + </Admonition.Text> + <Admonition.Text> + <InlineLinkText + label={_( + msg`Learn more about what is public on Bluesky.`, + )} + to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> + <Trans>Learn more about what is public on Bluesky.</Trans> + </InlineLinkText> + </Admonition.Text> + </View> + </Admonition.Row> + </Admonition.Outer> + </SettingsList.Item> + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx new file mode 100644 index 000000000..789ffb56f --- /dev/null +++ b/src/screens/Settings/Settings.tsx @@ -0,0 +1,282 @@ +import React from 'react' +import {View} from 'react-native' +import {Linking} from 'react-native' +import {AppBskyActorDefs, moderateProfile} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {HELP_DESK_URL} from '#/lib/constants' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' +import {useSession, useSessionApi} from '#/state/session' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {ProfileHeaderDisplayName} from '#/screens/Profile/Header/DisplayName' +import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a, useTheme} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' +import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' +import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' +import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' +import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' +import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' +import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' +import { + Person_Stroke2_Corner2_Rounded as PersonIcon, + PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon, +} from '#/components/icons/Person' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' +import * as Layout from '#/components/Layout' +import * as Prompt from '#/components/Prompt' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> +export function SettingsScreen({}: Props) { + const {_} = useLingui() + const {logoutEveryAccount} = useSessionApi() + const {accounts, currentAccount} = useSession() + const switchAccountControl = useDialogControl() + const signOutPromptControl = Prompt.usePromptControl() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) + const {setShowLoggedOut} = useLoggedOutViewControls() + const closeEverything = useCloseAllActiveElements() + + const onAddAnotherAccount = () => { + setShowLoggedOut(true) + closeEverything() + } + + return ( + <Layout.Screen> + <Layout.Header title={_(msg`Settings`)} /> + <Layout.Content> + <SettingsList.Container> + <View + style={[ + a.px_xl, + a.pb_md, + a.w_full, + a.gap_2xs, + a.align_center, + {minHeight: 160}, + ]}> + {profile && <ProfilePreview profile={profile} />} + </View> + <SettingsList.PressableItem + label={ + accounts.length > 1 + ? _(msg`Switch account`) + : _(msg`Add another account`) + } + onPress={() => + accounts.length > 1 + ? switchAccountControl.open() + : onAddAnotherAccount() + }> + <SettingsList.ItemIcon icon={PersonGroupIcon} /> + <SettingsList.ItemText> + {accounts.length > 1 ? ( + <Trans>Switch account</Trans> + ) : ( + <Trans>Add another account</Trans> + )} + </SettingsList.ItemText> + {accounts.length > 1 && ( + <AvatarStack + profiles={accounts + .map(acc => acc.did) + .filter(did => did !== currentAccount?.did) + .slice(0, 5)} + /> + )} + </SettingsList.PressableItem> + <SettingsList.Divider /> + <SettingsList.LinkItem to="/settings/account" label={_(msg`Account`)}> + <SettingsList.ItemIcon icon={PersonIcon} /> + <SettingsList.ItemText> + <Trans>Account</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/privacy-and-security" + label={_(msg`Privacy and security`)}> + <SettingsList.ItemIcon icon={LockIcon} /> + <SettingsList.ItemText> + <Trans>Privacy and security</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/moderation" label={_(msg`Moderation`)}> + <SettingsList.ItemIcon icon={HandIcon} /> + <SettingsList.ItemText> + <Trans>Moderation</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/content-and-media" + label={_(msg`Content and media`)}> + <SettingsList.ItemIcon icon={WindowIcon} /> + <SettingsList.ItemText> + <Trans>Content and media</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/appearance" + label={_(msg`Appearance`)}> + <SettingsList.ItemIcon icon={PaintRollerIcon} /> + <SettingsList.ItemText> + <Trans>Appearance</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/accessibility" + label={_(msg`Accessibility`)}> + <SettingsList.ItemIcon icon={AccessibilityIcon} /> + <SettingsList.ItemText> + <Trans>Accessibility</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings/language" + label={_(msg`Languages`)}> + <SettingsList.ItemIcon icon={EarthIcon} /> + <SettingsList.ItemText> + <Trans>Languages</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.PressableItem + onPress={() => Linking.openURL(HELP_DESK_URL)} + label={_(msg`Help`)} + accessibilityHint={_(msg`Open helpdesk in browser`)}> + <SettingsList.ItemIcon icon={CircleQuestionIcon} /> + <SettingsList.ItemText> + <Trans>Help</Trans> + </SettingsList.ItemText> + <SettingsList.Chevron /> + </SettingsList.PressableItem> + <SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}> + <SettingsList.ItemIcon icon={BubbleInfoIcon} /> + <SettingsList.ItemText> + <Trans>About</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.Divider /> + <SettingsList.PressableItem + destructive + onPress={() => signOutPromptControl.open()} + label={_(msg`Sign out`)}> + <SettingsList.ItemText> + <Trans>Sign out</Trans> + </SettingsList.ItemText> + </SettingsList.PressableItem> + </SettingsList.Container> + </Layout.Content> + + <Prompt.Basic + control={signOutPromptControl} + title={_(msg`Sign out?`)} + description={_(msg`You will be signed out of all your accounts.`)} + onConfirm={() => logoutEveryAccount('Settings')} + confirmButtonCta={_(msg`Sign out`)} + cancelButtonCta={_(msg`Cancel`)} + confirmButtonColor="negative" + /> + + <SwitchAccountDialog control={switchAccountControl} /> + </Layout.Screen> + ) +} + +function ProfilePreview({ + profile, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed +}) { + const shadow = useProfileShadow(profile) + const moderationOpts = useModerationOpts() + + if (!moderationOpts) return null + + const moderation = moderateProfile(profile, moderationOpts) + + return ( + <> + <UserAvatar + size={80} + avatar={shadow.avatar} + moderation={moderation.ui('avatar')} + /> + <ProfileHeaderDisplayName profile={shadow} moderation={moderation} /> + <ProfileHeaderHandle profile={shadow} /> + </> + ) +} + +const AVI_SIZE = 26 +const HALF_AVI_SIZE = AVI_SIZE / 2 + +function AvatarStack({profiles}: {profiles: string[]}) { + const {data, error} = useProfilesQuery({handles: profiles}) + const t = useTheme() + const moderationOpts = useModerationOpts() + + if (error) { + console.error(error) + return null + } + + const isPending = !data || !moderationOpts + + const items = isPending + ? Array.from({length: profiles.length}).map((_, i) => ({ + key: i, + profile: null, + moderation: null, + })) + : data.profiles.map(item => ({ + key: item.did, + profile: item, + moderation: moderateProfile(item, moderationOpts), + })) + + return ( + <View + style={[ + a.flex_row, + a.align_center, + a.relative, + {width: AVI_SIZE + (items.length - 1) * HALF_AVI_SIZE}, + ]}> + {items.map((item, i) => ( + <View + key={item.key} + style={[ + t.atoms.bg_contrast_25, + a.relative, + { + width: AVI_SIZE, + height: AVI_SIZE, + left: i * -HALF_AVI_SIZE, + borderWidth: 1, + borderColor: t.atoms.bg.backgroundColor, + borderRadius: 999, + zIndex: 3 - i, + }, + ]}> + {item.profile && ( + <UserAvatar + size={AVI_SIZE - 2} + avatar={item.profile.avatar} + moderation={item.moderation.ui('avatar')} + /> + )} + </View> + ))} + </View> + ) +} diff --git a/src/screens/Settings/components/Email2FAToggle.tsx b/src/screens/Settings/components/Email2FAToggle.tsx new file mode 100644 index 000000000..d89e5f18e --- /dev/null +++ b/src/screens/Settings/components/Email2FAToggle.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useModalControls} from '#/state/modals' +import {useAgent, useSession} from '#/state/session' +import {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog' +import {useDialogControl} from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' +import * as SettingsList from './SettingsList' + +export function Email2FAToggle() { + const {_} = useLingui() + const {currentAccount} = useSession() + const {openModal} = useModalControls() + const disableDialogControl = useDialogControl() + const enableDialogControl = useDialogControl() + const agent = useAgent() + + const enableEmailAuthFactor = React.useCallback(async () => { + if (currentAccount?.email) { + await agent.com.atproto.server.updateEmail({ + email: currentAccount.email, + emailAuthFactor: true, + }) + await agent.resumeSession(agent.session!) + } + }, [currentAccount, agent]) + + const onToggle = React.useCallback(() => { + if (!currentAccount) { + return + } + if (currentAccount.emailAuthFactor) { + disableDialogControl.open() + } else { + if (!currentAccount.emailConfirmed) { + openModal({ + name: 'verify-email', + onSuccess: enableDialogControl.open, + }) + return + } + enableDialogControl.open() + } + }, [currentAccount, enableDialogControl, openModal, disableDialogControl]) + + return ( + <> + <DisableEmail2FADialog control={disableDialogControl} /> + <Prompt.Basic + control={enableDialogControl} + title={_(msg`Enable Email 2FA`)} + description={_(msg`Require an email code to log in to your account.`)} + onConfirm={enableEmailAuthFactor} + confirmButtonCta={_(msg`Enable`)} + /> + <SettingsList.BadgeButton + label={ + currentAccount?.emailAuthFactor ? _(msg`Disable`) : _(msg`Enable`) + } + onPress={onToggle} + /> + </> + ) +} diff --git a/src/screens/Settings/components/PwiOptOut.tsx b/src/screens/Settings/components/PwiOptOut.tsx new file mode 100644 index 000000000..4339ade9b --- /dev/null +++ b/src/screens/Settings/components/PwiOptOut.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import {View} from 'react-native' +import {ComAtprotoLabelDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import { + useProfileQuery, + useProfileUpdateMutation, +} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' +import {Text} from '#/components/Typography' + +export function PwiOptOut() { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) + const updateProfile = useProfileUpdateMutation() + + const isOptedOut = + profile?.labels?.some(l => l.val === '!no-unauthenticated') || false + const canToggle = profile && !updateProfile.isPending + + const onToggleOptOut = React.useCallback(() => { + if (!profile) { + return + } + let wasAdded = false + updateProfile.mutate({ + profile, + updates: existing => { + // create labels attr if needed + existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels) + ? existing.labels + : { + $type: 'com.atproto.label.defs#selfLabels', + values: [], + } + + // toggle the label + const hasLabel = existing.labels.values.some( + l => l.val === '!no-unauthenticated', + ) + if (hasLabel) { + wasAdded = false + existing.labels.values = existing.labels.values.filter( + l => l.val !== '!no-unauthenticated', + ) + } else { + wasAdded = true + existing.labels.values.push({val: '!no-unauthenticated'}) + } + + // delete if no longer needed + if (existing.labels.values.length === 0) { + delete existing.labels + } + return existing + }, + checkCommitted: res => { + const exists = !!res.data.labels?.some( + l => l.val === '!no-unauthenticated', + ) + return exists === wasAdded + }, + }) + }, [updateProfile, profile]) + + return ( + <View style={[a.flex_1, a.gap_sm]}> + <Toggle.Item + name="logged_out_visibility" + disabled={!canToggle || updateProfile.isPending} + value={isOptedOut} + onChange={onToggleOptOut} + label={_( + msg`Discourage apps from showing my account to logged-out users`, + )} + style={[a.w_full]}> + <Toggle.LabelText style={[a.flex_1]}> + <Trans> + Discourage apps from showing my account to logged-out users + </Trans> + </Toggle.LabelText> + <Toggle.Platform /> + </Toggle.Item> + + <Text style={[a.leading_snug, t.atoms.text_contrast_high]}> + <Trans> + Bluesky will not show your profile and posts to logged-out users. + Other apps may not honor this request. This does not make your account + private. + </Trans> + </Text> + </View> + ) +} diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx new file mode 100644 index 000000000..86f8040af --- /dev/null +++ b/src/screens/Settings/components/SettingsList.tsx @@ -0,0 +1,300 @@ +import React, {useContext, useMemo} from 'react' +import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native' + +import {HITSLOP_10} from '#/lib/constants' +import {atoms as a, useTheme} from '#/alf' +import * as Button from '#/components/Button' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' +import {Link, LinkProps} from '#/components/Link' +import {createPortalGroup} from '#/components/Portal' +import {Text} from '#/components/Typography' + +const ItemContext = React.createContext({ + destructive: false, + withinGroup: false, +}) + +const Portal = createPortalGroup() + +export function Container({children}: {children: React.ReactNode}) { + return <View style={[a.flex_1, a.py_lg]}>{children}</View> +} + +/** + * This uses `Portal` magic ✨ to render the icons and title correctly. ItemIcon and ItemText components + * get teleported to the top row, leaving the rest of the children in the bottom row. + */ +export function Group({ + children, + destructive = false, + iconInset = true, + style, + contentContainerStyle, +}: { + children: React.ReactNode + destructive?: boolean + iconInset?: boolean + style?: StyleProp<ViewStyle> + contentContainerStyle?: StyleProp<ViewStyle> +}) { + const context = useMemo( + () => ({destructive, withinGroup: true}), + [destructive], + ) + return ( + <View style={[a.w_full, style]}> + <Portal.Provider> + <ItemContext.Provider value={context}> + <Item style={[a.pb_2xs, {minHeight: 42}]}> + <Portal.Outlet /> + </Item> + <Item + style={[ + a.flex_col, + a.pt_2xs, + a.align_start, + a.gap_0, + contentContainerStyle, + ]} + iconInset={iconInset}> + {children} + </Item> + </ItemContext.Provider> + </Portal.Provider> + </View> + ) +} + +export function Item({ + children, + destructive, + iconInset = false, + style, +}: { + children?: React.ReactNode + destructive?: boolean + /** + * Adds left padding so that the content will be aligned with other Items that contain icons + * @default false + */ + iconInset?: boolean + style?: StyleProp<ViewStyle> +}) { + const context = useContext(ItemContext) + const childContext = useMemo(() => { + if (typeof destructive !== 'boolean') return context + return {...context, destructive} + }, [context, destructive]) + return ( + <View + style={[ + a.px_xl, + a.py_sm, + a.align_center, + a.gap_md, + a.w_full, + a.flex_row, + {minHeight: 48}, + iconInset && { + paddingLeft: + // existing padding + a.pl_xl.paddingLeft + + // icon + 28 + + // gap + a.gap_md.gap, + }, + style, + ]}> + <ItemContext.Provider value={childContext}> + {children} + </ItemContext.Provider> + </View> + ) +} + +export function LinkItem({ + children, + destructive = false, + contentContainerStyle, + chevronColor, + ...props +}: LinkProps & { + contentContainerStyle?: StyleProp<ViewStyle> + destructive?: boolean + chevronColor?: string +}) { + const t = useTheme() + + return ( + <Link color="secondary" {...props}> + {args => ( + <Item + destructive={destructive} + style={[ + (args.hovered || args.pressed) && [t.atoms.bg_contrast_25], + contentContainerStyle, + ]}> + {typeof children === 'function' ? children(args) : children} + <Chevron color={chevronColor} /> + </Item> + )} + </Link> + ) +} + +export function PressableItem({ + children, + destructive = false, + contentContainerStyle, + hoverStyle, + ...props +}: Button.ButtonProps & { + contentContainerStyle?: StyleProp<ViewStyle> + destructive?: boolean +}) { + const t = useTheme() + return ( + <Button.Button {...props}> + {args => ( + <Item + destructive={destructive} + style={[ + (args.hovered || args.pressed) && [ + t.atoms.bg_contrast_25, + hoverStyle, + ], + contentContainerStyle, + ]}> + {typeof children === 'function' ? children(args) : children} + </Item> + )} + </Button.Button> + ) +} + +export function ItemIcon({ + icon: Comp, + size = 'xl', + color: colorProp, +}: Omit<React.ComponentProps<typeof Button.ButtonIcon>, 'position'> & { + color?: string +}) { + const t = useTheme() + const {destructive, withinGroup} = useContext(ItemContext) + + /* + * Copied here from icons/common.tsx so we can tweak if we need to, but + * also so that we can calculate transforms. + */ + const iconSize = { + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, + '2xl': 32, + }[size] + + const color = + colorProp ?? (destructive ? t.palette.negative_500 : t.atoms.text.color) + + const content = ( + <View style={[a.z_20, {width: iconSize, height: iconSize}]}> + <Comp width={iconSize} style={[{color}]} /> + </View> + ) + + if (withinGroup) { + return <Portal.Portal>{content}</Portal.Portal> + } else { + return content + } +} + +export function ItemText({ + // eslint-disable-next-line react/prop-types + style, + ...props +}: React.ComponentProps<typeof Button.ButtonText>) { + const t = useTheme() + const {destructive, withinGroup} = useContext(ItemContext) + + const content = ( + <Button.ButtonText + style={[ + a.text_md, + a.font_normal, + a.text_left, + a.flex_1, + destructive ? {color: t.palette.negative_500} : t.atoms.text, + style, + ]} + {...props} + /> + ) + + if (withinGroup) { + return <Portal.Portal>{content}</Portal.Portal> + } else { + return content + } +} + +export function Divider() { + const t = useTheme() + return ( + <View + style={[a.border_t, t.atoms.border_contrast_medium, a.w_full, a.my_sm]} + /> + ) +} + +export function Chevron({color: colorProp}: {color?: string}) { + const {destructive} = useContext(ItemContext) + const t = useTheme() + const color = + colorProp ?? (destructive ? t.palette.negative_500 : t.palette.contrast_500) + return <ItemIcon icon={ChevronRightIcon} size="md" color={color} /> +} + +export function BadgeText({children}: {children: React.ReactNode}) { + const t = useTheme() + return ( + <Text + style={[ + t.atoms.text_contrast_low, + a.text_md, + a.text_right, + a.leading_snug, + ]} + numberOfLines={1}> + {children} + </Text> + ) +} + +export function BadgeButton({ + label, + onPress, +}: { + label: string + onPress: (evt: GestureResponderEvent) => void +}) { + const t = useTheme() + return ( + <Button.Button label={label} onPress={onPress} hitSlop={HITSLOP_10}> + {({pressed}) => ( + <Button.ButtonText + style={[ + a.text_md, + a.font_normal, + a.text_right, + {color: pressed ? t.palette.contrast_300 : t.palette.primary_500}, + ]}> + {label} + </Button.ButtonText> + )} + </Button.Button> + ) +} diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index 6dd248ca7..b865d7bbf 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -10,15 +10,15 @@ import {LinearGradient} from 'expo-linear-gradient' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {cleanError} from '#/lib/strings/errors' +import {colors, gradients, s} from '#/lib/styles' +import {useTheme} from '#/lib/ThemeContext' +import {isAndroid, isWeb} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' import {useAgent, useSession, useSessionApi} from '#/state/session' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {cleanError} from 'lib/strings/errors' -import {colors, gradients, s} from 'lib/styles' -import {useTheme} from 'lib/ThemeContext' -import {isAndroid, isWeb} from 'platform/detection' import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' import {atoms as a, useTheme as useNewTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -210,6 +210,7 @@ export function Component({}: {}) { to="#" onPress={e => { e.preventDefault() + closeModal() deactivateAccountControl.open() return false }}> diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx index bf9f5fcb5..4dd5aa97b 100644 --- a/src/view/screens/AccessibilitySettings.tsx +++ b/src/view/screens/AccessibilitySettings.tsx @@ -4,6 +4,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' +import {IS_INTERNAL} from '#/lib/app-info' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' @@ -26,6 +27,7 @@ import {ToggleButton} from '#/view/com/util/forms/ToggleButton' import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' import {Text} from '#/view/com/util/text/Text' import {ScrollView} from '#/view/com/util/Views' +import {AccessibilitySettingsScreen as NewAccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' import {atoms as a} from '#/alf' import * as Layout from '#/components/Layout' @@ -33,7 +35,15 @@ type Props = NativeStackScreenProps< CommonNavigatorParams, 'AccessibilitySettings' > -export function AccessibilitySettingsScreen({}: Props) { +export function AccessibilitySettingsScreen(props: Props) { + return IS_INTERNAL ? ( + <NewAccessibilitySettingsScreen {...props} /> + ) : ( + <LegacyAccessibilitySettingsScreen {...props} /> + ) +} + +function LegacyAccessibilitySettingsScreen({}: Props) { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const {isMobile, isTabletOrMobile} = useWebMediaQueries() diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/view/screens/Settings/DisableEmail2FADialog.tsx index e4341fcd2..e3d088517 100644 --- a/src/view/screens/Settings/DisableEmail2FADialog.tsx +++ b/src/view/screens/Settings/DisableEmail2FADialog.tsx @@ -108,7 +108,11 @@ export function DisableEmail2FADialog({ {error ? <ErrorMessage message={error} /> : undefined} {stage === Stages.Email ? ( - <View style={gtMobile && [a.flex_row, a.justify_end, a.gap_md]}> + <View + style={[ + a.gap_sm, + gtMobile && [a.flex_row, a.justify_end, a.gap_md], + ]}> <Button testID="sendEmailButton" variant="solid" @@ -157,7 +161,11 @@ export function DisableEmail2FADialog({ /> </TextField.Root> </View> - <View style={gtMobile && [a.flex_row, a.justify_end]}> + <View + style={[ + a.gap_sm, + gtMobile && [a.flex_row, a.justify_end, a.gap_md], + ]}> <Button testID="resendCodeBtn" variant="ghost" diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx index 1d8d26471..2de3895d3 100644 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ b/src/view/screens/Settings/ExportCarDialog.tsx @@ -10,6 +10,7 @@ import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' @@ -76,6 +77,7 @@ export function ExportCarDialog({ label={_(msg`Download CAR file`)} disabled={loading} onPress={download}> + <ButtonIcon icon={DownloadIcon} /> <ButtonText> <Trans>Download CAR file</Trans> </ButtonText> diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index ce21a043b..7ec7b5dce 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -18,7 +18,7 @@ import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' -import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info' +import {appVersion, BUNDLE_DATE, bundleInfo, IS_INTERNAL} from '#/lib/app-info' import {STATUS_PAGE_URL} from '#/lib/constants' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useCustomPalette} from '#/lib/hooks/useCustomPalette' @@ -53,6 +53,7 @@ import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import {ScrollView} from '#/view/com/util/Views' import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' +import {SettingsScreen as NewSettingsScreen} from '#/screens/Settings/Settings' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' @@ -137,7 +138,15 @@ function SettingsAccountCard({ } type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> -export function SettingsScreen({}: Props) { +export function SettingsScreen(props: Props) { + return IS_INTERNAL ? ( + <NewSettingsScreen {...props} /> + ) : ( + <LegacySettingsScreen {...props} /> + ) +} + +function LegacySettingsScreen({}: Props) { const queryClient = useQueryClient() const pal = usePalette('default') const {_} = useLingui() diff --git a/src/view/screens/Storybook/Settings.tsx b/src/view/screens/Storybook/Settings.tsx new file mode 100644 index 000000000..6bc293c73 --- /dev/null +++ b/src/view/screens/Storybook/Settings.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import {View} from 'react-native' + +import * as Toast from '#/view/com/util/Toast' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a, useTheme} from '#/alf' +import {Alien_Stroke2_Corner0_Rounded as AlienIcon} from '#/components/icons/Alien' +import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' +import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' +import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' +import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' +import {Explosion_Stroke2_Corner0_Rounded as ExplosionIcon} from '#/components/icons/Explosion' +import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' +import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' +import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' +import {Pizza_Stroke2_Corner0_Rounded as PizzaIcon} from '#/components/icons/Pizza' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' +import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' +import {Text} from '#/components/Typography' + +export function Settings() { + const t = useTheme() + return ( + <View style={{marginLeft: -20, marginRight: -20}}> + <Text style={{marginLeft: 20, paddingBottom: 12}}>Settings</Text> + <SettingsList.LinkItem to="/settings" label="Account"> + <SettingsList.ItemIcon icon={PersonIcon} /> + <SettingsList.ItemText>Account</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/settings" label="Privacy and security"> + <SettingsList.ItemIcon icon={PaintRollerIcon} /> + <SettingsList.ItemText>Privacy and security</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/settings" label="Moderation"> + <SettingsList.ItemIcon icon={HandIcon} /> + <SettingsList.ItemText>Moderation</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/settings" label="Content and media"> + <SettingsList.ItemIcon icon={WindowIcon} /> + <SettingsList.ItemText>Content and media</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem + to="/settings" + label="Accessibility and appearance"> + <SettingsList.ItemIcon icon={PaintRollerIcon} /> + <SettingsList.ItemText> + Accessibilty and appearance + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/settings" label="Languages"> + <SettingsList.ItemIcon icon={EarthIcon} /> + <SettingsList.ItemText>Languages</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/settings" label="Help"> + <SettingsList.ItemIcon icon={CircleQuestionIcon} /> + <SettingsList.ItemText>Help</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/settings" label="About"> + <SettingsList.ItemIcon icon={BubbleInfoIcon} /> + <SettingsList.ItemText>About</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.Divider /> + <SettingsList.PressableItem + destructive + onPress={() => Toast.show('Sign out pressed')} + label="Sign out"> + <SettingsList.ItemText>Sign out</SettingsList.ItemText> + </SettingsList.PressableItem> + <SettingsList.Item style={[a.mt_xl]}> + <SettingsList.ItemIcon icon={PizzaIcon} /> + <SettingsList.ItemText>Not pressable</SettingsList.ItemText> + </SettingsList.Item> + <SettingsList.PressableItem + onPress={() => Toast.show('Pressable pressed')} + label="Pressable"> + <SettingsList.ItemIcon icon={AlienIcon} /> + <SettingsList.ItemText>Pressable</SettingsList.ItemText> + </SettingsList.PressableItem> + <SettingsList.LinkItem + to="/settings" + label="Destructive link" + destructive> + <SettingsList.ItemIcon icon={ExplosionIcon} /> + <SettingsList.ItemText>Destructive link</SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.PressableItem + label="Email" + onPress={() => Toast.show('Email change dialog goes here')}> + <SettingsList.ItemIcon icon={EnvelopeIcon} /> + <SettingsList.ItemText>Email</SettingsList.ItemText> + <SettingsList.BadgeText>hello@example.com</SettingsList.BadgeText> + </SettingsList.PressableItem> + <SettingsList.PressableItem + onPress={() => Toast.show('Pressable pressed')} + label="Protect your account" + style={[ + a.my_sm, + a.mx_lg, + a.rounded_md, + {backgroundColor: t.palette.primary_50}, + ]} + hoverStyle={[{backgroundColor: t.palette.primary_100}]} + contentContainerStyle={[a.rounded_md, a.px_lg]}> + <SettingsList.ItemIcon + icon={VerifiedIcon} + color={t.palette.primary_500} + /> + <SettingsList.ItemText + style={[{color: t.palette.primary_500}, a.font_bold]}> + Protect your account + </SettingsList.ItemText> + <SettingsList.Chevron color={t.palette.primary_500} /> + </SettingsList.PressableItem> + <SettingsList.Divider /> + <SettingsList.Item> + <SettingsList.ItemIcon icon={BirthdayCakeIcon} /> + <SettingsList.ItemText>Birthday</SettingsList.ItemText> + <SettingsList.BadgeButton + label="Edit" + onPress={() => Toast.show('Show edit birthday dialog')} + /> + </SettingsList.Item> + <SettingsList.LinkItem to="/settings" label="Long test"> + <SettingsList.ItemIcon icon={ExplosionIcon} /> + <SettingsList.ItemText> + long long long long long long long long long long long long long long + long long long long long long long long long long long long long long + long long long long long long long long long + </SettingsList.ItemText> + </SettingsList.LinkItem> + </View> + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index f7ac11ffc..de3d46533 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -18,6 +18,7 @@ import {Forms} from './Forms' import {Icons} from './Icons' import {Links} from './Links' import {Menus} from './Menus' +import {Settings} from './Settings' import {Shadows} from './Shadows' import {Spacing} from './Spacing' import {Theming} from './Theming' @@ -101,6 +102,8 @@ function StorybookInner() { <Admonitions /> + <Settings /> + <ThemeProvider theme="light"> <Theming /> </ThemeProvider> |