merge main
@@ -33,6 +33,7 @@ module.exports = {
|
||||
],
|
||||
'bsky-internal/use-exact-imports': 'error',
|
||||
'bsky-internal/use-typed-gates': 'error',
|
||||
'bsky-internal/use-prefixed-imports': 'warn',
|
||||
'simple-import-sort/imports': [
|
||||
'warn',
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
|
||||
- name: Check
|
||||
run: cd bskyweb/ && make check
|
||||
- name: Build (binary)
|
||||
@@ -38,6 +38,6 @@ jobs:
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
|
||||
- name: Lint
|
||||
run: cd bskyweb/ && make lint
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Ignore everything except JS-ey code.
|
||||
# Ignore everything except JS-ey or CSS code.
|
||||
# Based on https://stackoverflow.com/a/70715829/458193
|
||||
*
|
||||
!**/*.js
|
||||
@@ -7,6 +7,8 @@
|
||||
!**/*.tsx
|
||||
!*/
|
||||
|
||||
!**/*.css
|
||||
|
||||
# More specific ignores go below.
|
||||
.expo
|
||||
android
|
||||
|
||||
@@ -40,6 +40,7 @@ RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bs
|
||||
|
||||
# hack around issue with empty directory and go:embed
|
||||
RUN touch bskyweb/static/js/empty.txt
|
||||
RUN touch bskyweb/static/css/empty.txt
|
||||
RUN touch bskyweb/static/media/empty.txt
|
||||
|
||||
#
|
||||
|
||||
@@ -55,6 +55,7 @@ module.exports = function (config) {
|
||||
: undefined
|
||||
const UPDATES_ENABLED = !!UPDATES_CHANNEL
|
||||
|
||||
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
|
||||
IS_DEV ? 'dev' : ''
|
||||
}`
|
||||
@@ -186,7 +187,15 @@ module.exports = function (config) {
|
||||
},
|
||||
plugins: [
|
||||
'expo-localization',
|
||||
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
|
||||
USE_SENTRY && [
|
||||
'@sentry/react-native/expo',
|
||||
{
|
||||
organization: 'blueskyweb',
|
||||
project: 'react-native',
|
||||
release: VERSION,
|
||||
dist: SENTRY_DIST,
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-build-properties',
|
||||
{
|
||||
@@ -221,6 +230,31 @@ module.exports = function (config) {
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
'./plugins/withAppDelegateReferrer.js',
|
||||
[
|
||||
'expo-font',
|
||||
{
|
||||
fonts: [
|
||||
// './assets/fonts/inter/Inter-Thin.otf',
|
||||
// './assets/fonts/inter/Inter-ThinItalic.otf',
|
||||
// './assets/fonts/inter/Inter-ExtraLight.otf',
|
||||
// './assets/fonts/inter/Inter-ExtraLightItalic.otf',
|
||||
// './assets/fonts/inter/Inter-Light.otf',
|
||||
// './assets/fonts/inter/Inter-LightItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Regular.otf',
|
||||
'./assets/fonts/inter/Inter-Italic.otf',
|
||||
'./assets/fonts/inter/Inter-Medium.otf',
|
||||
'./assets/fonts/inter/Inter-MediumItalic.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBold.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Bold.otf',
|
||||
'./assets/fonts/inter/Inter-BoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBold.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Black.otf',
|
||||
'./assets/fonts/inter/Inter-BlackItalic.otf',
|
||||
],
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
@@ -263,7 +297,7 @@ module.exports = function (config) {
|
||||
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
|
||||
*/
|
||||
{
|
||||
file: 'sentry-expo/upload-sourcemaps',
|
||||
file: './postHooks/uploadSentrySourcemapsPostHook',
|
||||
config: {
|
||||
organization: 'blueskyweb',
|
||||
project: 'react-native',
|
||||
|
||||
@@ -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 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
@@ -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 2c0-.552.41-1 .917-1h14.666c.507 0 .917.448.917 1v20c0 .552-.41 1-.917 1H4.917C4.41 23 4 22.552 4 22V2Zm1.833 1v18h12.834V3H5.833Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -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 20.5c-.552 0-1-.41-1-.917V4.917C1 4.41 1.448 4 2 4h20c.552 0 1 .41 1 .917v14.666c0 .507-.448.917-1 .917H2Zm1-1.833h18V5.833H3v12.834Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 279 B |
@@ -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 21a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5v2H5v14h4v2H4Zm9-20v22h-2V1h2Zm7.707 2.293A1 1 0 0 1 21 4v1h-2V3h1a1 1 0 0 1 .707.293ZM19 7h2v2h-2V7Zm0 4h2v2h-2v-2Zm0 4h2v2h-2v-2Zm0 4h2v1a1 1 0 0 1-1 1h-1v-2Zm-2 0v2h-2v-2h2ZM15 5h2V3h-2v2Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 371 B |
@@ -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 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v5h-2V5H5v4H3V4Zm20 9H1v-2h22v2Zm-2.293 7.707A1 1 0 0 1 20 21h-1v-2h2v1a1 1 0 0 1-.293.707ZM17 19v2h-2v-2h2Zm-4 0v2h-2v-2h2Zm-4 0v2H7v-2h2Zm-4 0v2H4a1 1 0 0 1-1-1v-1h2Zm0-2H3v-2h2v2Zm14-2v2h2v-2h-2Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 376 B |
@@ -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 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -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.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 986 B |
@@ -21,7 +21,7 @@
|
||||
"eslint-config-preact": "^1.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tflwindcss": "^3.4.3",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.2.8",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
|
||||
@@ -158,6 +158,12 @@ export function Embed({
|
||||
return <Info>The quoted post is blocked.</Info>
|
||||
}
|
||||
|
||||
// Case 3.8: Detached quote post
|
||||
if (AppBskyEmbedRecord.isViewDetached(record)) {
|
||||
// Just don't show anything
|
||||
return null
|
||||
}
|
||||
|
||||
// Unknown embed type
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ static/js/*.js
|
||||
static/js/*.map
|
||||
static/js/*.js.LICENSE.txt
|
||||
static/js/empty.txt
|
||||
static/css/*.css
|
||||
static/css/*.map
|
||||
static/css/*.css.LICENSE.txt
|
||||
static/css/empty.txt
|
||||
static/media/*.png
|
||||
static/media/empty.txt
|
||||
templates/scripts.html
|
||||
|
||||
@@ -210,6 +210,11 @@ func serve(cctx *cli.Context) error {
|
||||
maxAge = 7 * (60 * 60 * 24) // 1 week
|
||||
}
|
||||
|
||||
// fonts can be cached for a year
|
||||
if strings.HasSuffix(path, ".otf") {
|
||||
maxAge = 365 * (60 * 60 * 24) // 1 year
|
||||
}
|
||||
|
||||
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
return next(c)
|
||||
}
|
||||
|
||||
@@ -13,252 +13,25 @@
|
||||
|
||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||
|
||||
<style>
|
||||
/**
|
||||
* Extend the react-native-web reset:
|
||||
* https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js
|
||||
*/
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
/* To smooth any scrolling behavior */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
/* Allows content to fill the viewport and go beyond the bottom */
|
||||
min-height: 100%;
|
||||
}
|
||||
#root {
|
||||
flex-shrink: 0;
|
||||
flex-basis: auto;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Regular.1f5ed03b6dd9fd1f9982.otf">
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Italic.95778eb0c75dc956257e.otf">
|
||||
<!--
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Medium.296aa2d65964269836b3.otf">
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-MediumItalic.0e57e17a6311368e2114.otf">
|
||||
-->
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-SemiBold.2277990330981b8409bb.otf">
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-SemiBoldItalic.f62fea3df3a521d6c8a7.otf">
|
||||
<!--
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Bold.8d330503e1d034ad68de.otf">
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BoldItalic.bb17e63f9baa0d861a20.otf">
|
||||
-->
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf">
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-ExtraBoldItalic.0e50b40728d24d40fdf4.otf">
|
||||
<!--
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Black.66e9a87f1c921e844ed4.otf">
|
||||
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BlackItalic.27b9f0ad06fd13a7b9da.otf">
|
||||
-->
|
||||
|
||||
html {
|
||||
/* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
height: calc(100% + env(safe-area-inset-top));
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#preload {
|
||||
width: 100px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Color theming */
|
||||
/* Default will always be white */
|
||||
:root {
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: hsl(211, 20%, 95%);
|
||||
}
|
||||
/* This gives us a black background when system is dark and we have not loaded the theme/color scheme values in JS */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: hsl(211, 20%, 20%);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
/* Overwrite those preferences with the selected theme */
|
||||
html.theme--light {
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: hsl(211, 20%, 95%);
|
||||
}
|
||||
html.theme--dark {
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: hsl(211, 20%, 20%);
|
||||
color-scheme: dark;
|
||||
}
|
||||
html.theme--dim {
|
||||
--text: white;
|
||||
--background: hsl(211, 20%, 4%);
|
||||
--backgroundLight: hsl(211, 20%, 10%);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Remove autofill styles on Webkit */
|
||||
input:autofill,
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active{
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: var(--text);
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
box-shadow: inset 0 0 20px 20px var(--background);
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
/* Force left-align date/time inputs on iOS mobile */
|
||||
input::-webkit-date-and-time-value {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
/* Allows you to scroll below the viewport; default value is visible */
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
background-color: var(--background);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-ms-overflow-style: scrollbar;
|
||||
font-synthesis-weight: none;
|
||||
}
|
||||
|
||||
/* Remove default link styling */
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
a[role="link"]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
a[role="link"][data-no-underline="1"]:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Styling hacks */
|
||||
*[data-word-wrap] {
|
||||
word-break: break-word;
|
||||
}
|
||||
*[data-stable-gutters] {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
/* ProseMirror */
|
||||
.ProseMirror {
|
||||
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
|
||||
min-height: 140px;
|
||||
}
|
||||
.ProseMirror-dark {
|
||||
color: white;
|
||||
}
|
||||
.ProseMirror p {
|
||||
margin: 0;
|
||||
}
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
color: #8d8e96;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ProseMirror .mention {
|
||||
color: #0085ff;
|
||||
}
|
||||
.ProseMirror a,
|
||||
.ProseMirror .autolink {
|
||||
color: #0085ff;
|
||||
}
|
||||
/* OLLIE: TODO -- this is not accessible */
|
||||
/* Remove focus state on inputs */
|
||||
.ProseMirror-focused {
|
||||
outline: 0;
|
||||
}
|
||||
textarea:focus,
|
||||
input:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.tippy-content .items {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateY(100%) translateY(8px) translateX(-50%);
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--backgroundLight);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
[data-tooltip]::before {
|
||||
content: '';
|
||||
display: none;
|
||||
position: absolute;
|
||||
border-bottom: 6px solid var(--backgroundLight);
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateY(100%) translateY(2px) translateX(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
[data-tooltip]:hover::after,
|
||||
[data-tooltip]:hover::before {
|
||||
display:block;
|
||||
}
|
||||
|
||||
/* NativeDropdown component */
|
||||
.radix-dropdown-item:focus,
|
||||
.nativeDropdown-item:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Spinner component */
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.rotate-500ms {
|
||||
position: absolute;
|
||||
inset:0;
|
||||
animation: rotate 500ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes avatarHoverFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes avatarHoverFadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.force-no-clicks > *,
|
||||
.force-no-clicks * {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
</style>
|
||||
{% include "scripts.html" %}
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
|
||||
@@ -5,5 +5,6 @@ module.exports = {
|
||||
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
|
||||
'use-exact-imports': require('./use-exact-imports'),
|
||||
'use-typed-gates': require('./use-typed-gates'),
|
||||
'use-prefixed-imports': require('./use-prefixed-imports'),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable bsky-internal/use-exact-imports */
|
||||
const BANNED_IMPORTS = [
|
||||
'@fortawesome/free-regular-svg-icons',
|
||||
'@fortawesome/free-solid-svg-icons',
|
||||
@@ -6,11 +5,12 @@ const BANNED_IMPORTS = [
|
||||
|
||||
exports.create = function create(context) {
|
||||
return {
|
||||
Literal(node) {
|
||||
if (typeof node.value !== 'string') {
|
||||
ImportDeclaration(node) {
|
||||
const source = node.source
|
||||
if (typeof source.value !== 'string') {
|
||||
return
|
||||
}
|
||||
if (BANNED_IMPORTS.includes(node.value)) {
|
||||
if (BANNED_IMPORTS.includes(source.value)) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const BANNED_IMPORT_PREFIXES = [
|
||||
'alf/',
|
||||
'components/',
|
||||
'lib/',
|
||||
'locale/',
|
||||
'logger/',
|
||||
'platform/',
|
||||
'state/',
|
||||
'storage/',
|
||||
'view/',
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const source = node.source
|
||||
if (typeof source.value !== 'string') {
|
||||
return
|
||||
}
|
||||
if (
|
||||
BANNED_IMPORT_PREFIXES.some(banned => source.value.startsWith(banned))
|
||||
) {
|
||||
context.report({
|
||||
node: source,
|
||||
message: `Use '#/${source.value}'`,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(source, `'#/${source.value}'`)
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.91.1",
|
||||
"version": "1.91.2",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -15,7 +15,7 @@
|
||||
"web": "expo start --web",
|
||||
"use-build-number": "./scripts/useBuildNumberEnv.sh",
|
||||
"use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
|
||||
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/ && cp -v ./web-build/static/media/*.png ./bskyweb/static/media/",
|
||||
"build-web": "expo export:web && node ./scripts/post-web-build.js",
|
||||
"build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
|
||||
"build-ios": "yarn use-build-number-with-bump eas build -p ios",
|
||||
"build-android": "yarn use-build-number-with-bump eas build -p android",
|
||||
@@ -67,7 +67,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@haileyok/bluesky-video": "0.1.8",
|
||||
"@haileyok/bluesky-video": "0.1.10",
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.8.0",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
@@ -84,7 +84,7 @@
|
||||
"@segment/analytics-react": "^1.0.0-rc1",
|
||||
"@segment/analytics-react-native": "^2.10.1",
|
||||
"@segment/sovran-react-native": "^0.4.5",
|
||||
"@sentry/react-native": "5.5.0",
|
||||
"@sentry/react-native": "5.32.0",
|
||||
"@tamagui/focus-scope": "^1.84.1",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
@@ -109,6 +109,7 @@
|
||||
"await-lock": "^2.2.2",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"base64-js": "^1.5.1",
|
||||
"bcp-47": "^2.1.0",
|
||||
"bcp-47-match": "^2.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"deprecated-react-native-prop-types": "^5.0.0",
|
||||
@@ -124,6 +125,7 @@
|
||||
"expo-dev-client": "^5.0.0-canary-20240912-1059f85",
|
||||
"expo-device": "~7.0.0-canary-20240912-1059f85",
|
||||
"expo-file-system": "^18.0.0-canary-20240912-1059f85",
|
||||
"expo-font": "13.0.0-canary-20240912-1059f85",
|
||||
"expo-haptics": "14.0.0-canary-20240912-1059f85",
|
||||
"expo-image": "~2.0.0-canary-20240912-1059f85",
|
||||
"expo-image-manipulator": "^13.0.0-canary-20240912-1059f85",
|
||||
@@ -200,10 +202,10 @@
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"sentry-expo": "~7.0.1",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
"tldts": "^6.1.46",
|
||||
"zeego": "^1.6.2",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
|
||||
index 7e0b4cd..177454c 100644
|
||||
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
|
||||
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
|
||||
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
|
||||
* This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
|
||||
*/
|
||||
export function ignoreRequireCycleLogs() {
|
||||
- LogBox.ignoreLogs(['Require cycle:']);
|
||||
+ try {
|
||||
+ LogBox.ignoreLogs(['Require cycle:']);
|
||||
+ } catch (e) {}
|
||||
}
|
||||
//# sourceMappingURL=ignorerequirecyclelogs.js.map
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
index 0f244f2..ae7dfb3 100755
|
||||
--- a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
+++ b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
@@ -174,6 +174,7 @@ if (!outputDir) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+const otherArgs = process.argv.slice(3);
|
||||
const files = getAssetPathsSync(outputDir);
|
||||
const groupedAssets = groupAssets(files);
|
||||
|
||||
@@ -195,7 +196,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) {
|
||||
|
||||
const isHermes = assets.find(asset => asset.endsWith('.hbc'));
|
||||
const windowsCallback = process.platform === "win32" ? 'node ' : '';
|
||||
- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, {
|
||||
+ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} ${otherArgs.join(' ')}`, {
|
||||
env: {
|
||||
...process.env,
|
||||
[SENTRY_PROJECT]: sentryProject,
|
||||
@@ -1,15 +0,0 @@
|
||||
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
|
||||
index 7e0b4cd..177454c 100644
|
||||
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
|
||||
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
|
||||
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
|
||||
* This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
|
||||
*/
|
||||
export function ignoreRequireCycleLogs() {
|
||||
- LogBox.ignoreLogs(['Require cycle:']);
|
||||
+ try {
|
||||
+ LogBox.ignoreLogs(['Require cycle:']);
|
||||
+ } catch (e) {}
|
||||
}
|
||||
//# sourceMappingURL=ignorerequirecyclelogs.js.map
|
||||
\ No newline at end of file
|
||||
@@ -0,0 +1,34 @@
|
||||
const exec = require('child_process').execSync
|
||||
|
||||
const SENTRY_AUTH_TOKEN = process.env.SENTRY_AUTH_TOKEN
|
||||
|
||||
module.exports = ({config}) => {
|
||||
if (!SENTRY_AUTH_TOKEN) {
|
||||
console.log(
|
||||
'SENTRY_AUTH_TOKEN environment variable must be set to upload sourcemaps. Skipping.',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const org = config.organization
|
||||
const project = config.project
|
||||
const release = config.release
|
||||
const dist = config.dist
|
||||
|
||||
if (!org || !project || !release || !dist) {
|
||||
console.log(
|
||||
'"organization", "project", "release", and "dist" must be set in the hook config to upload sourcemaps. Skipping.',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Uploading sourcemaps to Sentry...')
|
||||
exec(
|
||||
`node node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps dist --url https://sentry.io/ -o ${org} -p ${project} -r ${release} -d ${dist}`,
|
||||
)
|
||||
console.log('Sourcemaps uploaded to Sentry.')
|
||||
} catch (e) {
|
||||
console.error('Error uploading sourcemaps to Sentry:', e)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,30 @@ console.log(`Writing ${templateFile}`)
|
||||
const outputFile = entrypoints
|
||||
.map(name => {
|
||||
const file = path.basename(name)
|
||||
return `<script defer="defer" src="/static/js/${file}"></script>`
|
||||
const ext = path.extname(file)
|
||||
|
||||
if (ext === '.js') {
|
||||
return `<script defer="defer" src="/static/js/${file}"></script>`
|
||||
}
|
||||
if (ext === '.css') {
|
||||
return `<link rel="stylesheet" href="/static/css/${file}">`
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
.join('\n')
|
||||
fs.writeFileSync(templateFile, outputFile)
|
||||
|
||||
function copyFiles(sourceDir, targetDir) {
|
||||
const files = fs.readdirSync(path.join(projectRoot, sourceDir))
|
||||
files.forEach(file => {
|
||||
const sourcePath = path.join(projectRoot, sourceDir, file)
|
||||
const targetPath = path.join(projectRoot, targetDir, file)
|
||||
fs.copyFileSync(sourcePath, targetPath)
|
||||
console.log(`Copied ${sourcePath} to ${targetPath}`)
|
||||
})
|
||||
}
|
||||
|
||||
copyFiles('web-build/static/js', 'bskyweb/static/js')
|
||||
copyFiles('web-build/static/css', 'bskyweb/static/css')
|
||||
copyFiles('web-build/static/media', 'bskyweb/static/media')
|
||||
|
||||
@@ -29,6 +29,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
ensureGeolocationResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -66,6 +71,11 @@ import {BackgroundNotificationPreferencesProvider} from '../modules/expo-backgro
|
||||
|
||||
// SplashScreen.preventAutoHideAsync()
|
||||
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
beginResolveGeolocation()
|
||||
|
||||
function InnerApp() {
|
||||
const [_isReady, setIsReady] = React.useState(false)
|
||||
const {currentAccount} = useSession()
|
||||
@@ -157,7 +167,9 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
initPersistedState().then(() => setReady(true))
|
||||
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
|
||||
setReady(true),
|
||||
)
|
||||
}, [])
|
||||
|
||||
if (!isReady) {
|
||||
@@ -169,36 +181,38 @@ function App() {
|
||||
* that is set up in the InnerApp component above.
|
||||
*/
|
||||
return (
|
||||
<A11yProvider>
|
||||
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
</IntentDialogProvider>
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</KeyboardProvider>
|
||||
</A11yProvider>
|
||||
<GeolocationProvider>
|
||||
<A11yProvider>
|
||||
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
</IntentDialogProvider>
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</KeyboardProvider>
|
||||
</A11yProvider>
|
||||
</GeolocationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'lib/sentry' // must be near top
|
||||
import 'view/icons'
|
||||
import './style.css'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {KeyboardProvider} from 'react-native-keyboard-controller'
|
||||
@@ -18,6 +19,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
ensureGeolocationResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -54,6 +60,11 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
beginResolveGeolocation()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
const {currentAccount} = useSession()
|
||||
@@ -133,8 +144,8 @@ function InnerApp() {
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
<ToastContainer />
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
</RootSiblingParent>
|
||||
@@ -148,7 +159,9 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
initPersistedState().then(() => setReady(true))
|
||||
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
|
||||
setReady(true),
|
||||
)
|
||||
}, [])
|
||||
|
||||
if (!isReady) {
|
||||
@@ -160,31 +173,33 @@ function App() {
|
||||
* that is set up in the InnerApp component above.
|
||||
*/
|
||||
return (
|
||||
<A11yProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
</IntentDialogProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</A11yProvider>
|
||||
<GeolocationProvider>
|
||||
<A11yProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
</IntentDialogProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</A11yProvider>
|
||||
</GeolocationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -225,43 +225,43 @@ export const atoms = {
|
||||
},
|
||||
text_2xs: {
|
||||
fontSize: tokens.fontSize._2xs,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_xs: {
|
||||
fontSize: tokens.fontSize.xs,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_sm: {
|
||||
fontSize: tokens.fontSize.sm,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_md: {
|
||||
fontSize: tokens.fontSize.md,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_lg: {
|
||||
fontSize: tokens.fontSize.lg,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_xl: {
|
||||
fontSize: tokens.fontSize.xl,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_2xl: {
|
||||
fontSize: tokens.fontSize._2xl,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_3xl: {
|
||||
fontSize: tokens.fontSize._3xl,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_4xl: {
|
||||
fontSize: tokens.fontSize._4xl,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
text_5xl: {
|
||||
fontSize: tokens.fontSize._5xl,
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
leading_tight: {
|
||||
lineHeight: 1.15,
|
||||
@@ -273,22 +273,16 @@ export const atoms = {
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
tracking_normal: {
|
||||
letterSpacing: 0,
|
||||
},
|
||||
tracking_wide: {
|
||||
letterSpacing: 0.25,
|
||||
letterSpacing: tokens.TRACKING,
|
||||
},
|
||||
font_normal: {
|
||||
fontWeight: tokens.fontWeight.normal,
|
||||
},
|
||||
font_semibold: {
|
||||
fontWeight: tokens.fontWeight.semibold,
|
||||
fontWeight: tokens.fontWeight.regular,
|
||||
},
|
||||
font_bold: {
|
||||
fontWeight: tokens.fontWeight.bold,
|
||||
fontWeight: tokens.fontWeight.semibold,
|
||||
},
|
||||
font_heavy: {
|
||||
fontWeight: tokens.fontWeight.heavy,
|
||||
fontWeight: tokens.fontWeight.extrabold,
|
||||
},
|
||||
italic: {
|
||||
fontStyle: 'italic',
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import {useFonts as defaultUseFonts} from 'expo-font'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {Device, device} from '#/storage'
|
||||
|
||||
const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif`
|
||||
|
||||
const factor = 0.0625 // 1 - (15/16)
|
||||
const fontScaleMultipliers: Record<Device['fontScale'], number> = {
|
||||
'-2': 1 - factor * 3,
|
||||
'-1': 1 - factor * 2,
|
||||
'0': 1 - factor * 1, // default
|
||||
'1': 1,
|
||||
'2': 1 + factor * 1,
|
||||
}
|
||||
|
||||
export function computeFontScaleMultiplier(scale: Device['fontScale']) {
|
||||
return fontScaleMultipliers[scale]
|
||||
}
|
||||
|
||||
export function getFontScale() {
|
||||
return device.get(['fontScale']) ?? '0'
|
||||
}
|
||||
|
||||
export function setFontScale(fontScale: Device['fontScale']) {
|
||||
device.set(['fontScale'], fontScale)
|
||||
}
|
||||
|
||||
export function getFontFamily() {
|
||||
return device.get(['fontFamily']) || 'theme'
|
||||
}
|
||||
|
||||
export function setFontFamily(fontFamily: Device['fontFamily']) {
|
||||
device.set(['fontFamily'], fontFamily)
|
||||
}
|
||||
|
||||
/*
|
||||
* IMPORTANT: This is unused. Expo statically extracts these fonts, but we load
|
||||
* them manually so that we can parallelize the loading along with the JS
|
||||
* bundle.
|
||||
*
|
||||
* See `#/alf/util/useFonts` for the actually used hooks.
|
||||
*
|
||||
* All used fonts MUST be configured here. Unused fonts are commented out, but
|
||||
* the files are there if we need them.
|
||||
*/
|
||||
export function DO_NOT_USE() {
|
||||
return defaultUseFonts({
|
||||
// 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'),
|
||||
// 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'),
|
||||
// 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'),
|
||||
// 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'),
|
||||
// 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'),
|
||||
// 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'),
|
||||
'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'),
|
||||
'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'),
|
||||
// 'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
|
||||
// 'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
|
||||
'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'),
|
||||
'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
|
||||
// 'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
|
||||
// 'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
|
||||
'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'),
|
||||
'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
|
||||
// 'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
|
||||
// 'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Unused fonts are commented out, but the files are there if we need them.
|
||||
*/
|
||||
export function applyFonts(
|
||||
style: Record<string, any>,
|
||||
fontFamily: 'system' | 'theme',
|
||||
) {
|
||||
if (fontFamily === 'theme') {
|
||||
style.fontFamily =
|
||||
{
|
||||
// '100': 'Inter-Thin',
|
||||
// '200': 'Inter-ExtraLight',
|
||||
// '300': 'Inter-Light',
|
||||
'100': 'Inter-Regular',
|
||||
'200': 'Inter-Regular',
|
||||
'300': 'Inter-Regular',
|
||||
'400': 'Inter-Regular',
|
||||
'500': 'Inter-Medium',
|
||||
'600': 'Inter-SemiBold',
|
||||
'700': 'Inter-Bold',
|
||||
'800': 'Inter-ExtraBold',
|
||||
'900': 'Inter-Black',
|
||||
}[style.fontWeight as string] || 'Inter-Regular'
|
||||
|
||||
if (style.fontStyle === 'italic') {
|
||||
if (style.fontFamily === 'Inter-Regular') {
|
||||
style.fontFamily = 'Inter-Italic'
|
||||
} else {
|
||||
style.fontFamily += 'Italic'
|
||||
}
|
||||
}
|
||||
|
||||
// fallback families only supported on web
|
||||
if (isWeb) {
|
||||
style.fontFamily += `, ${FAMILIES}`
|
||||
}
|
||||
} else {
|
||||
// fallback families only supported on web
|
||||
if (isWeb) {
|
||||
style.fontFamily = style.fontFamily || FAMILIES
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable contextual ligatures
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant}
|
||||
*/
|
||||
style.fontVariant = ['no-contextual']
|
||||
}
|
||||
@@ -1,25 +1,47 @@
|
||||
import React from 'react'
|
||||
import {useMediaQuery} from 'react-responsive'
|
||||
|
||||
import {
|
||||
computeFontScaleMultiplier,
|
||||
getFontFamily,
|
||||
getFontScale,
|
||||
setFontFamily as persistFontFamily,
|
||||
setFontScale as persistFontScale,
|
||||
} from '#/alf/fonts'
|
||||
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||
import {Theme, ThemeName} from '#/alf/types'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
import {Device} from '#/storage'
|
||||
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * from '#/alf/fonts'
|
||||
export * as tokens from '#/alf/tokens'
|
||||
export * from '#/alf/types'
|
||||
export * from '#/alf/util/flatten'
|
||||
export * from '#/alf/util/platform'
|
||||
export * from '#/alf/util/themeSelector'
|
||||
|
||||
/*
|
||||
* Context
|
||||
*/
|
||||
export const Context = React.createContext<{
|
||||
export type Alf = {
|
||||
themeName: ThemeName
|
||||
theme: Theme
|
||||
themes: ReturnType<typeof createThemes>
|
||||
}>({
|
||||
fonts: {
|
||||
scale: Exclude<Device['fontScale'], undefined>
|
||||
scaleMultiplier: number
|
||||
family: Device['fontFamily']
|
||||
setFontScale: (fontScale: Exclude<Device['fontScale'], undefined>) => void
|
||||
setFontFamily: (fontFamily: Device['fontFamily']) => void
|
||||
}
|
||||
/**
|
||||
* Feature flags or other gated options
|
||||
*/
|
||||
flags: {}
|
||||
}
|
||||
|
||||
/*
|
||||
* Context
|
||||
*/
|
||||
export const Context = React.createContext<Alf>({
|
||||
themeName: 'light',
|
||||
theme: defaultTheme,
|
||||
themes: createThemes({
|
||||
@@ -29,12 +51,48 @@ export const Context = React.createContext<{
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
}),
|
||||
fonts: {
|
||||
scale: getFontScale(),
|
||||
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
|
||||
family: getFontFamily(),
|
||||
setFontScale: () => {},
|
||||
setFontFamily: () => {},
|
||||
},
|
||||
flags: {},
|
||||
})
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
theme: themeName,
|
||||
}: React.PropsWithChildren<{theme: ThemeName}>) {
|
||||
const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() =>
|
||||
getFontScale(),
|
||||
)
|
||||
const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() =>
|
||||
computeFontScaleMultiplier(fontScale),
|
||||
)
|
||||
const setFontScaleAndPersist = React.useCallback<
|
||||
Alf['fonts']['setFontScale']
|
||||
>(
|
||||
fontScale => {
|
||||
setFontScale(fontScale)
|
||||
persistFontScale(fontScale)
|
||||
setFontScaleMultiplier(computeFontScaleMultiplier(fontScale))
|
||||
},
|
||||
[setFontScale],
|
||||
)
|
||||
const [fontFamily, setFontFamily] = React.useState<Alf['fonts']['family']>(
|
||||
() => getFontFamily(),
|
||||
)
|
||||
const setFontFamilyAndPersist = React.useCallback<
|
||||
Alf['fonts']['setFontFamily']
|
||||
>(
|
||||
fontFamily => {
|
||||
setFontFamily(fontFamily)
|
||||
persistFontFamily(fontFamily)
|
||||
},
|
||||
[setFontFamily],
|
||||
)
|
||||
const themes = React.useMemo(() => {
|
||||
return createThemes({
|
||||
hues: {
|
||||
@@ -44,28 +102,47 @@ export function ThemeProvider({
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
const theme = themes[themeName]
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={React.useMemo(
|
||||
value={React.useMemo<Alf>(
|
||||
() => ({
|
||||
themes,
|
||||
themeName: themeName,
|
||||
theme: theme,
|
||||
theme: themes[themeName],
|
||||
fonts: {
|
||||
scale: fontScale,
|
||||
scaleMultiplier: fontScaleMultiplier,
|
||||
family: fontFamily,
|
||||
setFontScale: setFontScaleAndPersist,
|
||||
setFontFamily: setFontFamilyAndPersist,
|
||||
},
|
||||
flags: {},
|
||||
}),
|
||||
[theme, themeName, themes],
|
||||
[
|
||||
themeName,
|
||||
themes,
|
||||
fontScale,
|
||||
setFontScaleAndPersist,
|
||||
fontFamily,
|
||||
setFontFamilyAndPersist,
|
||||
fontScaleMultiplier,
|
||||
],
|
||||
)}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAlf() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function useTheme(theme?: ThemeName) {
|
||||
const ctx = React.useContext(Context)
|
||||
const alf = useAlf()
|
||||
return React.useMemo(() => {
|
||||
return theme ? ctx.themes[theme] : ctx.theme
|
||||
}, [theme, ctx])
|
||||
return theme ? alf.themes[theme] : alf.theme
|
||||
}, [theme, alf])
|
||||
}
|
||||
|
||||
export function useBreakpoints() {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {Platform} from 'react-native'
|
||||
|
||||
export const TRACKING = Platform.OS === 'android' ? 0.1 : 0
|
||||
|
||||
export const color = {
|
||||
temp_purple: 'rgb(105 0 255)',
|
||||
temp_purple_dark: 'rgb(83 0 202)',
|
||||
@@ -43,11 +47,16 @@ export const borderRadius = {
|
||||
full: 999,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* These correspond to Inter font files we actually load.
|
||||
*/
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
semibold: '500',
|
||||
bold: '600',
|
||||
heavy: '700',
|
||||
regular: '400',
|
||||
// medium: '500',
|
||||
semibold: '600',
|
||||
// bold: '700',
|
||||
extrabold: '800',
|
||||
// black: '900',
|
||||
} as const
|
||||
|
||||
export const gradients = {
|
||||
|
||||
@@ -24,8 +24,6 @@ export function AppLanguageDropdown() {
|
||||
if (sanitizedLang !== value) {
|
||||
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||
}
|
||||
setLangPrefs.setPrimaryLanguage(value)
|
||||
setLangPrefs.setContentLanguage(value)
|
||||
|
||||
// reset feeds to refetch content
|
||||
resetPostsFeedQueries(queryClient)
|
||||
|
||||
@@ -27,8 +27,6 @@ export function AppLanguageDropdown() {
|
||||
if (sanitizedLang !== value) {
|
||||
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||
}
|
||||
setLangPrefs.setPrimaryLanguage(value)
|
||||
setLangPrefs.setContentLanguage(value)
|
||||
|
||||
// reset feeds to refetch content
|
||||
resetPostsFeedQueries(queryClient)
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
PressableProps,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextProps,
|
||||
TextStyle,
|
||||
View,
|
||||
@@ -15,9 +14,9 @@ import {
|
||||
} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
|
||||
import {atoms as a, flatten, select, tokens, useTheme, web} from '#/alf'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {normalizeTextStyles} from '#/components/Typography'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
|
||||
export type ButtonColor =
|
||||
@@ -31,7 +30,7 @@ export type ButtonColor =
|
||||
| 'gradient_sunset'
|
||||
| 'gradient_nordic'
|
||||
| 'gradient_bonfire'
|
||||
export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large'
|
||||
export type ButtonSize = 'tiny' | 'small' | 'large'
|
||||
export type ButtonShape = 'round' | 'square' | 'default'
|
||||
export type VariantProps = {
|
||||
/**
|
||||
@@ -344,39 +343,46 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
|
||||
if (shape === 'default') {
|
||||
if (size === 'large') {
|
||||
baseStyles.push(
|
||||
{paddingVertical: 15},
|
||||
a.px_2xl,
|
||||
a.rounded_sm,
|
||||
a.gap_md,
|
||||
)
|
||||
} else if (size === 'medium') {
|
||||
baseStyles.push(
|
||||
{paddingVertical: 12},
|
||||
a.px_2xl,
|
||||
a.rounded_sm,
|
||||
a.gap_md,
|
||||
)
|
||||
baseStyles.push({
|
||||
paddingVertical: 13,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
gap: 8,
|
||||
})
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
||||
} else if (size === 'xsmall') {
|
||||
baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm)
|
||||
baseStyles.push({
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 6,
|
||||
gap: 6,
|
||||
})
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
|
||||
baseStyles.push({
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 4,
|
||||
gap: 4,
|
||||
})
|
||||
}
|
||||
} else if (shape === 'round' || shape === 'square') {
|
||||
if (size === 'large') {
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 54, width: 54})
|
||||
baseStyles.push({height: 46, width: 46})
|
||||
} else {
|
||||
baseStyles.push({height: 50, width: 50})
|
||||
baseStyles.push({height: 44, width: 44})
|
||||
}
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({height: 34, width: 34})
|
||||
} else if (size === 'xsmall') {
|
||||
baseStyles.push({height: 28, width: 28})
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 36, width: 36})
|
||||
} else {
|
||||
baseStyles.push({height: 34, width: 34})
|
||||
}
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push({height: 20, width: 20})
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 22, width: 22})
|
||||
} else {
|
||||
baseStyles.push({height: 21, width: 21})
|
||||
}
|
||||
}
|
||||
|
||||
if (shape === 'round') {
|
||||
@@ -620,11 +626,11 @@ export function useSharedButtonTextStyles() {
|
||||
}
|
||||
|
||||
if (size === 'large') {
|
||||
baseStyles.push(a.text_md, android({paddingBottom: 1}))
|
||||
baseStyles.push(a.text_md, a.leading_tight, web({paddingTop: 1}))
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push(a.text_sm, a.leading_tight, web({paddingTop: 1}))
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push(a.text_xs, android({paddingBottom: 1}))
|
||||
} else {
|
||||
baseStyles.push(a.text_sm, android({paddingBottom: 1}))
|
||||
baseStyles.push(a.text_xs, a.leading_tight)
|
||||
}
|
||||
|
||||
return StyleSheet.flatten(baseStyles)
|
||||
@@ -635,14 +641,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
|
||||
return (
|
||||
<Text
|
||||
{...rest}
|
||||
style={normalizeTextStyles([
|
||||
a.font_bold,
|
||||
a.text_center,
|
||||
textStyles,
|
||||
style,
|
||||
])}>
|
||||
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
@@ -651,31 +650,98 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
export function ButtonIcon({
|
||||
icon: Comp,
|
||||
position,
|
||||
size: iconSize,
|
||||
size,
|
||||
}: {
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
position?: 'left' | 'right'
|
||||
size?: SVGIconProps['size']
|
||||
}) {
|
||||
const {size, disabled} = useButtonContext()
|
||||
const {size: buttonSize, disabled} = useButtonContext()
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
const {iconSize, iconContainerSize} = React.useMemo(() => {
|
||||
/**
|
||||
* Pre-set icon sizes for different button sizes
|
||||
*/
|
||||
const iconSizeShorthand =
|
||||
size ??
|
||||
(({
|
||||
large: 'sm',
|
||||
small: 'xs',
|
||||
tiny: 'xs',
|
||||
}[buttonSize || 'small'] || 'sm') as Exclude<
|
||||
SVGIconProps['size'],
|
||||
undefined
|
||||
>)
|
||||
|
||||
/*
|
||||
* 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,
|
||||
}[iconSizeShorthand]
|
||||
|
||||
/*
|
||||
* Goal here is to match rendered text size so that different size icons
|
||||
* don't increase button size
|
||||
*/
|
||||
const iconContainerSize = {
|
||||
large: 18,
|
||||
small: 16,
|
||||
tiny: 13,
|
||||
}[buttonSize || 'small']
|
||||
|
||||
return {
|
||||
iconSize,
|
||||
iconContainerSize,
|
||||
}
|
||||
}, [buttonSize, size])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.z_20,
|
||||
{
|
||||
width: iconContainerSize,
|
||||
height: iconContainerSize,
|
||||
opacity: disabled ? 0.7 : 1,
|
||||
marginLeft: position === 'left' ? -2 : 0,
|
||||
marginRight: position === 'right' ? -2 : 0,
|
||||
},
|
||||
]}>
|
||||
<Comp
|
||||
size={
|
||||
iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm')
|
||||
}
|
||||
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: [
|
||||
{
|
||||
translateX: (iconSize / 2) * -1,
|
||||
},
|
||||
{
|
||||
translateY: (iconSize / 2) * -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
<Comp
|
||||
width={iconSize}
|
||||
style={[
|
||||
{
|
||||
color: textStyles.color,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {Portal} from '#/components/Portal'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/types'
|
||||
export * from '#/components/Dialog/utils'
|
||||
// @ts-ignore
|
||||
export const Input = createInput(BottomSheetTextInput)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {Portal} from '#/components/Portal'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/types'
|
||||
export * from '#/components/Dialog/utils'
|
||||
export {Input} from '#/components/forms/TextField'
|
||||
|
||||
const stopPropagation = (e: any) => e.stopPropagation()
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import {DialogControlProps} from '#/components/Dialog/types'
|
||||
|
||||
export function useAutoOpen(control: DialogControlProps, showTimeout?: number) {
|
||||
React.useEffect(() => {
|
||||
if (showTimeout) {
|
||||
const timeout = setTimeout(() => {
|
||||
control.open()
|
||||
}, showTimeout)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
} else {
|
||||
control.open()
|
||||
}
|
||||
}, [control, showTimeout])
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useLabelerInfoQuery} from '#/state/queries/labeler'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -43,21 +44,40 @@ export function Avatar({avatar}: {avatar?: string}) {
|
||||
}
|
||||
|
||||
export function Title({value}: {value: string}) {
|
||||
return <Text style={[a.text_md, a.font_bold]}>{value}</Text>
|
||||
return <Text style={[a.text_md, a.font_bold, a.leading_tight]}>{value}</Text>
|
||||
}
|
||||
|
||||
export function Description({value, handle}: {value?: string; handle: string}) {
|
||||
return value ? (
|
||||
<Text numberOfLines={2}>
|
||||
<RichText value={value} style={[]} />
|
||||
<RichText value={value} style={[a.leading_snug]} />
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
<Text style={[a.leading_snug]}>
|
||||
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function RegionalNotice() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
a.pt_2xs,
|
||||
{marginLeft: -2},
|
||||
]}>
|
||||
<Flag fill={t.atoms.text_contrast_low.color} size="sm" />
|
||||
<Text style={[a.italic, a.leading_snug]}>
|
||||
<Trans>Required in your region</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function LikeCount({count}: {count: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
@@ -66,7 +86,7 @@ export function LikeCount({count}: {count: number}) {
|
||||
a.mt_sm,
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
{fontWeight: '500'},
|
||||
{fontWeight: '600'},
|
||||
]}>
|
||||
<Plural value={count} one="Liked by # user" other="Liked by # users" />
|
||||
</Text>
|
||||
@@ -85,7 +105,7 @@ export function Content({children}: React.PropsWithChildren<{}>) {
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
]}>
|
||||
<View style={[a.gap_xs, a.flex_1]}>{children}</View>
|
||||
<View style={[a.gap_2xs, a.flex_1]}>{children}</View>
|
||||
|
||||
<ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
|
||||
</View>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function MediaInsetBorder({
|
||||
return (
|
||||
<Fill
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
opaque
|
||||
? [t.atoms.border_contrast_low]
|
||||
|
||||
@@ -170,6 +170,6 @@ const styles = StyleSheet.create({
|
||||
alt: {
|
||||
color: 'white',
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
fontWeight: '600',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -179,8 +179,10 @@ export function Outer({
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
a.border,
|
||||
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
|
||||
t.atoms.shadow_md,
|
||||
t.atoms.border_contrast_low,
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
|
||||
@@ -132,7 +132,7 @@ export function Label({
|
||||
<Text
|
||||
style={[
|
||||
text,
|
||||
a.font_semibold,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_medium,
|
||||
{paddingRight: 3},
|
||||
|
||||
@@ -411,6 +411,7 @@ function Inner({
|
||||
() => currentAccount?.did === profile.did,
|
||||
[currentAccount, profile],
|
||||
)
|
||||
const isLabeler = profile.associated?.labeler
|
||||
|
||||
return (
|
||||
<View>
|
||||
@@ -419,11 +420,13 @@ function Inner({
|
||||
<UserAvatar
|
||||
size={64}
|
||||
avatar={profile.avatar}
|
||||
type={isLabeler ? 'labeler' : 'user'}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{!isMe &&
|
||||
!isLabeler &&
|
||||
(isBlockedUser ? (
|
||||
<Link
|
||||
to={profileURL}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.font_semibold,
|
||||
a.font_bold,
|
||||
a.text_sm,
|
||||
{textTransform: 'uppercase'},
|
||||
]}>
|
||||
|
||||
@@ -35,9 +35,7 @@ export function ProgressGuideTask({
|
||||
)}
|
||||
|
||||
<View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
|
||||
<Text style={[a.text_sm, a.font_semibold, a.leading_tight]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[a.text_sm, a.font_bold, a.leading_tight]}>{title}</Text>
|
||||
{subtitle && (
|
||||
<Text
|
||||
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}>
|
||||
|
||||
@@ -154,7 +154,7 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
ref={animatedCheckRef}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[a.text_md, a.font_semibold]}>{title}</Text>
|
||||
<Text style={[a.text_md, a.font_bold]}>{title}</Text>
|
||||
{subtitle && (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{subtitle}
|
||||
|
||||
@@ -59,7 +59,9 @@ export function Outer({
|
||||
export function TitleText({children}: React.PropsWithChildren<{}>) {
|
||||
const {titleId} = React.useContext(Context)
|
||||
return (
|
||||
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
|
||||
<Text
|
||||
nativeID={titleId}
|
||||
style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
@@ -118,7 +120,7 @@ export function Cancel({
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size={gtMobile ? 'small' : 'medium'}
|
||||
size={gtMobile ? 'small' : 'large'}
|
||||
label={cta || _(msg`Cancel`)}
|
||||
onPress={onPress}>
|
||||
<ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
|
||||
@@ -161,7 +163,7 @@ export function Action({
|
||||
<Button
|
||||
variant="solid"
|
||||
color={color}
|
||||
size={gtMobile ? 'small' : 'medium'}
|
||||
size={gtMobile ? 'small' : 'large'}
|
||||
label={cta || _(msg`Confirm`)}
|
||||
onPress={handleOnPress}
|
||||
testID={testID}>
|
||||
|
||||
@@ -77,8 +77,7 @@ function LabelerButton({
|
||||
handle: labeler.creator.handle,
|
||||
})}
|
||||
/>
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}>
|
||||
<Text style={[t.atoms.text_contrast_medium, a.text_sm, a.font_bold]}>
|
||||
@{labeler.creator.handle}
|
||||
</Text>
|
||||
</LabelingServiceCard.Content>
|
||||
|
||||
@@ -125,7 +125,7 @@ export function WizardEditListDialog({
|
||||
label={_(msg`Close`)}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
size="small"
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
|
||||
@@ -101,7 +101,7 @@ function WizardListCard({
|
||||
label={_(msg`Remove`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="xsmall"
|
||||
size="small"
|
||||
style={[a.self_center, {marginLeft: 'auto'}]}
|
||||
onPress={onPress}>
|
||||
<ButtonText>
|
||||
|
||||
@@ -3,7 +3,7 @@ import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {atoms, flatten, useTheme, web} from '#/alf'
|
||||
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
||||
|
||||
export type TextProps = RNTextProps & {
|
||||
/**
|
||||
@@ -34,19 +34,30 @@ export function leading<
|
||||
* If the `lineHeight` value is > 2, we assume it's an absolute value and
|
||||
* returns it as-is.
|
||||
*/
|
||||
export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
|
||||
export function normalizeTextStyles(
|
||||
styles: StyleProp<TextStyle>,
|
||||
{
|
||||
fontScale,
|
||||
fontFamily,
|
||||
}: {
|
||||
fontScale: number
|
||||
fontFamily: Alf['fonts']['family']
|
||||
} & Pick<Alf, 'flags'>,
|
||||
) {
|
||||
const s = flatten(styles)
|
||||
// should always be defined on these components
|
||||
const fontSize = s.fontSize || atoms.text_md.fontSize
|
||||
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
|
||||
|
||||
if (s?.lineHeight) {
|
||||
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
|
||||
s.lineHeight = Math.round(fontSize * s.lineHeight)
|
||||
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
|
||||
}
|
||||
} else if (!isNative) {
|
||||
s.lineHeight = s.fontSize
|
||||
}
|
||||
|
||||
applyFonts(s, fontFamily)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -54,8 +65,13 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
|
||||
* Our main text component. Use this most of the time.
|
||||
*/
|
||||
export function Text({style, selectable, ...rest}: TextProps) {
|
||||
const {fonts, flags} = useAlf()
|
||||
const t = useTheme()
|
||||
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
|
||||
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags,
|
||||
})
|
||||
|
||||
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function BirthdayInner({
|
||||
<View style={isWeb && [a.flex_row, a.justify_end]}>
|
||||
<Button
|
||||
label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)}
|
||||
size="medium"
|
||||
size="large"
|
||||
onPress={onSave}
|
||||
variant="solid"
|
||||
color="primary">
|
||||
|
||||
@@ -120,7 +120,7 @@ function EmbedDialogInner({
|
||||
label={_(msg`Copy code`)}
|
||||
color="primary"
|
||||
variant="solid"
|
||||
size="medium"
|
||||
size="large"
|
||||
onPress={() => {
|
||||
ref.current?.focus()
|
||||
ref.current?.setSelection(0, snippet.length)
|
||||
|
||||
@@ -83,7 +83,7 @@ export function EmbedConsentDialog({
|
||||
onPress={onShowAllPress}
|
||||
onAccessibilityEscape={control.close}
|
||||
color="primary"
|
||||
size="medium"
|
||||
size="large"
|
||||
variant="solid">
|
||||
<ButtonText>
|
||||
<Trans>Enable external media</Trans>
|
||||
@@ -95,7 +95,7 @@ export function EmbedConsentDialog({
|
||||
onPress={onShowPress}
|
||||
onAccessibilityEscape={control.close}
|
||||
color="secondary"
|
||||
size="medium"
|
||||
size="large"
|
||||
variant="solid">
|
||||
<ButtonText>
|
||||
<Trans>Enable {externalEmbedLabels[source]} only</Trans>
|
||||
@@ -106,7 +106,7 @@ export function EmbedConsentDialog({
|
||||
onAccessibilityEscape={control.close}
|
||||
onPress={onHidePress}
|
||||
color="secondary"
|
||||
size="medium"
|
||||
size="large"
|
||||
variant="ghost">
|
||||
<ButtonText>
|
||||
<Trans>No thanks</Trans>
|
||||
|
||||
@@ -244,7 +244,7 @@ function ModalError({details, close}: {details?: string; close: () => void}) {
|
||||
label={_(msg`Close dialog`)}
|
||||
onPress={close}
|
||||
color="primary"
|
||||
size="medium"
|
||||
size="large"
|
||||
variant="solid">
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
|
||||
@@ -264,7 +264,7 @@ function DialogError({details}: {details?: string}) {
|
||||
label={_(msg`Close dialog`)}
|
||||
onPress={() => control.close()}
|
||||
color="primary"
|
||||
size="medium"
|
||||
size="large"
|
||||
variant="solid">
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
|
||||
@@ -319,7 +319,7 @@ function MutedWordsInner() {
|
||||
<Button
|
||||
disabled={isPending || !field}
|
||||
label={_(msg`Add mute word for configured settings`)}
|
||||
size="medium"
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
style={[]}
|
||||
|
||||
@@ -439,7 +439,7 @@ export function PostInteractionSettingsForm({
|
||||
onPress={onSave}
|
||||
onAccessibilityEscape={control.close}
|
||||
color="primary"
|
||||
size="medium"
|
||||
size="large"
|
||||
variant="solid"
|
||||
style={a.mt_xl}>
|
||||
<ButtonText>{_(msg`Save`)}</ButtonText>
|
||||
@@ -491,9 +491,7 @@ function Selectable({
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
<Text style={[a.text_sm, isSelected && a.font_semibold]}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text style={[a.text_sm, isSelected && a.font_bold]}>{label}</Text>
|
||||
{isSelected ? (
|
||||
<Check size="sm" fill={t.palette.primary_500} />
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings'
|
||||
import {atoms as a, useAlf, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
|
||||
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function NeueTypography() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const control = Dialog.useDialogControl()
|
||||
const {fonts} = useAlf()
|
||||
|
||||
Dialog.useAutoOpen(control, 3e3)
|
||||
|
||||
const onClose = React.useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
}, [nuxDialogs])
|
||||
|
||||
const onChangeFontFamily = React.useCallback(
|
||||
(values: string[]) => {
|
||||
const next = values[0] === 'system' ? 'system' : 'theme'
|
||||
fonts.setFontFamily(next)
|
||||
},
|
||||
[fonts],
|
||||
)
|
||||
|
||||
const onChangeFontScale = React.useCallback(
|
||||
(values: string[]) => {
|
||||
const next = values[0] || ('0' as any)
|
||||
fonts.setFontScale(next)
|
||||
},
|
||||
[fonts],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
|
||||
<View style={[a.gap_xl]}>
|
||||
<View style={[a.gap_md]}>
|
||||
<Text style={[a.text_3xl, a.font_heavy]}>
|
||||
<Trans>New font settings ✨</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}>
|
||||
<Trans>
|
||||
We're introducing a new theme font, along with adjustable font
|
||||
sizing.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
You can adjust these in your Appearance Settings later.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.gap_lg]}>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Font`)}
|
||||
description={_(
|
||||
msg`For the best experience, we recommend using the theme font.`,
|
||||
)}
|
||||
icon={Aa}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`System`),
|
||||
name: 'system',
|
||||
},
|
||||
{
|
||||
label: _(msg`Theme`),
|
||||
name: 'theme',
|
||||
},
|
||||
]}
|
||||
values={[fonts.family]}
|
||||
onChange={onChangeFontFamily}
|
||||
/>
|
||||
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Font size`)}
|
||||
icon={TextSize}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`Smaller`),
|
||||
name: '-1',
|
||||
},
|
||||
{
|
||||
label: _(msg`Default`),
|
||||
name: '0',
|
||||
},
|
||||
{
|
||||
label: _(msg`Larger`),
|
||||
name: '1',
|
||||
},
|
||||
]}
|
||||
values={[fonts.scale]}
|
||||
onChange={onChangeFontScale}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Svg, {Circle, Path} from 'react-native-svg'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
|
||||
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {TenMillion} from './'
|
||||
|
||||
export function Trigger({children}: {children: ButtonProps['children']}) {
|
||||
const {_} = useLingui()
|
||||
const {mutate: upsertNux} = useUpsertNuxMutation()
|
||||
const [show, setShow] = React.useState(false)
|
||||
const [fallback, setFallback] = React.useState(false)
|
||||
const control = Prompt.usePromptControl()
|
||||
|
||||
const handleOnPress = () => {
|
||||
if (!fallback) {
|
||||
setShow(true)
|
||||
upsertNux({
|
||||
id: Nux.TenMillionDialog,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
})
|
||||
} else {
|
||||
control.open()
|
||||
}
|
||||
}
|
||||
|
||||
const onHandleFallback = () => {
|
||||
setFallback(true)
|
||||
control.open()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Bluesky is celebrating 10 million users!`)}
|
||||
onPress={handleOnPress}>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
{show && !fallback && (
|
||||
<TenMillion
|
||||
showTimeout={0}
|
||||
onClose={() => setShow(false)}
|
||||
onFallback={onHandleFallback}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Prompt.Outer control={control}>
|
||||
<View style={{maxWidth: 300}}>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Bluesky is celebrating 10 million users!</Trans>
|
||||
</Prompt.TitleText>
|
||||
</View>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
Together, we're rebuilding the social internet. We're glad you're
|
||||
here.
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
To learn more,{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`View our post`)}
|
||||
to="/profile/bsky.app/post/3l47prg3wgy23"
|
||||
onPress={() => {
|
||||
control.close()
|
||||
}}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>check out our post.</Trans>
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Dialog.Close />
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Icon({width, style}: {width: number} & ViewStyleProp) {
|
||||
return (
|
||||
<Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
|
||||
<Path
|
||||
fill="#dd2e44"
|
||||
d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
|
||||
/>
|
||||
<Path
|
||||
fill="#ea596e"
|
||||
d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
|
||||
/>
|
||||
<Path
|
||||
fill="#a0041e"
|
||||
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
|
||||
/>
|
||||
<Path
|
||||
fill="#aa8dd8"
|
||||
d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
|
||||
/>
|
||||
<Path
|
||||
fill="#77b255"
|
||||
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
|
||||
/>
|
||||
<Path
|
||||
fill="#aa8dd8"
|
||||
d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
|
||||
/>
|
||||
<Path
|
||||
fill="#77b255"
|
||||
d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
|
||||
/>
|
||||
<Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="2" cy="18" r="2" fill="#9266cc" />
|
||||
<Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
|
||||
<Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
|
||||
<Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
|
||||
<Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
@@ -19,10 +19,10 @@ import {isIOS, isNative} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useComposerControls} from 'state/shell'
|
||||
import {useComposerControls} from '#/state/shell'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Logomark} from '#/view/icons/Logomark'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {
|
||||
atoms as a,
|
||||
ThemeProvider,
|
||||
@@ -87,7 +87,15 @@ function Frame({children}: {children: React.ReactNode}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function TenMillion() {
|
||||
export function TenMillion({
|
||||
showTimeout,
|
||||
onClose,
|
||||
onFallback,
|
||||
}: {
|
||||
showTimeout?: number
|
||||
onClose?: () => void
|
||||
onFallback?: () => void
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const [userNumber, setUserNumber] = React.useState<number>(0)
|
||||
@@ -120,7 +128,11 @@ export function TenMillion() {
|
||||
} else {
|
||||
// should be rare
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
}
|
||||
} else {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +140,7 @@ export function TenMillion() {
|
||||
fetching.current = true
|
||||
networkRetry(3, fetchUserNumber).catch(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
})
|
||||
}
|
||||
}, [
|
||||
@@ -136,12 +149,27 @@ export function TenMillion() {
|
||||
setUserNumber,
|
||||
nuxDialogs.dismissActiveNux,
|
||||
nuxDialogs,
|
||||
onFallback,
|
||||
])
|
||||
|
||||
return userNumber ? <TenMillionInner userNumber={userNumber} /> : null
|
||||
return userNumber ? (
|
||||
<TenMillionInner
|
||||
userNumber={userNumber}
|
||||
showTimeout={showTimeout ?? 3e3}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
export function TenMillionInner({
|
||||
userNumber,
|
||||
showTimeout,
|
||||
onClose: onCloseOuter,
|
||||
}: {
|
||||
userNumber: number
|
||||
showTimeout: number
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const lightTheme = useTheme('light')
|
||||
const {_, i18n} = useLingui()
|
||||
@@ -169,6 +197,27 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
const isLoadingData = isProfileLoading || !moderation || !profile
|
||||
const isLoadingImage = !uri
|
||||
|
||||
const displayName = React.useMemo(() => {
|
||||
if (!profile || !moderation) return ''
|
||||
return sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
}, [profile, moderation])
|
||||
const handle = React.useMemo(() => {
|
||||
if (!profile) return ''
|
||||
return sanitizeHandle(profile.handle, '@')
|
||||
}, [profile])
|
||||
const joinedDate = React.useMemo(() => {
|
||||
if (!profile || !profile.createdAt) return ''
|
||||
const date = i18n.date(profile.createdAt, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
return date
|
||||
}, [i18n, profile])
|
||||
|
||||
const error: string = React.useMemo(() => {
|
||||
if (profileError) {
|
||||
return _(
|
||||
@@ -184,14 +233,15 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
control.open()
|
||||
}, 3e3)
|
||||
}, showTimeout)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [control])
|
||||
}, [control, showTimeout])
|
||||
const onClose = React.useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
}, [nuxDialogs])
|
||||
onCloseOuter?.()
|
||||
}, [nuxDialogs, onCloseOuter])
|
||||
|
||||
/*
|
||||
* Actions
|
||||
@@ -206,19 +256,34 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
|
||||
userNumber,
|
||||
)}!`,
|
||||
), // TODO
|
||||
),
|
||||
imageUris: [
|
||||
{
|
||||
uri,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
altText: _(
|
||||
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
|
||||
userNumber,
|
||||
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
}, 1e3)
|
||||
})
|
||||
}
|
||||
}, [_, i18n, control, openComposer, uri, userNumber])
|
||||
}, [
|
||||
_,
|
||||
i18n,
|
||||
control,
|
||||
openComposer,
|
||||
uri,
|
||||
userNumber,
|
||||
displayName,
|
||||
handle,
|
||||
joinedDate,
|
||||
])
|
||||
const onNativeShare = React.useCallback(() => {
|
||||
if (uri) {
|
||||
control.close(() => {
|
||||
@@ -376,10 +441,10 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.font_heavy,
|
||||
{
|
||||
color: t.palette.primary_500,
|
||||
fontSize: 32,
|
||||
fontWeight: '900',
|
||||
width: 32,
|
||||
top: isNative ? -10 : 0,
|
||||
left: 0,
|
||||
@@ -397,11 +462,11 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
style={[
|
||||
a.relative,
|
||||
a.text_center,
|
||||
a.font_heavy,
|
||||
{
|
||||
fontStyle: 'italic',
|
||||
fontSize: getFontSize(userNumber),
|
||||
lineHeight: getFontSize(userNumber),
|
||||
fontWeight: '900',
|
||||
letterSpacing: -2,
|
||||
},
|
||||
]}>
|
||||
@@ -461,11 +526,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
a.leading_tight,
|
||||
{maxWidth: '60%'},
|
||||
]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName ||
|
||||
sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
{displayName}
|
||||
</Text>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
|
||||
@@ -475,11 +536,11 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_semibold,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
lightTheme.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
{handle}
|
||||
</Text>
|
||||
|
||||
{profile.createdAt && (
|
||||
@@ -490,19 +551,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_semibold,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
a.text_right,
|
||||
lightTheme.atoms.text_contrast_low,
|
||||
]}>
|
||||
<Trans>
|
||||
Joined{' '}
|
||||
{i18n.date(profile.createdAt, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Trans>
|
||||
<Trans>Joined on {joinedDate}</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
@@ -589,14 +643,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.text_5xl,
|
||||
a.leading_tight,
|
||||
a.pb_lg,
|
||||
{
|
||||
fontWeight: '900',
|
||||
},
|
||||
]}>
|
||||
style={[a.text_5xl, a.leading_tight, a.pb_lg, a.font_heavy]}>
|
||||
<Trans>Thanks for being one of our first 10 million users.</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -617,9 +664,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
a.gap_md,
|
||||
a.pt_xl,
|
||||
]}>
|
||||
<Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Brag a little!</Trans>
|
||||
</Text>
|
||||
{gtMobile && (
|
||||
<Text
|
||||
style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Brag a little!</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={isLoadingImage}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
@@ -8,9 +9,16 @@ import {
|
||||
useRemoveNuxsMutation,
|
||||
useUpsertNuxMutation,
|
||||
} from '#/state/queries/nuxs'
|
||||
import {useSession} from '#/state/session'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {SessionAccount, useSession} from '#/state/session'
|
||||
import {useOnboardingState} from '#/state/shell'
|
||||
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
// NUXs
|
||||
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
@@ -19,31 +27,28 @@ type Context = {
|
||||
dismissActiveNux: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* If we fail to complete a NUX here, it may show again on next reload,
|
||||
* or if prefs state updates. If `true`, this fallback ensures that the last
|
||||
* shown NUX won't show again, at least for this session.
|
||||
*
|
||||
* This is temporary, and only needed for the 10Milly dialog rn, since we
|
||||
* aren't snoozing that one in device storage.
|
||||
*/
|
||||
let __isSnoozedFallback = false
|
||||
|
||||
const queuedNuxs: {
|
||||
id: Nux
|
||||
enabled(props: {gate: ReturnType<typeof useGate>}): boolean
|
||||
/**
|
||||
* TEMP only intended for use with the 10Milly dialog rn, since there are no
|
||||
* other NUX dialogs configured
|
||||
*/
|
||||
unsafe_disableSnooze: boolean
|
||||
enabled?: (props: {
|
||||
gate: ReturnType<typeof useGate>
|
||||
currentAccount: SessionAccount
|
||||
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}) => boolean
|
||||
}[] = [
|
||||
{
|
||||
id: Nux.TenMillionDialog,
|
||||
enabled({gate}) {
|
||||
return gate('ten_million_dialog')
|
||||
},
|
||||
{
|
||||
id: Nux.NeueTypography,
|
||||
enabled(props) {
|
||||
if (props.currentProfile.createdAt) {
|
||||
if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
unsafe_disableSnooze: true,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -57,12 +62,31 @@ export function useNuxDialogContext() {
|
||||
}
|
||||
|
||||
export function NuxDialogs() {
|
||||
const {hasSession} = useSession()
|
||||
const onboardingState = useOnboardingState()
|
||||
return hasSession && !onboardingState.isActive ? <Inner /> : null
|
||||
const {currentAccount} = useSession()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
const onboardingActive = useOnboardingState().isActive
|
||||
|
||||
const isLoading =
|
||||
!currentAccount || !preferences || !profile || onboardingActive
|
||||
return !isLoading ? (
|
||||
<Inner
|
||||
currentAccount={currentAccount}
|
||||
currentProfile={profile}
|
||||
preferences={preferences}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
function Inner({
|
||||
currentAccount,
|
||||
currentProfile,
|
||||
preferences,
|
||||
}: {
|
||||
currentAccount: SessionAccount
|
||||
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}) {
|
||||
const gate = useGate()
|
||||
const {nuxs} = useNuxs()
|
||||
const [snoozed, setSnoozed] = React.useState(() => {
|
||||
@@ -92,30 +116,32 @@ function Inner() {
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (__isSnoozedFallback) return
|
||||
if (snoozed) return
|
||||
if (!nuxs) return
|
||||
|
||||
for (const {id, enabled, unsafe_disableSnooze} of queuedNuxs) {
|
||||
for (const {id, enabled} of queuedNuxs) {
|
||||
const nux = nuxs.find(nux => nux.id === id)
|
||||
|
||||
// check if completed first
|
||||
if (nux && nux.completed) continue
|
||||
if (nux && nux.completed) {
|
||||
continue
|
||||
}
|
||||
|
||||
// then check gate (track exposure)
|
||||
if (!enabled({gate})) continue
|
||||
if (
|
||||
enabled &&
|
||||
!enabled({gate, currentAccount, currentProfile, preferences})
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.debug(`NUX dialogs: activating '${id}' NUX`)
|
||||
|
||||
// we have a winner
|
||||
setActiveNux(id)
|
||||
|
||||
/**
|
||||
* TEMP only intended for use with the 10Milly dialog rn, since there are no
|
||||
* other NUX dialogs configured
|
||||
*/
|
||||
if (!unsafe_disableSnooze) {
|
||||
// immediately snooze for a day
|
||||
snoozeNuxDialog()
|
||||
}
|
||||
// immediately snooze for a day
|
||||
snoozeNuxDialog()
|
||||
|
||||
// immediately update remote data (affects next reload)
|
||||
upsertNux({
|
||||
@@ -126,17 +152,20 @@ function Inner() {
|
||||
logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
/*
|
||||
* TEMP only intended for use with the 10Milly dialog rn
|
||||
*/
|
||||
if (unsafe_disableSnooze) {
|
||||
__isSnoozedFallback = true
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}, [nuxs, snoozed, snoozeNuxDialog, upsertNux, gate])
|
||||
}, [
|
||||
nuxs,
|
||||
snoozed,
|
||||
snoozeNuxDialog,
|
||||
upsertNux,
|
||||
gate,
|
||||
currentAccount,
|
||||
currentProfile,
|
||||
preferences,
|
||||
])
|
||||
|
||||
const ctx = React.useMemo(() => {
|
||||
return {
|
||||
@@ -148,6 +177,7 @@ function Inner() {
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{activeNux === Nux.TenMillionDialog && <TenMillion />}
|
||||
{activeNux === Nux.NeueTypography && <NeueTypography />}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ function DialogInner({
|
||||
<Button
|
||||
label={_(msg`Start chatting`)}
|
||||
accessibilityHint={_(msg`Close modal`)}
|
||||
size="medium"
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={() => control.close()}>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function DateField({
|
||||
<Button
|
||||
label={_(msg`Done`)}
|
||||
onPress={() => control.close()}
|
||||
size="medium"
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid">
|
||||
<ButtonText>
|
||||
|
||||
@@ -351,8 +351,8 @@ export function Checkbox() {
|
||||
t.atoms.border_contrast_high,
|
||||
{
|
||||
borderWidth: 1,
|
||||
height: 20,
|
||||
width: 20,
|
||||
height: 24,
|
||||
width: 24,
|
||||
},
|
||||
baseStyles,
|
||||
hovered ? baseHoverStyles : {},
|
||||
@@ -383,9 +383,9 @@ export function Switch() {
|
||||
t.atoms.border_contrast_high,
|
||||
{
|
||||
borderWidth: 1,
|
||||
height: 20,
|
||||
width: 32,
|
||||
padding: 2,
|
||||
height: 24,
|
||||
width: 36,
|
||||
padding: 3,
|
||||
},
|
||||
baseStyles,
|
||||
hovered ? baseHoverStyles : {},
|
||||
@@ -395,8 +395,8 @@ export function Switch() {
|
||||
style={[
|
||||
a.rounded_full,
|
||||
{
|
||||
height: 14,
|
||||
width: 14,
|
||||
height: 16,
|
||||
width: 16,
|
||||
},
|
||||
selected
|
||||
? {
|
||||
@@ -436,8 +436,8 @@ export function Radio() {
|
||||
t.atoms.border_contrast_high,
|
||||
{
|
||||
borderWidth: 1,
|
||||
height: 20,
|
||||
width: 20,
|
||||
height: 24,
|
||||
width: 24,
|
||||
},
|
||||
baseStyles,
|
||||
hovered ? baseHoverStyles : {},
|
||||
@@ -447,7 +447,7 @@ export function Radio() {
|
||||
style={[
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
{height: 12, width: 12},
|
||||
{height: 16, width: 16},
|
||||
selected
|
||||
? {
|
||||
backgroundColor: t.palette.primary_500,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const AspectRatio11_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Z',
|
||||
})
|
||||
|
||||
export const AspectRatio43_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M2 20.5c-.552 0-1-.41-1-.917V4.917C1 4.41 1.448 4 2 4h20c.552 0 1 .41 1 .917v14.666c0 .507-.448.917-1 .917H2Zm1-1.833h18V5.833H3v12.834Z',
|
||||
})
|
||||
|
||||
export const AspectRatio34_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M4 2c0-.552.41-1 .917-1h14.666c.507 0 .917.448.917 1v20c0 .552-.41 1-.917 1H4.917C4.41 23 4 22.552 4 22V2Zm1.833 1v18h12.834V3H5.833Z',
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const FlipVertical_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v5h-2V5H5v4H3V4Zm20 9H1v-2h22v2Zm-2.293 7.707A1 1 0 0 1 20 21h-1v-2h2v1a1 1 0 0 1-.293.707ZM17 19v2h-2v-2h2Zm-4 0v2h-2v-2h2Zm-4 0v2H7v-2h2Zm-4 0v2H4a1 1 0 0 1-1-1v-1h2Zm0-2H3v-2h2v2Zm14-2v2h2v-2h-2Z',
|
||||
})
|
||||
|
||||
export const FlipHorizontal_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M4 21a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5v2H5v14h4v2H4Zm9-20v22h-2V1h2Zm7.707 2.293A1 1 0 0 1 21 4v1h-2V3h1a1 1 0 0 1 .707.293ZM19 7h2v2h-2V7Zm0 4h2v2h-2v-2Zm0 4h2v2h-2v-2Zm0 4h2v1a1 1 0 0 1-1 1h-1v-2Zm-2 0v2h-2v-2h2ZM15 5h2V3h-2v2Z',
|
||||
})
|
||||