New Onboarding (#2596)
* Add round and square buttons * Allow some style for buttons, add icons * Change text selection color * Center button text, whoops * Outer layout, some primitive updates * WIP * onboarding feed prefs (#2590) * add `style` to toggle label to modify text style * Revert "add `style` to toggle label to modify text style" This reverts commit 8f4b517b8585ca64a4bf44f6cb40ac070ece8932. * following feed prefs * remove unnecessary memo * reusable divider component * org imports * add finished screen * Theme SelectedAccountCard * Require at least 3 interests * Placeholder save logic * WIP algo feeds * Improve lineHeight handling, add RichText, improve Link by adding InlineLink * Inherit lineHeight in heading comps * Algo feeds mostly good * Topical feeds ish * Layout cleanup * Improve button styles * moderation prefs for onboarding (#2594) * WIP algo feeds * modify controlalbelgroup typing for easy .map() * adjust padding on button * add moderation screen * add moderation screen * add moderation screen --------- Co-authored-by: Eric Bailey <git@esb.lol> * Fix toggle button styles * A11y props on outer portal * Put it all on red * New data shape * Handle mock data * Bulk write (not yet) * Remove interests validation * Clean up interests * i18n layout and first step * Clean up suggested follows screen * Clean up following step * Clean up algo feeds step * Clean up topical feeds * Add skeleton for feed card * WIP moderation step * cleanup moderation styles (#2605) * cleanup moderation styles * fix(?) toggle button group styles * adjust toggle to fit any screen * Some more cleanup * Icons * ToggleButton tweaks * Reset * Hook up data * Better suggestions * Bulk write * Some logging * Use new api * Concat topical feeds * Metrics * Disable links in RichText, feedcards * Tweak primary feed cards * Update metrics * Fix layout shift * Fix ToggleButton again, whoops * Error state * Bump api package, ensure interests are saved * Better fix for autofill * i18n, button positions * Remove unused export * Add default prefs object * Fix overflow in user cards * Add 2 lines of bios to suggested accounts cards * Nits * Don't resolve facets by default * Update storybook * Disable flag for now * Remove age dialog from moderations step * Improvements and tweaks to new onboarding --------- Co-authored-by: Hailey <153161762+haileyok@users.noreply.github.com> Co-authored-by: Paul Frazee <pfrazee@gmail.com>zio/stable
|
@ -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="M5 3a1 1 0 0 1 1 1v1.423c.498-.46 1.02-.869 1.58-1.213C8.863 3.423 10.302 3 12.028 3a9 9 0 1 1-8.487 12 1 1 0 0 1 1.885-.667A7 7 0 1 0 12.028 5c-1.37 0-2.444.327-3.402.915-.474.29-.93.652-1.383 1.085H9a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 391 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="M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 756 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="M21.59 3.193a1 1 0 0 1 .217 1.397l-11.706 16a1 1 0 0 1-1.429.193l-6.294-5a1 1 0 1 1 1.244-1.566l5.48 4.353 11.09-15.16a1 1 0 0 1 1.398-.217Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 283 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="M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 263 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="M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 263 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="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 351 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="M6.343 6.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 6.343 6.343ZM19.071 4.93c-3.905-3.905-10.237-3.905-14.142 0-3.905 3.905-3.905 10.237 0 14.142 3.905 3.905 10.237 3.905 14.142 0 3.905-3.905 3.905-10.237 0-14.142Zm-3.537 9.535a5 5 0 0 0-7.07 0 1 1 0 1 0 1.413 1.415 3 3 0 0 1 4.243 0 1 1 0 0 0 1.414-1.415ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5ZM9.25 11c.69 0 1.25-.672 1.25-1.5S9.94 8 9.25 8 8 8.672 8 9.5 8.56 11 9.25 11Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 607 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.293 2.293a1 1 0 0 1 1.414 0L7.335 5.92l.03.03 3.22 3.222 4.243 4.242 3.22 3.22.03.03 3.63 3.629a1 1 0 0 1-1.415 1.414l-3.09-3.09c-2.65 1.478-5.625 1.778-8.421.869-3.039-.987-5.779-3.37-7.67-7.027a1 1 0 0 1 0-.918c1.086-2.1 2.452-3.78 3.996-5.019L2.293 3.707a1 1 0 0 1 0-1.414Zm4.24 5.654 2.021 2.021a4 4 0 0 0 5.478 5.478l1.688 1.688c-2.042.982-4.246 1.124-6.32.45-2.34-.76-4.594-2.586-6.265-5.584.97-1.739 2.135-3.083 3.398-4.053Zm3.535 3.535 2.45 2.45a2 2 0 0 1-2.45-2.45Zm.81-5.405c3.573-.49 7.45 1.369 9.987 5.923a14.797 14.797 0 0 1-1.347 2.02 1 1 0 1 0 1.564 1.247 17.078 17.078 0 0 0 1.806-2.808 1 1 0 0 0 0-.918c-2.833-5.479-7.584-8.088-12.281-7.446a1 1 0 0 0 .271 1.982Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 825 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="M7.002 5a1 1 0 0 0-2 0v11.587l-1.295-1.294a1 1 0 0 0-1.414 1.414l3.002 3a1 1 0 0 0 1.414 0l2.998-3a1 1 0 0 0-1.414-1.414l-1.291 1.292V5ZM16 16a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2h-4Zm-3-4a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1Zm-1-6a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2h-8Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 411 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-1h1a8.003 8.003 0 0 1 7.75 6.006A7.985 7.985 0 0 1 19 6h1a1 1 0 0 1 1 1v1a8 8 0 0 1-8 8v4a1 1 0 1 1-2 0v-7a8 8 0 0 1-8-8V4Zm2 1a6 6 0 0 1 6 6 6 6 0 0 1-6-6Zm8 9a6 6 0 0 1 6-6 6 6 0 0 1-6 6Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 349 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.124 3.008a1 1 0 0 1 .868 1.116L9.632 7h5.985l.39-3.124a1 1 0 0 1 1.985.248L17.632 7H20a1 1 0 1 1 0 2h-2.617l-.75 6H20a1 1 0 1 1 0 2h-3.617l-.39 3.124a1 1 0 1 1-1.985-.248l.36-2.876H8.382l-.39 3.124a1 1 0 1 1-1.985-.248L6.368 17H4a1 1 0 1 1 0-2h2.617l.75-6H4a1 1 0 1 1 0-2h3.617l.39-3.124a1 1 0 0 1 1.117-.868ZM9.383 9l-.75 6h5.984l.75-6H9.383Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 489 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-1h13a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm1 4a1 1 0 0 0 0 2h5a1 1 0 0 0 0-2H4Zm-1 7a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm0 5a1 1 0 0 1 1-1h13a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm9-8a4 4 0 1 1 7.446 2.032l.99.989a1 1 0 1 1-1.415 1.414l-.99-.989A4 4 0 0 1 12 12Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 452 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 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3a1 1 0 0 1 .92.606l1.342 3.132 3.132 1.343a1 1 0 0 1 0 1.838l-3.132 1.343-1.343 3.132a1 1 0 0 1-1.838 0l-1.343-3.132-3.132-1.343a1 1 0 0 1 0-1.838l3.132-1.343 1.343-3.132A1 1 0 0 1 17 9Zm0 3.539-.58 1.355a1 1 0 0 1-.526.525L14.539 15l1.355.58a1 1 0 0 1 .525.526L17 17.461l.58-1.355a1 1 0 0 1 .526-.525L19.461 15l-1.355-.58a1 1 0 0 1-.525-.526L17 12.539Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 629 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="M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 246 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="M1 5a1 1 0 0 1 1-1h7a3.99 3.99 0 0 1 3 1.354A3.99 3.99 0 0 1 15 4h7a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-6.723c-.52 0-1 .125-1.4.372-.421.26-.761.633-.983 1.075a1 1 0 0 1-1.788 0 2.664 2.664 0 0 0-.983-1.075c-.4-.247-.88-.372-1.4-.372H2a1 1 0 0 1-1-1V5Zm10 3a2 2 0 0 0-2-2H3v12h5.723c.776 0 1.564.173 2.277.569V8Zm2 10.569V8a2 2 0 0 1 2-2h6v12h-5.723c-.776 0-1.564.173-2.277.569Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 517 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="M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 237 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="m18.192 5.004 1.864 5.31a1 1 0 0 0 1.887-.662L20.08 4.34c-.665-1.893-3.378-1.741-3.834.207l-3.381 14.449-2.985-9.605C9.3 7.531 6.684 7.506 6.07 9.355l-1.18 3.56-.969-2.312a1 1 0 0 0-1.844.772l.97 2.315c.715 1.71 3.159 1.613 3.741-.144l1.18-3.56 2.985 9.605c.607 1.952 3.392 1.848 3.857-.138l3.381-14.449Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 447 B |
|
@ -39,25 +39,6 @@
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove autofill styles on Webkit */
|
|
||||||
input:-webkit-autofill,
|
|
||||||
input:-webkit-autofill:hover,
|
|
||||||
input:-webkit-autofill:focus,
|
|
||||||
textarea:-webkit-autofill,
|
|
||||||
textarea:-webkit-autofill:hover,
|
|
||||||
textarea:-webkit-autofill:focus,
|
|
||||||
select:-webkit-autofill,
|
|
||||||
select:-webkit-autofill:hover,
|
|
||||||
select:-webkit-autofill:focus {
|
|
||||||
border: 0;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
}
|
|
||||||
/* Force left-align date/time inputs on iOS mobile */
|
|
||||||
input::-webkit-date-and-time-value {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color theming */
|
/* Color theming */
|
||||||
:root {
|
:root {
|
||||||
--text: black;
|
--text: black;
|
||||||
|
@ -86,6 +67,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--backgroundLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* Allows you to scroll below the viewport; default value is visible */
|
/* Allows you to scroll below the viewport; default value is visible */
|
||||||
|
|
|
@ -104,6 +104,9 @@ export const atoms = {
|
||||||
flex: {
|
flex: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
|
flex_col: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
flex_row: {
|
flex_row: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
|
@ -149,45 +152,38 @@ export const atoms = {
|
||||||
},
|
},
|
||||||
text_2xs: {
|
text_2xs: {
|
||||||
fontSize: tokens.fontSize._2xs,
|
fontSize: tokens.fontSize._2xs,
|
||||||
lineHeight: tokens.fontSize._2xs,
|
|
||||||
},
|
},
|
||||||
text_xs: {
|
text_xs: {
|
||||||
fontSize: tokens.fontSize.xs,
|
fontSize: tokens.fontSize.xs,
|
||||||
lineHeight: tokens.fontSize.xs,
|
|
||||||
},
|
},
|
||||||
text_sm: {
|
text_sm: {
|
||||||
fontSize: tokens.fontSize.sm,
|
fontSize: tokens.fontSize.sm,
|
||||||
lineHeight: tokens.fontSize.sm,
|
|
||||||
},
|
},
|
||||||
text_md: {
|
text_md: {
|
||||||
fontSize: tokens.fontSize.md,
|
fontSize: tokens.fontSize.md,
|
||||||
lineHeight: tokens.fontSize.md,
|
|
||||||
},
|
},
|
||||||
text_lg: {
|
text_lg: {
|
||||||
fontSize: tokens.fontSize.lg,
|
fontSize: tokens.fontSize.lg,
|
||||||
lineHeight: tokens.fontSize.lg,
|
|
||||||
},
|
},
|
||||||
text_xl: {
|
text_xl: {
|
||||||
fontSize: tokens.fontSize.xl,
|
fontSize: tokens.fontSize.xl,
|
||||||
lineHeight: tokens.fontSize.xl,
|
|
||||||
},
|
},
|
||||||
text_2xl: {
|
text_2xl: {
|
||||||
fontSize: tokens.fontSize._2xl,
|
fontSize: tokens.fontSize._2xl,
|
||||||
lineHeight: tokens.fontSize._2xl,
|
|
||||||
},
|
},
|
||||||
text_3xl: {
|
text_3xl: {
|
||||||
fontSize: tokens.fontSize._3xl,
|
fontSize: tokens.fontSize._3xl,
|
||||||
lineHeight: tokens.fontSize._3xl,
|
|
||||||
},
|
},
|
||||||
text_4xl: {
|
text_4xl: {
|
||||||
fontSize: tokens.fontSize._4xl,
|
fontSize: tokens.fontSize._4xl,
|
||||||
lineHeight: tokens.fontSize._4xl,
|
|
||||||
},
|
},
|
||||||
text_5xl: {
|
text_5xl: {
|
||||||
fontSize: tokens.fontSize._5xl,
|
fontSize: tokens.fontSize._5xl,
|
||||||
lineHeight: tokens.fontSize._5xl,
|
|
||||||
},
|
},
|
||||||
leading_tight: {
|
leading_tight: {
|
||||||
|
lineHeight: 1.15,
|
||||||
|
},
|
||||||
|
leading_snug: {
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
leading_normal: {
|
leading_normal: {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||||
import {Dimensions} from 'react-native'
|
import {Dimensions} from 'react-native'
|
||||||
import * as themes from '#/alf/themes'
|
import * as themes from '#/alf/themes'
|
||||||
|
|
||||||
|
export * from '#/alf/types'
|
||||||
export * as tokens from '#/alf/tokens'
|
export * as tokens from '#/alf/tokens'
|
||||||
export {atoms} from '#/alf/atoms'
|
export {atoms} from '#/alf/atoms'
|
||||||
export * from '#/alf/util/platform'
|
export * from '#/alf/util/platform'
|
||||||
|
|
|
@ -142,6 +142,14 @@ export const gradients = {
|
||||||
],
|
],
|
||||||
hover_value: '#B88BB6',
|
hover_value: '#B88BB6',
|
||||||
},
|
},
|
||||||
|
summer: {
|
||||||
|
values: [
|
||||||
|
[0, '#FF6A56'],
|
||||||
|
[0.3, '#FF9156'],
|
||||||
|
[1, '#FFDD87'],
|
||||||
|
],
|
||||||
|
hover_value: '#FF9156',
|
||||||
|
},
|
||||||
nordic: {
|
nordic: {
|
||||||
values: [
|
values: [
|
||||||
[0, '#083367'],
|
[0, '#083367'],
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {StyleProp, ViewStyle, TextStyle} from 'react-native'
|
||||||
|
|
||||||
type LiteralToCommon<T extends PropertyKey> = T extends number
|
type LiteralToCommon<T extends PropertyKey> = T extends number
|
||||||
? number
|
? number
|
||||||
: T extends string
|
: T extends string
|
||||||
|
@ -14,3 +16,11 @@ export type Mutable<T> = {
|
||||||
? LiteralToCommon<T[K]>
|
? LiteralToCommon<T[K]>
|
||||||
: Mutable<T[K]>
|
: Mutable<T[K]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TextStyleProp = {
|
||||||
|
style?: StyleProp<TextStyle>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ViewStyleProp = {
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}
|
||||||
|
|
|
@ -9,10 +9,11 @@ import {
|
||||||
View,
|
View,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
StyleProp,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
|
|
||||||
import {useTheme, atoms as a, tokens, web, native} from '#/alf'
|
import {useTheme, atoms as a, tokens, android, flatten} from '#/alf'
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||||
|
|
||||||
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
|
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
|
||||||
|
@ -27,6 +28,7 @@ export type ButtonColor =
|
||||||
| 'gradient_nordic'
|
| 'gradient_nordic'
|
||||||
| 'gradient_bonfire'
|
| 'gradient_bonfire'
|
||||||
export type ButtonSize = 'small' | 'large'
|
export type ButtonSize = 'small' | 'large'
|
||||||
|
export type ButtonShape = 'round' | 'square' | 'default'
|
||||||
export type VariantProps = {
|
export type VariantProps = {
|
||||||
/**
|
/**
|
||||||
* The style variation of the button
|
* The style variation of the button
|
||||||
|
@ -40,6 +42,10 @@ export type VariantProps = {
|
||||||
* The size of the button
|
* The size of the button
|
||||||
*/
|
*/
|
||||||
size?: ButtonSize
|
size?: ButtonSize
|
||||||
|
/**
|
||||||
|
* The shape of the button
|
||||||
|
*/
|
||||||
|
shape?: ButtonShape
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonProps = React.PropsWithChildren<
|
export type ButtonProps = React.PropsWithChildren<
|
||||||
|
@ -47,6 +53,7 @@ export type ButtonProps = React.PropsWithChildren<
|
||||||
AccessibilityProps &
|
AccessibilityProps &
|
||||||
VariantProps & {
|
VariantProps & {
|
||||||
label: string
|
label: string
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||||
|
@ -74,8 +81,10 @@ export function Button({
|
||||||
variant,
|
variant,
|
||||||
color,
|
color,
|
||||||
size,
|
size,
|
||||||
|
shape = 'default',
|
||||||
label,
|
label,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
style,
|
||||||
...rest
|
...rest
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
@ -175,18 +184,18 @@ export function Button({
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: light
|
backgroundColor: light
|
||||||
? tokens.color.gray_100
|
? tokens.color.gray_50
|
||||||
: tokens.color.gray_900,
|
: tokens.color.gray_900,
|
||||||
})
|
})
|
||||||
hoverStyles.push({
|
hoverStyles.push({
|
||||||
backgroundColor: light
|
backgroundColor: light
|
||||||
? tokens.color.gray_200
|
? tokens.color.gray_100
|
||||||
: tokens.color.gray_950,
|
: tokens.color.gray_950,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: light
|
backgroundColor: light
|
||||||
? tokens.color.gray_300
|
? tokens.color.gray_200
|
||||||
: tokens.color.gray_950,
|
: tokens.color.gray_950,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -197,7 +206,7 @@ export function Button({
|
||||||
|
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push(a.border, {
|
baseStyles.push(a.border, {
|
||||||
borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500,
|
borderColor: light ? tokens.color.gray_300 : tokens.color.gray_700,
|
||||||
})
|
})
|
||||||
hoverStyles.push(a.border, t.atoms.bg_contrast_50)
|
hoverStyles.push(a.border, t.atoms.bg_contrast_50)
|
||||||
} else {
|
} else {
|
||||||
|
@ -262,10 +271,28 @@ export function Button({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shape === 'default') {
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm)
|
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm)
|
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
||||||
|
}
|
||||||
|
} else if (shape === 'round' || shape === 'square') {
|
||||||
|
if (size === 'large') {
|
||||||
|
if (shape === 'round') {
|
||||||
|
baseStyles.push({height: 54, width: 54})
|
||||||
|
} else {
|
||||||
|
baseStyles.push({height: 50, width: 50})
|
||||||
|
}
|
||||||
|
} else if (size === 'small') {
|
||||||
|
baseStyles.push({height: 40, width: 40})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape === 'round') {
|
||||||
|
baseStyles.push(a.rounded_full)
|
||||||
|
} else if (shape === 'square') {
|
||||||
|
baseStyles.push(a.rounded_sm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -278,7 +305,7 @@ export function Button({
|
||||||
} as ViewStyle,
|
} as ViewStyle,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}, [t, variant, color, size, disabled])
|
}, [t, variant, color, size, shape, disabled])
|
||||||
|
|
||||||
const {gradientColors, gradientHoverColors, gradientLocations} =
|
const {gradientColors, gradientHoverColors, gradientLocations} =
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
|
@ -334,8 +361,10 @@ export function Button({
|
||||||
disabled: disabled || false,
|
disabled: disabled || false,
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
|
flatten(style),
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
...baseStyles,
|
...baseStyles,
|
||||||
|
@ -462,17 +491,9 @@ export function useSharedButtonTextStyles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push(
|
baseStyles.push(a.text_md, android({paddingBottom: 1}))
|
||||||
a.text_md,
|
|
||||||
web({paddingBottom: 1}),
|
|
||||||
native({marginTop: 2}),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push(
|
baseStyles.push(a.text_sm, android({paddingBottom: 1}))
|
||||||
a.text_md,
|
|
||||||
web({paddingBottom: 1}),
|
|
||||||
native({marginTop: 2}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return StyleSheet.flatten(baseStyles)
|
return StyleSheet.flatten(baseStyles)
|
||||||
|
@ -491,14 +512,24 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||||
|
|
||||||
export function ButtonIcon({
|
export function ButtonIcon({
|
||||||
icon: Comp,
|
icon: Comp,
|
||||||
|
position,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
|
position?: 'left' | 'right'
|
||||||
}) {
|
}) {
|
||||||
const {size} = useButtonContext()
|
const {size, disabled} = useButtonContext()
|
||||||
const textStyles = useSharedButtonTextStyles()
|
const textStyles = useSharedButtonTextStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.z_20]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.z_20,
|
||||||
|
{
|
||||||
|
opacity: disabled ? 0.7 : 1,
|
||||||
|
marginLeft: position === 'left' ? -2 : 0,
|
||||||
|
marginRight: position === 'right' ? -2 : 0,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
<Comp
|
<Comp
|
||||||
size={size === 'large' ? 'md' : 'sm'}
|
size={size === 'large' ? 'md' : 'sm'}
|
||||||
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {ViewStyleProp} from '#/alf'
|
||||||
|
|
||||||
|
export function Divider({style}: ViewStyleProp) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return <View style={[a.w_full, a.border_t, t.atoms.border, style]} />
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Text,
|
|
||||||
TextStyle,
|
|
||||||
StyleProp,
|
|
||||||
GestureResponderEvent,
|
GestureResponderEvent,
|
||||||
Linking,
|
Linking,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
import {
|
||||||
useLinkProps,
|
useLinkProps,
|
||||||
|
@ -13,9 +11,10 @@ import {
|
||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||||
|
|
||||||
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useTheme, web, flatten} from '#/alf'
|
import {useTheme, web, flatten, TextStyleProp} from '#/alf'
|
||||||
import {Button, ButtonProps, useButtonContext} from '#/components/Button'
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||||
import {
|
import {
|
||||||
convertBskyAppUrlIfNeeded,
|
convertBskyAppUrlIfNeeded,
|
||||||
|
@ -24,43 +23,39 @@ import {
|
||||||
} from '#/lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export type LinkProps = Omit<
|
/**
|
||||||
ButtonProps,
|
* Only available within a `Link`, since that inherits from `Button`.
|
||||||
'style' | 'onPress' | 'disabled' | 'label'
|
* `InlineLink` provides no context.
|
||||||
> & {
|
|
||||||
/**
|
|
||||||
* `TextStyle` to apply to the anchor element itself. Does not apply to any children.
|
|
||||||
*/
|
*/
|
||||||
style?: StyleProp<TextStyle>
|
export {useButtonContext as useLinkContext} from '#/components/Button'
|
||||||
|
|
||||||
|
type BaseLinkProps = Pick<
|
||||||
|
Parameters<typeof useLinkProps<AllNavigatorParams>>[0],
|
||||||
|
'to'
|
||||||
|
> & {
|
||||||
/**
|
/**
|
||||||
* The React Navigation `StackAction` to perform when the link is pressed.
|
* The React Navigation `StackAction` to perform when the link is pressed.
|
||||||
*/
|
*/
|
||||||
action?: 'push' | 'replace' | 'navigate'
|
action?: 'push' | 'replace' | 'navigate'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, will warn the user if the link text does not match the href. Only
|
* If true, will warn the user if the link text does not match the href.
|
||||||
* works for Links with children that are strings i.e. text links.
|
*
|
||||||
|
* Note: atm this only works for `InlineLink`s with a string child.
|
||||||
*/
|
*/
|
||||||
warnOnMismatchingTextChild?: boolean
|
warnOnMismatchingTextChild?: boolean
|
||||||
label?: ButtonProps['label']
|
}
|
||||||
} & Pick<Parameters<typeof useLinkProps<AllNavigatorParams>>[0], 'to'>
|
|
||||||
|
|
||||||
/**
|
export function useLink({
|
||||||
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
|
||||||
* will translate the `href` to navigator screens and params and dispatch a
|
|
||||||
* navigation action.
|
|
||||||
*
|
|
||||||
* Intended to behave as a web anchor tag. For more complex routing, use a
|
|
||||||
* `Button`.
|
|
||||||
*/
|
|
||||||
export function Link({
|
|
||||||
children,
|
|
||||||
to,
|
to,
|
||||||
|
displayText,
|
||||||
action = 'push',
|
action = 'push',
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
style,
|
}: BaseLinkProps & {
|
||||||
...rest
|
displayText: string
|
||||||
}: LinkProps) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {href} = useLinkProps<AllNavigatorParams>({
|
const {href} = useLinkProps<AllNavigatorParams>({
|
||||||
to:
|
to:
|
||||||
|
@ -68,14 +63,14 @@ export function Link({
|
||||||
})
|
})
|
||||||
const isExternal = isExternalUrl(href)
|
const isExternal = isExternalUrl(href)
|
||||||
const {openModal, closeModal} = useModalControls()
|
const {openModal, closeModal} = useModalControls()
|
||||||
|
|
||||||
const onPress = React.useCallback(
|
const onPress = React.useCallback(
|
||||||
(e: GestureResponderEvent) => {
|
(e: GestureResponderEvent) => {
|
||||||
const stringChildren = typeof children === 'string' ? children : ''
|
|
||||||
const requiresWarning = Boolean(
|
const requiresWarning = Boolean(
|
||||||
warnOnMismatchingTextChild &&
|
warnOnMismatchingTextChild &&
|
||||||
stringChildren &&
|
displayText &&
|
||||||
isExternal &&
|
isExternal &&
|
||||||
linkRequiresWarning(href, stringChildren),
|
linkRequiresWarning(href, displayText),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (requiresWarning) {
|
if (requiresWarning) {
|
||||||
|
@ -83,7 +78,7 @@ export function Link({
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
name: 'link-warning',
|
name: 'link-warning',
|
||||||
text: stringChildren,
|
text: displayText,
|
||||||
href: href,
|
href: href,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -134,12 +129,42 @@ export function Link({
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
navigation,
|
navigation,
|
||||||
action,
|
action,
|
||||||
children,
|
displayText,
|
||||||
closeModal,
|
closeModal,
|
||||||
openModal,
|
openModal,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isExternal,
|
||||||
|
href,
|
||||||
|
onPress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
||||||
|
Omit<ButtonProps, 'style' | 'onPress' | 'disabled' | 'label'> & {
|
||||||
|
/**
|
||||||
|
* Label for a11y. Defaults to the href.
|
||||||
|
*/
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
||||||
|
* will translate the `href` to navigator screens and params and dispatch a
|
||||||
|
* navigation action.
|
||||||
|
*
|
||||||
|
* Intended to behave as a web anchor tag. For more complex routing, use a
|
||||||
|
* `Button`.
|
||||||
|
*/
|
||||||
|
export function Link({children, to, action = 'push', ...rest}: LinkProps) {
|
||||||
|
const {href, isExternal, onPress} = useLink({
|
||||||
|
to,
|
||||||
|
displayText: typeof children === 'string' ? children : '',
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
label={href}
|
label={href}
|
||||||
|
@ -158,34 +183,81 @@ export function Link({
|
||||||
noUnderline: '1',
|
noUnderline: '1',
|
||||||
},
|
},
|
||||||
})}>
|
})}>
|
||||||
{typeof children === 'string' ? (
|
{children}
|
||||||
<LinkText style={style}>{children}</LinkText>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LinkText({
|
export type InlineLinkProps = React.PropsWithChildren<
|
||||||
|
BaseLinkProps &
|
||||||
|
TextStyleProp & {
|
||||||
|
/**
|
||||||
|
* Label for a11y. Defaults to the href.
|
||||||
|
*/
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
export function InlineLink({
|
||||||
children,
|
children,
|
||||||
|
to,
|
||||||
|
action = 'push',
|
||||||
|
warnOnMismatchingTextChild,
|
||||||
style,
|
style,
|
||||||
}: React.PropsWithChildren<{
|
...rest
|
||||||
style?: StyleProp<TextStyle>
|
}: InlineLinkProps) {
|
||||||
}>) {
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {hovered} = useButtonContext()
|
const stringChildren = typeof children === 'string'
|
||||||
|
const {href, isExternal, onPress} = useLink({
|
||||||
|
to,
|
||||||
|
displayText: stringChildren ? children : '',
|
||||||
|
action,
|
||||||
|
warnOnMismatchingTextChild,
|
||||||
|
})
|
||||||
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
|
const {
|
||||||
|
state: pressed,
|
||||||
|
onIn: onPressIn,
|
||||||
|
onOut: onPressOut,
|
||||||
|
} = useInteractionState()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={onPressIn}
|
||||||
|
onPressOut={onPressOut}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}>
|
||||||
<Text
|
<Text
|
||||||
|
label={href}
|
||||||
|
{...rest}
|
||||||
style={[
|
style={[
|
||||||
{color: t.palette.primary_500},
|
{color: t.palette.primary_500},
|
||||||
hovered && {
|
(focused || pressed) && {
|
||||||
|
outline: 0,
|
||||||
textDecorationLine: 'underline',
|
textDecorationLine: 'underline',
|
||||||
textDecorationColor: t.palette.primary_500,
|
textDecorationColor: t.palette.primary_500,
|
||||||
},
|
},
|
||||||
flatten(style),
|
flatten(style),
|
||||||
]}>
|
]}
|
||||||
{children as string}
|
role="link"
|
||||||
|
accessibilityRole="link"
|
||||||
|
href={href}
|
||||||
|
{...web({
|
||||||
|
hrefAttrs: {
|
||||||
|
target: isExternal ? 'blank' : undefined,
|
||||||
|
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||||
|
},
|
||||||
|
dataSet: stringChildren
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
// default to no underline, apply this ourselves
|
||||||
|
noUnderline: '1',
|
||||||
|
},
|
||||||
|
})}>
|
||||||
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,14 @@ type ComponentMap = {
|
||||||
[id: string]: Component
|
[id: string]: Component
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Context = React.createContext<ContextType>({
|
export function createPortalGroup() {
|
||||||
|
const Context = React.createContext<ContextType>({
|
||||||
outlet: null,
|
outlet: null,
|
||||||
append: () => {},
|
append: () => {},
|
||||||
remove: () => {},
|
remove: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Provider(props: React.PropsWithChildren<{}>) {
|
function Provider(props: React.PropsWithChildren<{}>) {
|
||||||
const map = React.useRef<ComponentMap>({})
|
const map = React.useRef<ComponentMap>({})
|
||||||
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
|
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
|
||||||
|
|
||||||
|
@ -38,14 +39,14 @@ export function Provider(props: React.PropsWithChildren<{}>) {
|
||||||
{props.children}
|
{props.children}
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outlet() {
|
function Outlet() {
|
||||||
const ctx = React.useContext(Context)
|
const ctx = React.useContext(Context)
|
||||||
return ctx.outlet
|
return ctx.outlet
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Portal({children}: React.PropsWithChildren<{}>) {
|
function Portal({children}: React.PropsWithChildren<{}>) {
|
||||||
const {append, remove} = React.useContext(Context)
|
const {append, remove} = React.useContext(Context)
|
||||||
const id = React.useId()
|
const id = React.useId()
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -53,4 +54,12 @@ export function Portal({children}: React.PropsWithChildren<{}>) {
|
||||||
return () => remove(id)
|
return () => remove(id)
|
||||||
}, [id, children, append, remove])
|
}, [id, children, append, remove])
|
||||||
return null
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {Provider, Outlet, Portal}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultPortal = createPortalGroup()
|
||||||
|
export const Provider = DefaultPortal.Provider
|
||||||
|
export const Outlet = DefaultPortal.Outlet
|
||||||
|
export const Portal = DefaultPortal.Portal
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'
|
||||||
|
|
||||||
|
import {atoms as a, TextStyleProp} from '#/alf'
|
||||||
|
import {InlineLink} from '#/components/Link'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {toShortUrl} from 'lib/strings/url-helpers'
|
||||||
|
import {getAgent} from '#/state/session'
|
||||||
|
|
||||||
|
const WORD_WRAP = {wordWrap: 1}
|
||||||
|
|
||||||
|
export function RichText({
|
||||||
|
testID,
|
||||||
|
value,
|
||||||
|
style,
|
||||||
|
numberOfLines,
|
||||||
|
disableLinks,
|
||||||
|
resolveFacets = false,
|
||||||
|
}: TextStyleProp & {
|
||||||
|
value: RichTextAPI | string
|
||||||
|
testID?: string
|
||||||
|
numberOfLines?: number
|
||||||
|
disableLinks?: boolean
|
||||||
|
resolveFacets?: boolean
|
||||||
|
}) {
|
||||||
|
const detected = React.useRef(false)
|
||||||
|
const [richText, setRichText] = React.useState<RichTextAPI>(() =>
|
||||||
|
value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
|
||||||
|
)
|
||||||
|
const styles = [a.leading_normal, style]
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!resolveFacets) return
|
||||||
|
|
||||||
|
async function detectFacets() {
|
||||||
|
const rt = new RichTextAPI({text: richText.text})
|
||||||
|
await rt.detectFacets(getAgent())
|
||||||
|
setRichText(rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detected.current) {
|
||||||
|
detected.current = true
|
||||||
|
detectFacets()
|
||||||
|
}
|
||||||
|
}, [richText, setRichText, resolveFacets])
|
||||||
|
|
||||||
|
const {text, facets} = richText
|
||||||
|
|
||||||
|
if (!facets?.length) {
|
||||||
|
if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
testID={testID}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
fontSize: 26,
|
||||||
|
lineHeight: 30,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
dataSet={WORD_WRAP}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
testID={testID}
|
||||||
|
style={styles}
|
||||||
|
numberOfLines={numberOfLines}
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
dataSet={WORD_WRAP}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const els = []
|
||||||
|
let key = 0
|
||||||
|
// N.B. must access segments via `richText.segments`, not via destructuring
|
||||||
|
for (const segment of richText.segments()) {
|
||||||
|
const link = segment.link
|
||||||
|
const mention = segment.mention
|
||||||
|
if (
|
||||||
|
mention &&
|
||||||
|
AppBskyRichtextFacet.validateMention(mention).success &&
|
||||||
|
!disableLinks
|
||||||
|
) {
|
||||||
|
els.push(
|
||||||
|
<InlineLink
|
||||||
|
key={key}
|
||||||
|
to={`/profile/${mention.did}`}
|
||||||
|
style={[...styles, {pointerEvents: 'auto'}]}
|
||||||
|
// @ts-ignore TODO
|
||||||
|
dataSet={WORD_WRAP}>
|
||||||
|
{segment.text}
|
||||||
|
</InlineLink>,
|
||||||
|
)
|
||||||
|
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
||||||
|
if (disableLinks) {
|
||||||
|
els.push(toShortUrl(segment.text))
|
||||||
|
} else {
|
||||||
|
els.push(
|
||||||
|
<InlineLink
|
||||||
|
key={key}
|
||||||
|
to={link.uri}
|
||||||
|
style={[...styles, {pointerEvents: 'auto'}]}
|
||||||
|
// @ts-ignore TODO
|
||||||
|
dataSet={WORD_WRAP}
|
||||||
|
warnOnMismatchingLabel>
|
||||||
|
{toShortUrl(segment.text)}
|
||||||
|
</InlineLink>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
els.push(segment.text)
|
||||||
|
}
|
||||||
|
key++
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
testID={testID}
|
||||||
|
style={styles}
|
||||||
|
numberOfLines={numberOfLines}
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
dataSet={WORD_WRAP}>
|
||||||
|
{els}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,11 +1,50 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Text as RNText, TextProps} from 'react-native'
|
import {Text as RNText, TextStyle, TextProps} from 'react-native'
|
||||||
|
|
||||||
import {useTheme, atoms, web, flatten} from '#/alf'
|
import {useTheme, atoms, web, flatten} from '#/alf'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util to calculate lineHeight from a text size atom and a leading atom
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
|
||||||
|
*/
|
||||||
|
export function leading<
|
||||||
|
Size extends {fontSize?: number},
|
||||||
|
Leading extends {lineHeight?: number},
|
||||||
|
>(textSize: Size, leading: Leading) {
|
||||||
|
const size = textSize?.fontSize || atoms.text_md.fontSize
|
||||||
|
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
|
||||||
|
return size * lineHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
|
||||||
|
* other relative leading atoms.
|
||||||
|
*
|
||||||
|
* If the `lineHeight` value is > 2, we assume it's an absolute value and
|
||||||
|
* returns it as-is.
|
||||||
|
*/
|
||||||
|
function normalizeTextStyles(styles: TextStyle[]) {
|
||||||
|
const s = flatten(styles)
|
||||||
|
// should always be defined on these components
|
||||||
|
const fontSize = s.fontSize || atoms.text_md.fontSize
|
||||||
|
|
||||||
|
if (s?.lineHeight) {
|
||||||
|
if (s.lineHeight <= 2) {
|
||||||
|
s.lineHeight = fontSize * s.lineHeight
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.lineHeight = fontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
export function Text({style, ...rest}: TextProps) {
|
export function Text({style, ...rest}: TextProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
|
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
|
||||||
|
return <RNText style={s} {...rest} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function H1({style, ...rest}: TextProps) {
|
export function H1({style, ...rest}: TextProps) {
|
||||||
|
@ -19,7 +58,12 @@ export function H1({style, ...rest}: TextProps) {
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[atoms.text_5xl, atoms.font_bold, t.atoms.text, flatten(style)]}
|
style={normalizeTextStyles([
|
||||||
|
atoms.text_5xl,
|
||||||
|
atoms.font_bold,
|
||||||
|
t.atoms.text,
|
||||||
|
flatten(style),
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -35,7 +79,12 @@ export function H2({style, ...rest}: TextProps) {
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[atoms.text_4xl, atoms.font_bold, t.atoms.text, flatten(style)]}
|
style={normalizeTextStyles([
|
||||||
|
atoms.text_4xl,
|
||||||
|
atoms.font_bold,
|
||||||
|
t.atoms.text,
|
||||||
|
flatten(style),
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +100,12 @@ export function H3({style, ...rest}: TextProps) {
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[atoms.text_3xl, atoms.font_bold, t.atoms.text, flatten(style)]}
|
style={normalizeTextStyles([
|
||||||
|
atoms.text_3xl,
|
||||||
|
atoms.font_bold,
|
||||||
|
t.atoms.text,
|
||||||
|
flatten(style),
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -67,7 +121,12 @@ export function H4({style, ...rest}: TextProps) {
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[atoms.text_2xl, atoms.font_bold, t.atoms.text, flatten(style)]}
|
style={normalizeTextStyles([
|
||||||
|
atoms.text_2xl,
|
||||||
|
atoms.font_bold,
|
||||||
|
t.atoms.text,
|
||||||
|
flatten(style),
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -83,7 +142,12 @@ export function H5({style, ...rest}: TextProps) {
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[atoms.text_xl, atoms.font_bold, t.atoms.text, flatten(style)]}
|
style={normalizeTextStyles([
|
||||||
|
atoms.text_xl,
|
||||||
|
atoms.font_bold,
|
||||||
|
t.atoms.text,
|
||||||
|
flatten(style),
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +163,12 @@ export function H6({style, ...rest}: TextProps) {
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[atoms.text_lg, atoms.font_bold, t.atoms.text, flatten(style)]}
|
style={normalizeTextStyles([
|
||||||
|
atoms.text_lg,
|
||||||
|
atoms.font_bold,
|
||||||
|
t.atoms.text,
|
||||||
|
flatten(style),
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -110,15 +179,16 @@ export function P({style, ...rest}: TextProps) {
|
||||||
web({
|
web({
|
||||||
role: 'paragraph',
|
role: 'paragraph',
|
||||||
}) || {}
|
}) || {}
|
||||||
const _style = flatten(style)
|
|
||||||
const lineHeight =
|
|
||||||
(_style?.lineHeight || atoms.text_md.lineHeight) *
|
|
||||||
atoms.leading_normal.lineHeight
|
|
||||||
return (
|
return (
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[atoms.text_md, t.atoms.text, _style, {lineHeight}]}
|
style={normalizeTextStyles([
|
||||||
|
atoms.text_md,
|
||||||
|
atoms.leading_normal,
|
||||||
|
t.atoms.text,
|
||||||
|
flatten(style),
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,7 +208,7 @@ export function createInput(Component: typeof TextInput) {
|
||||||
paddingBottom: 2,
|
paddingBottom: 2,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
lineHeight: a.text_md.lineHeight * 1.1875,
|
lineHeight: a.text_md.fontSize * 1.1875,
|
||||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||||
minHeight: rest.multiline ? 60 : undefined,
|
minHeight: rest.multiline ? 60 : undefined,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import {Pressable, View, ViewStyle} from 'react-native'
|
import {Pressable, View, ViewStyle} from 'react-native'
|
||||||
|
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
import {useTheme, atoms as a, web, native} from '#/alf'
|
import {useTheme, atoms as a, web, native, flatten, ViewStyleProp} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ export type GroupProps = React.PropsWithChildren<{
|
||||||
label: string
|
label: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type ItemProps = {
|
export type ItemProps = ViewStyleProp & {
|
||||||
type?: 'radio' | 'checkbox'
|
type?: 'radio' | 'checkbox'
|
||||||
name: string
|
name: string
|
||||||
label: string
|
label: string
|
||||||
|
@ -57,7 +57,6 @@ export type ItemProps = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onChange?: (selected: boolean) => void
|
onChange?: (selected: boolean) => void
|
||||||
isInvalid?: boolean
|
isInvalid?: boolean
|
||||||
style?: (state: ItemState) => ViewStyle
|
|
||||||
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
|
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +124,7 @@ export function Group({
|
||||||
return (
|
return (
|
||||||
<GroupContext.Provider value={context}>
|
<GroupContext.Provider value={context}>
|
||||||
<View
|
<View
|
||||||
|
style={[a.w_full]}
|
||||||
role={groupRole}
|
role={groupRole}
|
||||||
{...(groupRole === 'radiogroup'
|
{...(groupRole === 'radiogroup'
|
||||||
? {
|
? {
|
||||||
|
@ -224,7 +224,7 @@ export function Item({
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
focused ? web({outline: 'none'}) : {},
|
focused ? web({outline: 'none'}) : {},
|
||||||
style?.(state),
|
flatten(style),
|
||||||
]}>
|
]}>
|
||||||
{typeof children === 'function' ? children(state) : children}
|
{typeof children === 'function' ? children(state) : children}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function Group({children, multiple, ...props}: GroupProps) {
|
||||||
<Toggle.Group type={multiple ? 'checkbox' : 'radio'} {...props}>
|
<Toggle.Group type={multiple ? 'checkbox' : 'radio'} {...props}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
a.w_full,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.border,
|
a.border,
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
|
@ -34,7 +35,7 @@ export function Group({children, multiple, ...props}: GroupProps) {
|
||||||
|
|
||||||
export function Button({children, ...props}: ItemProps) {
|
export function Button({children, ...props}: ItemProps) {
|
||||||
return (
|
return (
|
||||||
<Toggle.Item {...props}>
|
<Toggle.Item {...props} style={[a.flex_grow]}>
|
||||||
<ButtonInner>{children}</ButtonInner>
|
<ButtonInner>{children}</ButtonInner>
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
)
|
)
|
||||||
|
@ -95,11 +96,12 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
|
||||||
borderLeftWidth: 1,
|
borderLeftWidth: 1,
|
||||||
marginLeft: -1,
|
marginLeft: -1,
|
||||||
},
|
},
|
||||||
a.px_lg,
|
a.flex_grow,
|
||||||
a.py_md,
|
a.py_md,
|
||||||
native({
|
native({
|
||||||
paddingTop: 14,
|
paddingBottom: 10,
|
||||||
}),
|
}),
|
||||||
|
a.px_sm,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
t.atoms.border,
|
t.atoms.border,
|
||||||
baseStyles,
|
baseStyles,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded =
|
||||||
|
createSinglePathSVG({
|
||||||
|
path: 'M5 3a1 1 0 0 1 1 1v1.423c.498-.46 1.02-.869 1.58-1.213C8.863 3.423 10.302 3 12.028 3a9 9 0 1 1-8.487 12 1 1 0 0 1 1.885-.667A7 7 0 1 0 12.028 5c-1.37 0-2.444.327-3.402.915-.474.29-.93.652-1.383 1.085H9a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const At_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Check_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M21.59 3.193a1 1 0 0 1 .217 1.397l-11.706 16a1 1 0 0 1-1.429.193l-6.294-5a1 1 0 1 1 1.244-1.566l5.48 4.353 11.09-15.16a1 1 0 0 1 1.398-.217Z',
|
||||||
|
})
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const CircleInfo_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const EmojiSad_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M6.343 6.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 6.343 6.343ZM19.071 4.93c-3.905-3.905-10.237-3.905-14.142 0-3.905 3.905-3.905 10.237 0 14.142 3.905 3.905 10.237 3.905 14.142 0 3.905-3.905 3.905-10.237 0-14.142Zm-3.537 9.535a5 5 0 0 0-7.07 0 1 1 0 1 0 1.413 1.415 3 3 0 0 1 4.243 0 1 1 0 0 0 1.414-1.415ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5ZM9.25 11c.69 0 1.25-.672 1.25-1.5S9.94 8 9.25 8 8 8.672 8 9.5 8.56 11 9.25 11Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const EyeSlash_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M2.293 2.293a1 1 0 0 1 1.414 0L7.335 5.92l.03.03 3.22 3.222 4.243 4.242 3.22 3.22.03.03 3.63 3.629a1 1 0 0 1-1.415 1.414l-3.09-3.09c-2.65 1.478-5.625 1.778-8.421.869-3.039-.987-5.779-3.37-7.67-7.027a1 1 0 0 1 0-.918c1.086-2.1 2.452-3.78 3.996-5.019L2.293 3.707a1 1 0 0 1 0-1.414Zm4.24 5.654 2.021 2.021a4 4 0 0 0 5.478 5.478l1.688 1.688c-2.042.982-4.246 1.124-6.32.45-2.34-.76-4.594-2.586-6.265-5.584.97-1.739 2.135-3.083 3.398-4.053Zm3.535 3.535 2.45 2.45a2 2 0 0 1-2.45-2.45Zm.81-5.405c3.573-.49 7.45 1.369 9.987 5.923a14.797 14.797 0 0 1-1.347 2.02 1 1 0 1 0 1.564 1.247 17.078 17.078 0 0 0 1.806-2.808 1 1 0 0 0 0-.918c-2.833-5.479-7.584-8.088-12.281-7.446a1 1 0 0 0 .271 1.982Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const FilterTimeline_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M7.002 5a1 1 0 0 0-2 0v11.587l-1.295-1.294a1 1 0 0 0-1.414 1.414l3.002 3a1 1 0 0 0 1.414 0l2.998-3a1 1 0 0 0-1.414-1.414l-1.291 1.292V5ZM16 16a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2h-4Zm-3-4a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1Zm-1-6a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2h-8Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Growth_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M3 4a1 1 0 0 1 1-1h1a8.003 8.003 0 0 1 7.75 6.006A7.985 7.985 0 0 1 19 6h1a1 1 0 0 1 1 1v1a8 8 0 0 1-8 8v4a1 1 0 1 1-2 0v-7a8 8 0 0 1-8-8V4Zm2 1a6 6 0 0 1 6 6 6 6 0 0 1-6-6Zm8 9a6 6 0 0 1 6-6 6 6 0 0 1-6 6Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Hashtag_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M9.124 3.008a1 1 0 0 1 .868 1.116L9.632 7h5.985l.39-3.124a1 1 0 0 1 1.985.248L17.632 7H20a1 1 0 1 1 0 2h-2.617l-.75 6H20a1 1 0 1 1 0 2h-3.617l-.39 3.124a1 1 0 1 1-1.985-.248l.36-2.876H8.382l-.39 3.124a1 1 0 1 1-1.985-.248L6.368 17H4a1 1 0 1 1 0-2h2.617l.75-6H4a1 1 0 1 1 0-2h3.617l.39-3.124a1 1 0 0 1 1.117-.868ZM9.383 9l-.75 6h5.984l.75-6H9.383Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const ListMagnifyingGlass_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm1 4a1 1 0 0 0 0 2h5a1 1 0 0 0 0-2H4Zm-1 7a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm0 5a1 1 0 0 1 1-1h13a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm9-8a4 4 0 1 1 7.446 2.032l.99.989a1 1 0 1 1-1.415 1.414l-.99-.989A4 4 0 0 1 12 12Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const ListSparkle_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3a1 1 0 0 1 .92.606l1.342 3.132 3.132 1.343a1 1 0 0 1 0 1.838l-3.132 1.343-1.343 3.132a1 1 0 0 1-1.838 0l-1.343-3.132-3.132-1.343a1 1 0 0 1 0-1.838l3.132-1.343 1.343-3.132A1 1 0 0 1 17 9Zm0 3.539-.58 1.355a1 1 0 0 1-.526.525L14.539 15l1.355.58a1 1 0 0 1 .525.526L17 17.461l.58-1.355a1 1 0 0 1 .526-.525L19.461 15l-1.355-.58a1 1 0 0 1-.525-.526L17 12.539Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const News2_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M1 5a1 1 0 0 1 1-1h7a3.99 3.99 0 0 1 3 1.354A3.99 3.99 0 0 1 15 4h7a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-6.723c-.52 0-1 .125-1.4.372-.421.26-.761.633-.983 1.075a1 1 0 0 1-1.788 0 2.664 2.664 0 0 0-.983-1.075c-.4-.247-.88-.372-1.4-.372H2a1 1 0 0 1-1-1V5Zm10 3a2 2 0 0 0-2-2H3v12h5.723c.776 0 1.564.173 2.277.569V8Zm2 10.569V8a2 2 0 0 1 2-2h6v12h-5.723c-.776 0-1.564.173-2.277.569Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PlusLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Trending2_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||||
|
path: 'm18.192 5.004 1.864 5.31a1 1 0 0 0 1.887-.662L20.08 4.34c-.665-1.893-3.378-1.741-3.834.207l-3.381 14.449-2.985-9.605C9.3 7.531 6.684 7.506 6.07 9.355l-1.18 3.56-.969-2.312a1 1 0 0 0-1.844.772l.97 2.315c.715 1.71 3.159 1.613 3.741-.144l1.18-3.56 2.985 9.605c.607 1.952 3.392 1.848 3.857-.138l3.381-14.449Z',
|
||||||
|
})
|
|
@ -131,6 +131,38 @@ interface TrackPropertiesMap {
|
||||||
'Onboarding:Reset': {}
|
'Onboarding:Reset': {}
|
||||||
'Onboarding:SuggestedFollowFollowed': {}
|
'Onboarding:SuggestedFollowFollowed': {}
|
||||||
'Onboarding:CustomFeedAdded': {}
|
'Onboarding:CustomFeedAdded': {}
|
||||||
|
// Onboarding v2
|
||||||
|
'OnboardingV2:Begin': {}
|
||||||
|
'OnboardingV2:StepInterests:Start': {}
|
||||||
|
'OnboardingV2:StepInterests:End': {
|
||||||
|
selectedInterests: string[]
|
||||||
|
selectedInterestsLength: number
|
||||||
|
}
|
||||||
|
'OnboardingV2:StepInterests:Error': {}
|
||||||
|
'OnboardingV2:StepSuggestedAccounts:Start': {}
|
||||||
|
'OnboardingV2:StepSuggestedAccounts:End': {
|
||||||
|
selectedAccountsLength: number
|
||||||
|
}
|
||||||
|
'OnboardingV2:StepFollowingFeed:Start': {}
|
||||||
|
'OnboardingV2:StepFollowingFeed:End': {}
|
||||||
|
'OnboardingV2:StepAlgoFeeds:Start': {}
|
||||||
|
'OnboardingV2:StepAlgoFeeds:End': {
|
||||||
|
selectedPrimaryFeeds: string[]
|
||||||
|
selectedPrimaryFeedsLength: number
|
||||||
|
selectedSecondaryFeeds: string[]
|
||||||
|
selectedSecondaryFeedsLength: number
|
||||||
|
}
|
||||||
|
'OnboardingV2:StepTopicalFeeds:Start': {}
|
||||||
|
'OnboardingV2:StepTopicalFeeds:End': {
|
||||||
|
selectedFeeds: string[]
|
||||||
|
selectedFeedsLength: number
|
||||||
|
}
|
||||||
|
'OnboardingV2:StepModeration:Start': {}
|
||||||
|
'OnboardingV2:StepModeration:End': {}
|
||||||
|
'OnboardingV2:StepFinished:Start': {}
|
||||||
|
'OnboardingV2:StepFinished:End': {}
|
||||||
|
'OnboardingV2:Complete': {}
|
||||||
|
'OnboardingV2:Skip': {}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScreenPropertiesMap {
|
interface ScreenPropertiesMap {
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||||
export const PWI_ENABLED = true
|
export const PWI_ENABLED = true
|
||||||
|
export const NEW_ONBOARDING_ENABLED = false
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useTheme,
|
||||||
|
atoms as a,
|
||||||
|
ViewStyleProp,
|
||||||
|
TextStyleProp,
|
||||||
|
flatten,
|
||||||
|
} from '#/alf'
|
||||||
|
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||||
|
import {Props} from '#/components/icons/common'
|
||||||
|
|
||||||
|
export function IconCircle({
|
||||||
|
icon: Icon,
|
||||||
|
size = 'xl',
|
||||||
|
style,
|
||||||
|
iconStyle,
|
||||||
|
}: ViewStyleProp & {
|
||||||
|
icon: typeof Growth
|
||||||
|
size?: Props['size']
|
||||||
|
iconStyle?: TextStyleProp['style']
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
backgroundColor:
|
||||||
|
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
||||||
|
},
|
||||||
|
flatten(style),
|
||||||
|
]}>
|
||||||
|
<Icon
|
||||||
|
size={size}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
color: t.palette.primary_500,
|
||||||
|
},
|
||||||
|
flatten(iconStyle),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useTheme,
|
||||||
|
atoms as a,
|
||||||
|
useBreakpoints,
|
||||||
|
web,
|
||||||
|
native,
|
||||||
|
flatten,
|
||||||
|
TextStyleProp,
|
||||||
|
} from '#/alf'
|
||||||
|
import {H2, P, leading} from '#/components/Typography'
|
||||||
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
|
||||||
|
const COL_WIDTH = 500
|
||||||
|
|
||||||
|
export const OnboardingControls = createPortalGroup()
|
||||||
|
|
||||||
|
export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const scrollview = React.useRef<ScrollView>(null)
|
||||||
|
const prevActiveStep = React.useRef<string>(state.activeStep)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (state.activeStep !== prevActiveStep.current) {
|
||||||
|
prevActiveStep.current = state.activeStep
|
||||||
|
scrollview.current?.scrollTo({y: 0, animated: false})
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
const paddingTop = gtMobile ? a.py_5xl : a.py_lg
|
||||||
|
const dialogLabel = _(msg`Set up your account`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
aria-modal
|
||||||
|
role="dialog"
|
||||||
|
aria-role="dialog"
|
||||||
|
aria-label={dialogLabel}
|
||||||
|
accessibilityLabel={dialogLabel}
|
||||||
|
accessibilityHint={_(
|
||||||
|
msg`The following steps will help customize your Bluesky experience.`,
|
||||||
|
)}
|
||||||
|
style={[
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
isWeb ? a.fixed : a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
a.flex_1,
|
||||||
|
t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
{IS_DEV && (
|
||||||
|
<View style={[a.absolute, a.p_xl, a.z_10, {right: 0, top: insets.top}]}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="negative"
|
||||||
|
size="small"
|
||||||
|
onPress={() => onboardDispatch({type: 'skip'})}
|
||||||
|
// DEV ONLY
|
||||||
|
label="Clear onboarding state">
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!gtMobile && state.hasPrev && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
web(a.fixed),
|
||||||
|
native(a.absolute),
|
||||||
|
a.flex_row,
|
||||||
|
a.w_full,
|
||||||
|
a.justify_center,
|
||||||
|
a.z_20,
|
||||||
|
a.px_xl,
|
||||||
|
{
|
||||||
|
top: paddingTop.paddingTop + insets.top - 1,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={[a.w_full, a.align_start, {maxWidth: COL_WIDTH}]}>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
label={_(msg`Go back to previous step`)}
|
||||||
|
style={[a.absolute]}
|
||||||
|
onPress={() => dispatch({type: 'prev'})}>
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollview}
|
||||||
|
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
|
||||||
|
contentContainerStyle={{borderWidth: 0}}
|
||||||
|
// @ts-ignore web only --prf
|
||||||
|
dataSet={{'stable-gutters': 1}}>
|
||||||
|
<View
|
||||||
|
style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
|
||||||
|
<View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
|
||||||
|
<View style={[a.w_full, a.align_center, paddingTop]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_sm,
|
||||||
|
a.w_full,
|
||||||
|
{paddingTop: 17, maxWidth: '60%'},
|
||||||
|
]}>
|
||||||
|
{Array(state.totalSteps)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.pt_xs,
|
||||||
|
a.rounded_full,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
i + 1 <= state.activeStepIndex
|
||||||
|
? t.palette.primary_500
|
||||||
|
: t.palette.contrast_100,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[a.w_full, a.mb_5xl, {paddingTop: gtMobile ? 20 : 40}]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{height: 200}} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
isWeb ? a.fixed : a.absolute,
|
||||||
|
{bottom: 0, left: 0, right: 0},
|
||||||
|
t.atoms.bg,
|
||||||
|
t.atoms.border,
|
||||||
|
a.border_t,
|
||||||
|
a.align_center,
|
||||||
|
gtMobile ? a.px_5xl : a.px_xl,
|
||||||
|
isWeb
|
||||||
|
? a.py_2xl
|
||||||
|
: {
|
||||||
|
paddingTop: a.pt_lg.paddingTop,
|
||||||
|
paddingBottom: insets.bottom,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
{maxWidth: COL_WIDTH},
|
||||||
|
gtMobile && [a.flex_row, a.justify_between],
|
||||||
|
]}>
|
||||||
|
{gtMobile &&
|
||||||
|
(state.hasPrev ? (
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
label={_(msg`Go back to previous step`)}
|
||||||
|
onPress={() => dispatch({type: 'prev'})}>
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<View style={{height: 54}} />
|
||||||
|
))}
|
||||||
|
<OnboardingControls.Outlet />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Title({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: React.PropsWithChildren<TextStyleProp>) {
|
||||||
|
return (
|
||||||
|
<H2
|
||||||
|
style={[
|
||||||
|
a.pb_sm,
|
||||||
|
{
|
||||||
|
lineHeight: leading(a.text_4xl, a.leading_tight),
|
||||||
|
},
|
||||||
|
flatten(style),
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</H2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Description({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: React.PropsWithChildren<TextStyleProp>) {
|
||||||
|
const t = useTheme()
|
||||||
|
return <P style={[t.atoms.text_contrast_700, flatten(style)]}>{children}</P>
|
||||||
|
}
|
|
@ -0,0 +1,378 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
|
||||||
|
import {Text, H3} from '#/components/Typography'
|
||||||
|
import {RichText} from '#/components/RichText'
|
||||||
|
|
||||||
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
|
import {FeedConfig} from '#/screens/Onboarding/StepAlgoFeeds'
|
||||||
|
|
||||||
|
function PrimaryFeedCardInner({
|
||||||
|
feed,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
feed: FeedSourceInfo
|
||||||
|
config: FeedConfig
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ctx = Toggle.useItemContext()
|
||||||
|
|
||||||
|
const styles = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
active: [t.atoms.bg_contrast_25],
|
||||||
|
selected: [
|
||||||
|
a.shadow_md,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedHover: [
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
textSelected: [{color: t.palette.white}],
|
||||||
|
checkboxSelected: [
|
||||||
|
{
|
||||||
|
borderColor: t.palette.white,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[t],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
a.p_lg,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
(ctx.hovered || ctx.focused || ctx.pressed) && styles.active,
|
||||||
|
ctx.selected && styles.selected,
|
||||||
|
ctx.selected &&
|
||||||
|
(ctx.hovered || ctx.focused || ctx.pressed) &&
|
||||||
|
styles.selectedHover,
|
||||||
|
]}>
|
||||||
|
{ctx.selected && config.gradient && (
|
||||||
|
<LinearGradient
|
||||||
|
colors={config.gradient.values.map(v => v[1])}
|
||||||
|
locations={config.gradient.values.map(v => v[0])}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
},
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={{uri: feed.avatar}}
|
||||||
|
style={[a.w_full, a.h_full]}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.pt_xs, a.flex_grow]}>
|
||||||
|
<H3
|
||||||
|
style={[
|
||||||
|
a.text_lg,
|
||||||
|
a.font_bold,
|
||||||
|
ctx.selected && styles.textSelected,
|
||||||
|
]}>
|
||||||
|
{feed.displayName}
|
||||||
|
</H3>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
{opacity: 0.6},
|
||||||
|
a.text_md,
|
||||||
|
a.py_xs,
|
||||||
|
ctx.selected && styles.textSelected,
|
||||||
|
]}>
|
||||||
|
by @{feed.creatorHandle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
},
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.rounded_sm,
|
||||||
|
ctx.selected ? [a.border, styles.checkboxSelected] : t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
{ctx.selected && <Check size="sm" fill={t.palette.white} />}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
opacity: ctx.selected ? 0.3 : 1,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
a.mt_md,
|
||||||
|
a.w_full,
|
||||||
|
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
|
||||||
|
ctx.selected && {
|
||||||
|
borderTopColor: t.palette.white,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={[a.pt_md]}>
|
||||||
|
<RichText
|
||||||
|
value={feed.description}
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
ctx.selected &&
|
||||||
|
(t.name === 'light'
|
||||||
|
? t.atoms.text_inverted
|
||||||
|
: {color: t.palette.white}),
|
||||||
|
]}
|
||||||
|
disableLinks
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PrimaryFeedCard({config}: {config: FeedConfig}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {data: feed} = useFeedSourceInfoQuery({uri: config.uri})
|
||||||
|
|
||||||
|
return !feed ? (
|
||||||
|
<FeedCardPlaceholder primary />
|
||||||
|
) : (
|
||||||
|
<Toggle.Item
|
||||||
|
name={feed.uri}
|
||||||
|
label={_(msg`Subscribe to the ${feed.displayName} feed`)}>
|
||||||
|
<PrimaryFeedCardInner config={config} feed={feed} />
|
||||||
|
</Toggle.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeedCardInner({feed}: {feed: FeedSourceInfo; config: FeedConfig}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ctx = Toggle.useItemContext()
|
||||||
|
|
||||||
|
const styles = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
active: [t.atoms.bg_contrast_25],
|
||||||
|
selected: [
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedHover: [
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
textSelected: [],
|
||||||
|
checkboxSelected: [
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.primary_500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[t],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
(ctx.hovered || ctx.focused || ctx.pressed) && styles.active,
|
||||||
|
ctx.selected && styles.selected,
|
||||||
|
ctx.selected &&
|
||||||
|
(ctx.hovered || ctx.focused || ctx.pressed) &&
|
||||||
|
styles.selectedHover,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
},
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={{uri: feed.avatar}}
|
||||||
|
style={[a.w_full, a.h_full]}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.pt_2xs, a.flex_grow]}>
|
||||||
|
<H3
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_bold,
|
||||||
|
ctx.selected && styles.textSelected,
|
||||||
|
]}>
|
||||||
|
{feed.displayName}
|
||||||
|
</H3>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
{opacity: 0.8},
|
||||||
|
a.pt_xs,
|
||||||
|
ctx.selected && styles.textSelected,
|
||||||
|
]}>
|
||||||
|
@{feed.creatorHandle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg,
|
||||||
|
ctx.selected && styles.checkboxSelected,
|
||||||
|
{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{ctx.selected && <Check size="sm" fill={t.palette.white} />}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
opacity: ctx.selected ? 0.3 : 1,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
a.mt_md,
|
||||||
|
a.w_full,
|
||||||
|
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
|
||||||
|
ctx.selected && {
|
||||||
|
borderTopColor: t.palette.primary_200,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={[a.pt_md]}>
|
||||||
|
<RichText value={feed.description} disableLinks />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedCard({config}: {config: FeedConfig}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {data: feed} = useFeedSourceInfoQuery({uri: config.uri})
|
||||||
|
|
||||||
|
return !feed ? (
|
||||||
|
<FeedCardPlaceholder />
|
||||||
|
) : feed.avatar ? (
|
||||||
|
<Toggle.Item
|
||||||
|
name={feed.uri}
|
||||||
|
label={_(msg`Subscribe to the ${feed.displayName} feed`)}>
|
||||||
|
<FeedCardInner config={config} feed={feed} />
|
||||||
|
</Toggle.Item>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedCardPlaceholder({primary}: {primary?: boolean}) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width: primary ? 64 : 44,
|
||||||
|
height: primary ? 64 : 44,
|
||||||
|
},
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={[a.pt_2xs, a.flex_grow, a.gap_sm]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 100, height: primary ? 20 : 16},
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 60, height: 12},
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
a.mt_md,
|
||||||
|
a.w_full,
|
||||||
|
t.atoms.border,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={[a.pt_md, a.gap_xs]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: '60%', height: 12},
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
|
||||||
|
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||||
|
|
||||||
|
export type FeedConfig = {
|
||||||
|
default: boolean
|
||||||
|
uri: string
|
||||||
|
gradient?: typeof tokens.gradients.midnight | typeof tokens.gradients.nordic
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRIMARY_FEEDS: FeedConfig[] = [
|
||||||
|
{
|
||||||
|
default: true,
|
||||||
|
uri: 'at://did:plc:wqowuobffl66jv3kpsvo7ak4/app.bsky.feed.generator/the-algorithm',
|
||||||
|
gradient: tokens.gradients.midnight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot',
|
||||||
|
gradient: tokens.gradients.midnight,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const SECONDARY_FEEDS: FeedConfig[] = [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
uri: 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/infreq',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/best-of-follows',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
uri: 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/catch-up',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
uri: 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/at-bangers',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function StepAlgoFeeds() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const t = useTheme()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const [primaryFeedUris, setPrimaryFeedUris] = React.useState<string[]>(
|
||||||
|
PRIMARY_FEEDS.map(f => (f.default ? f.uri : '')).filter(Boolean),
|
||||||
|
)
|
||||||
|
const [secondaryFeedUris, setSeconaryFeedUris] = React.useState<string[]>([])
|
||||||
|
const [saving, setSaving] = React.useState(false)
|
||||||
|
|
||||||
|
const saveFeeds = React.useCallback(async () => {
|
||||||
|
setSaving(true)
|
||||||
|
|
||||||
|
const uris = primaryFeedUris.concat(secondaryFeedUris)
|
||||||
|
dispatch({type: 'setAlgoFeedsStepResults', feedUris: uris})
|
||||||
|
|
||||||
|
setSaving(false)
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
track('OnboardingV2:StepAlgoFeeds:End', {
|
||||||
|
selectedPrimaryFeeds: primaryFeedUris,
|
||||||
|
selectedPrimaryFeedsLength: primaryFeedUris.length,
|
||||||
|
selectedSecondaryFeeds: secondaryFeedUris,
|
||||||
|
selectedSecondaryFeedsLength: secondaryFeedUris.length,
|
||||||
|
})
|
||||||
|
}, [primaryFeedUris, secondaryFeedUris, dispatch, track])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:StepAlgoFeeds:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<IconCircle icon={ListSparkle} style={[a.mb_2xl]} />
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
<Trans>Choose your algorithmic feeds</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description>
|
||||||
|
<Trans>
|
||||||
|
Feeds are created by users and can give you entirely new experiences.
|
||||||
|
</Trans>
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.pb_2xl]}>
|
||||||
|
<Toggle.Group
|
||||||
|
values={primaryFeedUris}
|
||||||
|
onChange={setPrimaryFeedUris}
|
||||||
|
label={_(msg`Select your primary algorithmic feeds`)}>
|
||||||
|
<Text
|
||||||
|
style={[a.text_md, a.pt_4xl, a.pb_md, t.atoms.text_contrast_700]}>
|
||||||
|
<Trans>We recommend "For You" by Skygaze:</Trans>
|
||||||
|
</Text>
|
||||||
|
<FeedCard config={PRIMARY_FEEDS[0]} />
|
||||||
|
<Text
|
||||||
|
style={[a.text_md, a.pt_4xl, a.pb_lg, t.atoms.text_contrast_700]}>
|
||||||
|
<Trans>Or you can try our "Discover" algorithm:</Trans>
|
||||||
|
</Text>
|
||||||
|
<FeedCard config={PRIMARY_FEEDS[1]} />
|
||||||
|
</Toggle.Group>
|
||||||
|
|
||||||
|
<Toggle.Group
|
||||||
|
values={secondaryFeedUris}
|
||||||
|
onChange={setSeconaryFeedUris}
|
||||||
|
label={_(msg`Select your secondary algorithmic feeds`)}>
|
||||||
|
<Text
|
||||||
|
style={[a.text_md, a.pt_4xl, a.pb_lg, t.atoms.text_contrast_700]}>
|
||||||
|
<Trans>There are many feeds to try:</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
{SECONDARY_FEEDS.map(config => (
|
||||||
|
<FeedCard key={config.uri} config={config} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
disabled={saving}
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Continue to the next step`)}
|
||||||
|
onPress={saveFeeds}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={saving ? Loader : ChevronRight} position="right" />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||||
|
import {News2_Stroke2_Corner0_Rounded as News} from '#/components/icons/News2'
|
||||||
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
|
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||||
|
import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
|
||||||
|
import {getAgent} from '#/state/session'
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||||
|
import {
|
||||||
|
bulkWriteFollows,
|
||||||
|
sortPrimaryAlgorithmFeeds,
|
||||||
|
} from '#/screens/Onboarding/util'
|
||||||
|
|
||||||
|
export function StepFinished() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
|
const [saving, setSaving] = React.useState(false)
|
||||||
|
const {mutateAsync: saveFeeds} = useSetSaveFeedsMutation()
|
||||||
|
|
||||||
|
const finishOnboarding = React.useCallback(async () => {
|
||||||
|
setSaving(true)
|
||||||
|
|
||||||
|
const {
|
||||||
|
interestsStepResults,
|
||||||
|
suggestedAccountsStepResults,
|
||||||
|
algoFeedsStepResults,
|
||||||
|
topicalFeedsStepResults,
|
||||||
|
} = state
|
||||||
|
const {selectedInterests} = interestsStepResults
|
||||||
|
const selectedFeeds = [
|
||||||
|
...sortPrimaryAlgorithmFeeds(algoFeedsStepResults.feedUris),
|
||||||
|
...topicalFeedsStepResults.feedUris,
|
||||||
|
]
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
bulkWriteFollows(suggestedAccountsStepResults.accountDids),
|
||||||
|
// these must be serial
|
||||||
|
(async () => {
|
||||||
|
await getAgent().setInterestsPref({tags: selectedInterests})
|
||||||
|
await saveFeeds({
|
||||||
|
saved: selectedFeeds,
|
||||||
|
pinned: selectedFeeds,
|
||||||
|
})
|
||||||
|
})(),
|
||||||
|
])
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.info(`onboarding: bulk save failed`)
|
||||||
|
logger.error(e)
|
||||||
|
// don't alert the user, just let them into their account
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(false)
|
||||||
|
dispatch({type: 'finish'})
|
||||||
|
onboardDispatch({type: 'finish'})
|
||||||
|
track('OnboardingV2:StepFinished:End')
|
||||||
|
track('OnboardingV2:Complete')
|
||||||
|
}, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:StepFinished:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<IconCircle icon={Check} style={[a.mb_2xl]} />
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
<Trans>You're ready to go!</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description>
|
||||||
|
<Trans>We hope you have a wonderful time. Remember, Bluesky is:</Trans>
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<View style={[a.pt_5xl, a.gap_3xl]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}>
|
||||||
|
<IconCircle icon={Growth} size="lg" style={{width: 48, height: 48}} />
|
||||||
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
|
<Text style={[a.font_bold, a.text_lg]}>
|
||||||
|
<Trans>Public</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[t.atoms.text_contrast_500, a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Your posts, likes, and blocks are public. Mutes are private.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}>
|
||||||
|
<IconCircle icon={News} size="lg" style={{width: 48, height: 48}} />
|
||||||
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
|
<Text style={[a.font_bold, a.text_lg]}>
|
||||||
|
<Trans>Open</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[t.atoms.text_contrast_500, a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>Never lose access to your followers and data.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}>
|
||||||
|
<IconCircle
|
||||||
|
icon={Trending}
|
||||||
|
size="lg"
|
||||||
|
style={{width: 48, height: 48}}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
|
<Text style={[a.font_bold, a.text_lg]}>
|
||||||
|
<Trans>Flexible</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[t.atoms.text_contrast_500, a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>Choose the algorithms that power your custom feeds.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
disabled={saving}
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Complete onboarding and start using your account`)}
|
||||||
|
onPress={finishOnboarding}>
|
||||||
|
<ButtonText>
|
||||||
|
{saving ? <Trans>Finalizing</Trans> : <Trans>Let's go!</Trans>}
|
||||||
|
</ButtonText>
|
||||||
|
{saving && <ButtonIcon icon={Loader} position="right" />}
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
useSetFeedViewPreferencesMutation,
|
||||||
|
} from 'state/queries/preferences'
|
||||||
|
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||||
|
|
||||||
|
export function StepFollowingFeed() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {dispatch} = React.useContext(Context)
|
||||||
|
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutate: setFeedViewPref, variables} =
|
||||||
|
useSetFeedViewPreferencesMutation()
|
||||||
|
|
||||||
|
const showReplies = !(
|
||||||
|
variables?.hideReplies ?? preferences?.feedViewPrefs.hideReplies
|
||||||
|
)
|
||||||
|
const showReposts = !(
|
||||||
|
variables?.hideReposts ?? preferences?.feedViewPrefs.hideReposts
|
||||||
|
)
|
||||||
|
const showQuotes = !(
|
||||||
|
variables?.hideQuotePosts ?? preferences?.feedViewPrefs.hideQuotePosts
|
||||||
|
)
|
||||||
|
|
||||||
|
const onContinue = React.useCallback(() => {
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
track('OnboardingV2:StepFollowingFeed:End')
|
||||||
|
}, [track, dispatch])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:StepFollowingFeed:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Hack for now to move the image container up
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<IconCircle icon={FilterTimeline} style={[a.mb_2xl]} />
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
<Trans>Your default feed is "Following"</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description style={[a.mb_md]}>
|
||||||
|
<Trans>It show posts from the people your follow as they happen.</Trans>
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<View style={[a.w_full]}>
|
||||||
|
<Toggle.Item
|
||||||
|
name="Show Replies" // no need to translate
|
||||||
|
label={_(msg`Show replies in Following feed`)}
|
||||||
|
value={showReplies}
|
||||||
|
onChange={() => {
|
||||||
|
setFeedViewPref({
|
||||||
|
hideReplies: showReplies,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.w_full,
|
||||||
|
a.py_lg,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md, a.font_bold]}>
|
||||||
|
<Trans>Show replies in Following</Trans>
|
||||||
|
</Text>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</View>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Divider />
|
||||||
|
<Toggle.Item
|
||||||
|
name="Show Reposts" // no need to translate
|
||||||
|
label={_(msg`Show re-posts in Following feed`)}
|
||||||
|
value={showReposts}
|
||||||
|
onChange={() => {
|
||||||
|
setFeedViewPref({
|
||||||
|
hideReposts: showReposts,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.w_full,
|
||||||
|
a.py_lg,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md, a.font_bold]}>
|
||||||
|
<Trans>Show reposts in Following</Trans>
|
||||||
|
</Text>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</View>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Divider />
|
||||||
|
<Toggle.Item
|
||||||
|
name="Show Quotes" // no need to translate
|
||||||
|
label={_(msg`Show quote-posts in Following feed`)}
|
||||||
|
value={showQuotes}
|
||||||
|
onChange={() => {
|
||||||
|
setFeedViewPref({
|
||||||
|
hideQuotePosts: showQuotes,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.w_full,
|
||||||
|
a.py_lg,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md, a.font_bold]}>
|
||||||
|
<Trans>Show quotes in Following</Trans>
|
||||||
|
</Text>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</View>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Description style={[a.mt_lg]}>
|
||||||
|
<Trans>You can change these settings later.</Trans>
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Continue to next step`)}
|
||||||
|
onPress={onContinue}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={ChevronRight} position="right" />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View, ViewStyle, TextStyle} from 'react-native'
|
||||||
|
|
||||||
|
import {useTheme, atoms as a, native} from '#/alf'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data'
|
||||||
|
|
||||||
|
export function InterestButton({interest}: {interest: string}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ctx = Toggle.useItemContext()
|
||||||
|
|
||||||
|
const styles = React.useMemo(() => {
|
||||||
|
const hovered: ViewStyle[] = [
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
t.name === 'light' ? t.palette.contrast_200 : t.palette.contrast_50,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const focused: ViewStyle[] = []
|
||||||
|
const pressed: ViewStyle[] = []
|
||||||
|
const selected: ViewStyle[] = [
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.contrast_900,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const selectedHover: ViewStyle[] = [
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.contrast_800,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const textSelected: TextStyle[] = [
|
||||||
|
{
|
||||||
|
color: t.palette.contrast_100,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
hovered,
|
||||||
|
focused,
|
||||||
|
pressed,
|
||||||
|
selected,
|
||||||
|
selectedHover,
|
||||||
|
textSelected,
|
||||||
|
}
|
||||||
|
}, [t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.contrast_100,
|
||||||
|
paddingVertical: 15,
|
||||||
|
},
|
||||||
|
a.rounded_full,
|
||||||
|
a.px_2xl,
|
||||||
|
ctx.hovered ? styles.hovered : {},
|
||||||
|
ctx.focused ? styles.hovered : {},
|
||||||
|
ctx.pressed ? styles.hovered : {},
|
||||||
|
ctx.selected ? styles.selected : {},
|
||||||
|
ctx.selected && (ctx.hovered || ctx.focused || ctx.pressed)
|
||||||
|
? styles.selectedHover
|
||||||
|
: {},
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
color: t.palette.contrast_900,
|
||||||
|
},
|
||||||
|
a.font_bold,
|
||||||
|
native({paddingTop: 2}),
|
||||||
|
ctx.selected ? styles.textSelected : {},
|
||||||
|
]}>
|
||||||
|
{INTEREST_TO_DISPLAY_NAME[interest]}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
export const INTEREST_TO_DISPLAY_NAME: {
|
||||||
|
[key: string]: string
|
||||||
|
} = {
|
||||||
|
news: 'News',
|
||||||
|
journalism: 'Journalism',
|
||||||
|
nature: 'Nature',
|
||||||
|
art: 'Art',
|
||||||
|
comics: 'Comics',
|
||||||
|
writers: 'Writers',
|
||||||
|
culture: 'Culture',
|
||||||
|
sports: 'Sports',
|
||||||
|
pets: 'Pets',
|
||||||
|
animals: 'Animals',
|
||||||
|
books: 'Books',
|
||||||
|
education: 'Education',
|
||||||
|
climate: 'Climate',
|
||||||
|
science: 'Science',
|
||||||
|
politics: 'Politics',
|
||||||
|
fitness: 'Fitness',
|
||||||
|
tech: 'Tech',
|
||||||
|
dev: 'Software Dev',
|
||||||
|
comedy: 'Comedy',
|
||||||
|
gaming: 'Video Games',
|
||||||
|
food: 'Food',
|
||||||
|
cooking: 'Cooking',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiResponseMap = {
|
||||||
|
interests: string[]
|
||||||
|
suggestedAccountDids: {
|
||||||
|
[key: string]: string[]
|
||||||
|
}
|
||||||
|
suggestedFeedUris: {
|
||||||
|
[key: string]: string[]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,260 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
|
||||||
|
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSad} from '#/components/icons/Emoji'
|
||||||
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwise} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {getAgent} from '#/state/session'
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
import {
|
||||||
|
ApiResponseMap,
|
||||||
|
INTEREST_TO_DISPLAY_NAME,
|
||||||
|
} from '#/screens/Onboarding/StepInterests/data'
|
||||||
|
import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
|
||||||
|
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||||
|
|
||||||
|
export function StepInterests() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const [saving, setSaving] = React.useState(false)
|
||||||
|
const [interests, setInterests] = React.useState<string[]>(
|
||||||
|
state.interestsStepResults.selectedInterests.map(i => i),
|
||||||
|
)
|
||||||
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
|
const {isLoading, isError, error, data, refetch, isFetching} = useQuery({
|
||||||
|
queryKey: ['interests'],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const {data} =
|
||||||
|
await getAgent().app.bsky.unspecced.getTaggedSuggestions()
|
||||||
|
return data.suggestions.reduce(
|
||||||
|
(agg, s) => {
|
||||||
|
const {tag, subject, subjectType} = s
|
||||||
|
const isDefault = tag === 'default'
|
||||||
|
|
||||||
|
if (!agg.interests.includes(tag) && !isDefault) {
|
||||||
|
agg.interests.push(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subjectType === 'user') {
|
||||||
|
agg.suggestedAccountDids[tag] =
|
||||||
|
agg.suggestedAccountDids[tag] || []
|
||||||
|
agg.suggestedAccountDids[tag].push(subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subjectType === 'feed') {
|
||||||
|
// agg all feeds into defaults
|
||||||
|
if (isDefault) {
|
||||||
|
agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || []
|
||||||
|
} else {
|
||||||
|
agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || []
|
||||||
|
agg.suggestedFeedUris[tag].push(subject)
|
||||||
|
agg.suggestedFeedUris.default.push(subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interests: [],
|
||||||
|
suggestedAccountDids: {},
|
||||||
|
suggestedFeedUris: {},
|
||||||
|
} as ApiResponseMap,
|
||||||
|
)
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.info(
|
||||||
|
`onboarding: getTaggedSuggestions fetch or processing failed`,
|
||||||
|
)
|
||||||
|
logger.error(e)
|
||||||
|
track('OnboardingV2:StepInterests:Error')
|
||||||
|
|
||||||
|
throw new Error(`a network error occurred`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveInterests = React.useCallback(async () => {
|
||||||
|
setSaving(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(false)
|
||||||
|
dispatch({
|
||||||
|
type: 'setInterestsStepResults',
|
||||||
|
apiResponse: data!,
|
||||||
|
selectedInterests: interests,
|
||||||
|
})
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
|
||||||
|
track('OnboardingV2:StepInterests:End', {
|
||||||
|
selectedInterests: interests,
|
||||||
|
selectedInterestsLength: interests.length,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.info(`onboading: error saving interests`)
|
||||||
|
logger.error(e)
|
||||||
|
}
|
||||||
|
}, [interests, data, setSaving, dispatch, track])
|
||||||
|
|
||||||
|
const skipOnboarding = React.useCallback(() => {
|
||||||
|
onboardDispatch({type: 'finish'})
|
||||||
|
dispatch({type: 'finish'})
|
||||||
|
track('OnboardingV2:Skip')
|
||||||
|
}, [onboardDispatch, dispatch, track])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:Begin')
|
||||||
|
track('OnboardingV2:StepInterests:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
const title = isError ? (
|
||||||
|
<Trans>Oh no! Something went wrong.</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>What are your interests?</Trans>
|
||||||
|
)
|
||||||
|
const description = isError ? (
|
||||||
|
<Trans>
|
||||||
|
We weren't able to connect. Please try again to continue setting up your
|
||||||
|
account. If it continues to fail, you can skip this flow.
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>We'll use this to help customize your experience.</Trans>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<IconCircle
|
||||||
|
icon={isError ? EmojiSad : Hashtag}
|
||||||
|
style={[
|
||||||
|
a.mb_2xl,
|
||||||
|
isError
|
||||||
|
? {
|
||||||
|
backgroundColor: t.palette.negative_50,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
iconStyle={[
|
||||||
|
isError
|
||||||
|
? {
|
||||||
|
color: t.palette.negative_900,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Title>{title}</Title>
|
||||||
|
<Description>{description}</Description>
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.pt_2xl]}>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader size="xl" />
|
||||||
|
) : isError || !data ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.p_lg,
|
||||||
|
a.rounded_md,
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.negative_50,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_bold,
|
||||||
|
{
|
||||||
|
color: t.palette.negative_900,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
Error:{' '}
|
||||||
|
</Text>
|
||||||
|
{error?.message || 'an unknown error occurred'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Toggle.Group
|
||||||
|
values={interests}
|
||||||
|
onChange={setInterests}
|
||||||
|
label={_(msg`Select your interests from the options below`)}>
|
||||||
|
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||||
|
{data.interests.map(interest => (
|
||||||
|
<Toggle.Item
|
||||||
|
key={interest}
|
||||||
|
name={interest}
|
||||||
|
label={INTEREST_TO_DISPLAY_NAME[interest]}>
|
||||||
|
<InterestButton interest={interest} />
|
||||||
|
</Toggle.Item>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
{isError ? (
|
||||||
|
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||||
|
<Button
|
||||||
|
disabled={isFetching}
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Retry`)}
|
||||||
|
onPress={() => refetch()}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Retry</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={ArrowRotateCounterClockwise} position="right" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Skip this flow`)}
|
||||||
|
onPress={skipOnboarding}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Skip</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
disabled={saving || !data}
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Continue to next step`)}
|
||||||
|
onPress={saveInterests}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon
|
||||||
|
icon={saving ? Loader : ChevronRight}
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
usePreferencesSetAdultContentMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {InlineLink} from '#/components/Link'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
|
|
||||||
|
function Card({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_md,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.mb_md,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdultContentEnabledPref() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
// Reuse logic here form ContentFilteringSettings.tsx
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutate, variables} = usePreferencesSetAdultContentMutation()
|
||||||
|
|
||||||
|
const onToggleAdultContent = React.useCallback(async () => {
|
||||||
|
if (isIOS) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
mutate({
|
||||||
|
enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show(
|
||||||
|
_(msg`There was an issue syncing your preferences with the server`),
|
||||||
|
)
|
||||||
|
logger.error('Failed to update preferences with server', {error: e})
|
||||||
|
}
|
||||||
|
}, [variables, preferences, mutate, _])
|
||||||
|
|
||||||
|
if (!preferences) return null
|
||||||
|
|
||||||
|
if (isIOS) {
|
||||||
|
if (preferences?.adultContentEnabled === true) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CircleInfo size="sm" fill={t.palette.contrast_500} />
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
t.atoms.text_contrast_700,
|
||||||
|
a.leading_snug,
|
||||||
|
{paddingTop: 1},
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
Adult content can only be enabled via the Web at{' '}
|
||||||
|
<InlineLink style={[a.leading_snug]} to="https://bsky.app">
|
||||||
|
bsky.app
|
||||||
|
</InlineLink>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (preferences?.userAge) {
|
||||||
|
if (preferences.userAge >= 18) {
|
||||||
|
return (
|
||||||
|
<View style={[a.w_full]}>
|
||||||
|
<Toggle.Item
|
||||||
|
name={_(msg`Enable adult content in your feeds`)}
|
||||||
|
label={_(msg`Enable adult content in your feeds`)}
|
||||||
|
value={variables?.enabled ?? preferences?.adultContentEnabled}
|
||||||
|
onChange={onToggleAdultContent}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.w_full,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
a.py_md,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.font_bold]}>Enable Adult Content</Text>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</View>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CircleInfo size="sm" fill={t.palette.contrast_500} />
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
t.atoms.text_contrast_700,
|
||||||
|
a.leading_snug,
|
||||||
|
{paddingTop: 1},
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
You must be 18 years or older to enable adult content
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {LabelPreference} from '@atproto/api'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CONFIGURABLE_LABEL_GROUPS,
|
||||||
|
ConfigurableLabelGroup,
|
||||||
|
usePreferencesQuery,
|
||||||
|
usePreferencesSetContentLabelMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||||
|
|
||||||
|
export function ModerationOption({
|
||||||
|
labelGroup,
|
||||||
|
}: {
|
||||||
|
labelGroup: ConfigurableLabelGroup
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const groupInfo = CONFIGURABLE_LABEL_GROUPS[labelGroup]
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutate, variables} = usePreferencesSetContentLabelMutation()
|
||||||
|
const visibility =
|
||||||
|
variables?.visibility ?? preferences?.contentLabels?.[labelGroup]
|
||||||
|
|
||||||
|
const onChange = React.useCallback(
|
||||||
|
(vis: string[]) => {
|
||||||
|
mutate({labelGroup, visibility: vis[0] as LabelPreference})
|
||||||
|
},
|
||||||
|
[mutate, labelGroup],
|
||||||
|
)
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
hide: _(msg`Hide`),
|
||||||
|
warn: _(msg`Warn`),
|
||||||
|
show: _(msg`Show`),
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_sm,
|
||||||
|
a.py_xs,
|
||||||
|
a.px_xs,
|
||||||
|
a.align_center,
|
||||||
|
]}>
|
||||||
|
<View style={[a.gap_xs, {width: '50%'}]}>
|
||||||
|
<Text style={[a.font_bold]}>{groupInfo.title}</Text>
|
||||||
|
<Text style={[t.atoms.text_contrast_700, a.leading_snug]}>
|
||||||
|
{groupInfo.subtitle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[a.justify_center, {minHeight: 35}]}>
|
||||||
|
{!preferences?.adultContentEnabled && groupInfo.isAdultImagery ? (
|
||||||
|
<View style={[a.justify_center, {minHeight: 40}]}>
|
||||||
|
<Text style={[a.font_bold]}>{labels.hide}</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<ToggleButton.Group
|
||||||
|
label={_(
|
||||||
|
msg`Configure content filtering setting for category: ${groupInfo.title.toLowerCase()}`,
|
||||||
|
)}
|
||||||
|
values={[visibility ?? 'hide']}
|
||||||
|
onChange={onChange}>
|
||||||
|
<ToggleButton.Button name="hide" label={labels.hide}>
|
||||||
|
{labels.hide}
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="warn" label={labels.warn}>
|
||||||
|
{labels.warn}
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="ignore" label={labels.show}>
|
||||||
|
{labels.show}
|
||||||
|
</ToggleButton.Button>
|
||||||
|
</ToggleButton.Group>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {configurableLabelGroups} from 'state/queries/preferences'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||||
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
Title,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
import {ModerationOption} from '#/screens/Onboarding/StepModeration/ModerationOption'
|
||||||
|
import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/AdultContentEnabledPref'
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||||
|
|
||||||
|
export function StepModeration() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
|
||||||
|
const onContinue = React.useCallback(() => {
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
track('OnboardingV2:StepModeration:End')
|
||||||
|
}, [track, dispatch])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:StepModeration:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<IconCircle icon={EyeSlash} style={[a.mb_2xl]} />
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
<Trans>You are in control</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description style={[a.mb_xl]}>
|
||||||
|
<Trans>
|
||||||
|
Select the types of content that you want to see (or not see), and
|
||||||
|
we'll handle the rest.
|
||||||
|
</Trans>
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
{!preferences ? (
|
||||||
|
<View style={[a.pt_md]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AdultContentEnabledPref />
|
||||||
|
|
||||||
|
<View style={[a.gap_sm, a.w_full]}>
|
||||||
|
{configurableLabelGroups.map((g, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{index === 0 && <Divider />}
|
||||||
|
<ModerationOption labelGroup={g} />
|
||||||
|
<Divider />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Continue to next step`)}
|
||||||
|
onPress={onContinue}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={ChevronRight} position="right" />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View, ViewStyle} from 'react-native'
|
||||||
|
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||||
|
|
||||||
|
import {useTheme, atoms as a, flatten} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useItemContext} from '#/components/forms/Toggle'
|
||||||
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
|
import {RichText} from '#/components/RichText'
|
||||||
|
|
||||||
|
export function SuggestedAccountCard({
|
||||||
|
profile,
|
||||||
|
moderationOpts,
|
||||||
|
}: {
|
||||||
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
|
moderationOpts: ReturnType<typeof useModerationOpts>
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ctx = useItemContext()
|
||||||
|
const moderation = moderateProfile(profile, moderationOpts!)
|
||||||
|
|
||||||
|
const styles = React.useMemo(() => {
|
||||||
|
const light = t.name === 'light'
|
||||||
|
const base: ViewStyle[] = [t.atoms.bg_contrast_50]
|
||||||
|
const hover: ViewStyle[] = [t.atoms.bg_contrast_25]
|
||||||
|
const selected: ViewStyle[] = [
|
||||||
|
{
|
||||||
|
backgroundColor: light ? t.palette.primary_50 : t.palette.primary_950,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const selectedHover: ViewStyle[] = [
|
||||||
|
{
|
||||||
|
backgroundColor: light ? t.palette.primary_25 : t.palette.primary_975,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const checkboxBase: ViewStyle[] = [t.atoms.bg]
|
||||||
|
const checkboxSelected: ViewStyle[] = [
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.primary_500,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const avatarBase: ViewStyle[] = [t.atoms.bg_contrast_100]
|
||||||
|
const avatarSelected: ViewStyle[] = [
|
||||||
|
{
|
||||||
|
backgroundColor: light ? t.palette.primary_100 : t.palette.primary_900,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
base,
|
||||||
|
hover: flatten(hover),
|
||||||
|
selected: flatten(selected),
|
||||||
|
selectedHover: flatten(selectedHover),
|
||||||
|
checkboxBase: flatten(checkboxBase),
|
||||||
|
checkboxSelected: flatten(checkboxSelected),
|
||||||
|
avatarBase: flatten(avatarBase),
|
||||||
|
avatarSelected: flatten(avatarSelected),
|
||||||
|
}
|
||||||
|
}, [t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.p_md,
|
||||||
|
a.pr_lg,
|
||||||
|
a.gap_md,
|
||||||
|
a.rounded_md,
|
||||||
|
styles.base,
|
||||||
|
(ctx.hovered || ctx.focused || ctx.pressed) && styles.hover,
|
||||||
|
ctx.selected && styles.selected,
|
||||||
|
ctx.selected &&
|
||||||
|
(ctx.hovered || ctx.focused || ctx.pressed) &&
|
||||||
|
styles.selectedHover,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
|
||||||
|
<View style={[a.flex_row, a.flex_1, a.align_center, a.gap_md]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 48, height: 48},
|
||||||
|
a.relative,
|
||||||
|
a.rounded_full,
|
||||||
|
styles.avatarBase,
|
||||||
|
ctx.selected && styles.avatarSelected,
|
||||||
|
]}>
|
||||||
|
<UserAvatar
|
||||||
|
size={48}
|
||||||
|
avatar={profile.avatar}
|
||||||
|
moderation={moderation.avatar}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<Text style={[a.font_bold, a.text_md, a.pb_xs]} numberOfLines={1}>
|
||||||
|
{profile.displayName}
|
||||||
|
</Text>
|
||||||
|
<Text style={[t.atoms.text_contrast_600]}>{profile.handle}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.rounded_sm,
|
||||||
|
styles.checkboxBase,
|
||||||
|
ctx.selected && styles.checkboxSelected,
|
||||||
|
{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{ctx.selected && <Check size="sm" fill={t.palette.white} />}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{profile.description && (
|
||||||
|
<>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
opacity: ctx.selected ? 0.3 : 1,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
a.w_full,
|
||||||
|
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
|
||||||
|
ctx.selected && {
|
||||||
|
borderTopColor: t.palette.primary_200,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RichText
|
||||||
|
value={profile.description}
|
||||||
|
disableLinks
|
||||||
|
numberOfLines={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SuggestedAccountCardPlaceholder() {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
a.p_md,
|
||||||
|
a.pr_lg,
|
||||||
|
a.gap_xl,
|
||||||
|
a.rounded_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 48, height: 48},
|
||||||
|
a.relative,
|
||||||
|
a.rounded_full,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View style={[a.gap_xs]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 100, height: 16},
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 60, height: 12},
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {atoms as a, useBreakpoints} from '#/alf'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useProfilesQuery} from '#/state/queries/profile'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
import {
|
||||||
|
SuggestedAccountCard,
|
||||||
|
SuggestedAccountCardPlaceholder,
|
||||||
|
} from '#/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard'
|
||||||
|
import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data'
|
||||||
|
import {aggregateInterestItems} from '#/screens/Onboarding/util'
|
||||||
|
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||||
|
|
||||||
|
export function Inner({
|
||||||
|
profiles,
|
||||||
|
onSelect,
|
||||||
|
moderationOpts,
|
||||||
|
}: {
|
||||||
|
profiles: AppBskyActorDefs.ProfileViewDetailed[]
|
||||||
|
onSelect: (dids: string[]) => void
|
||||||
|
moderationOpts: ReturnType<typeof useModerationOpts>
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const [dids, setDids] = React.useState<string[]>(profiles.map(p => p.did))
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onSelect(dids)
|
||||||
|
}, [dids, onSelect])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toggle.Group
|
||||||
|
values={dids}
|
||||||
|
onChange={setDids}
|
||||||
|
label={_(msg`Select some accounts below to follow`)}>
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
{profiles.map(profile => (
|
||||||
|
<Toggle.Item
|
||||||
|
key={profile.did}
|
||||||
|
name={profile.did}
|
||||||
|
label={_(msg`Follow ${profile.handle}`)}>
|
||||||
|
<SuggestedAccountCard
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
</Toggle.Item>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepSuggestedAccounts() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const suggestedDids = React.useMemo(() => {
|
||||||
|
return aggregateInterestItems(
|
||||||
|
state.interestsStepResults.selectedInterests,
|
||||||
|
state.interestsStepResults.apiResponse.suggestedAccountDids,
|
||||||
|
state.interestsStepResults.apiResponse.suggestedAccountDids.default,
|
||||||
|
)
|
||||||
|
}, [state.interestsStepResults])
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const {
|
||||||
|
isLoading: isProfilesLoading,
|
||||||
|
isError,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
} = useProfilesQuery({
|
||||||
|
handles: suggestedDids,
|
||||||
|
})
|
||||||
|
const [dids, setDids] = React.useState<string[]>([])
|
||||||
|
const [saving, setSaving] = React.useState(false)
|
||||||
|
|
||||||
|
const interestsText = React.useMemo(() => {
|
||||||
|
const i = state.interestsStepResults.selectedInterests.map(
|
||||||
|
i => INTEREST_TO_DISPLAY_NAME[i],
|
||||||
|
)
|
||||||
|
return i.join(', ')
|
||||||
|
}, [state.interestsStepResults.selectedInterests])
|
||||||
|
|
||||||
|
const handleContinue = React.useCallback(async () => {
|
||||||
|
setSaving(true)
|
||||||
|
|
||||||
|
if (dids.length) {
|
||||||
|
dispatch({type: 'setSuggestedAccountsStepResults', accountDids: dids})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(false)
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
track('OnboardingV2:StepSuggestedAccounts:Start', {
|
||||||
|
selectedAccountsLength: dids.length,
|
||||||
|
})
|
||||||
|
}, [dids, setSaving, dispatch, track])
|
||||||
|
|
||||||
|
const handleSkip = React.useCallback(() => {
|
||||||
|
// if a user comes back and clicks skip, erase follows
|
||||||
|
dispatch({type: 'setSuggestedAccountsStepResults', accountDids: []})
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
|
const isLoading = isProfilesLoading && moderationOpts
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:StepSuggestedAccounts:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<IconCircle icon={At} style={[a.mb_2xl]} />
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
<Trans>Here are some accounts for your to follow</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description>
|
||||||
|
{state.interestsStepResults.selectedInterests.length ? (
|
||||||
|
<Trans>Based on your interest in {interestsText}</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>These are popular accounts you might like.</Trans>
|
||||||
|
)}
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.pt_xl]}>
|
||||||
|
{isLoading ? (
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
{Array(10)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<SuggestedAccountCardPlaceholder key={i} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : isError || !data ? (
|
||||||
|
<Text>{error?.toString()}</Text>
|
||||||
|
) : (
|
||||||
|
<Inner
|
||||||
|
profiles={data.profiles}
|
||||||
|
onSelect={setDids}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.gap_md,
|
||||||
|
gtMobile ? {flexDirection: 'row-reverse'} : a.flex_col,
|
||||||
|
]}>
|
||||||
|
<Button
|
||||||
|
disabled={dids.length === 0}
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label={_(
|
||||||
|
msg`Follow selected accounts and continue to then next step`,
|
||||||
|
)}
|
||||||
|
onPress={handleContinue}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Follow All</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={saving ? Loader : Plus} position="right" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
label={_(
|
||||||
|
msg`Continue to the next step without following any accounts`,
|
||||||
|
)}
|
||||||
|
onPress={handleSkip}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Skip</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
|
||||||
|
import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data'
|
||||||
|
import {aggregateInterestItems} from '#/screens/Onboarding/util'
|
||||||
|
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||||
|
|
||||||
|
export function StepTopicalFeeds() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([])
|
||||||
|
const [saving, setSaving] = React.useState(false)
|
||||||
|
const suggestedFeedUris = React.useMemo(() => {
|
||||||
|
return aggregateInterestItems(
|
||||||
|
state.interestsStepResults.selectedInterests,
|
||||||
|
state.interestsStepResults.apiResponse.suggestedFeedUris,
|
||||||
|
state.interestsStepResults.apiResponse.suggestedFeedUris.default,
|
||||||
|
).slice(0, 10)
|
||||||
|
}, [state.interestsStepResults])
|
||||||
|
|
||||||
|
const interestsText = React.useMemo(() => {
|
||||||
|
const i = state.interestsStepResults.selectedInterests.map(
|
||||||
|
i => INTEREST_TO_DISPLAY_NAME[i],
|
||||||
|
)
|
||||||
|
return i.join(', ')
|
||||||
|
}, [state.interestsStepResults.selectedInterests])
|
||||||
|
|
||||||
|
const saveFeeds = React.useCallback(async () => {
|
||||||
|
setSaving(true)
|
||||||
|
|
||||||
|
dispatch({type: 'setTopicalFeedsStepResults', feedUris: selectedFeedUris})
|
||||||
|
|
||||||
|
setSaving(false)
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
track('OnboardingV2:StepTopicalFeeds:End', {
|
||||||
|
selectedFeeds: selectedFeedUris,
|
||||||
|
selectedFeedsLength: selectedFeedUris.length,
|
||||||
|
})
|
||||||
|
}, [selectedFeedUris, dispatch, track])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:StepTopicalFeeds:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<IconCircle icon={ListMagnifyingGlass} style={[a.mb_2xl]} />
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
<Trans>Feeds can be topical as well!</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description>
|
||||||
|
{state.interestsStepResults.selectedInterests.length ? (
|
||||||
|
<Trans>
|
||||||
|
Here are some topical feeds based on your interests: {interestsText}
|
||||||
|
. You can choose to follow as many as you like.
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
Here are some popular topical feeds. You can choose to follow as
|
||||||
|
many as you like.
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.pb_2xl, a.pt_2xl]}>
|
||||||
|
<Toggle.Group
|
||||||
|
values={selectedFeedUris}
|
||||||
|
onChange={setSelectedFeedUris}
|
||||||
|
label={_(msg`Select topical feeds to follow from the list below`)}>
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
{suggestedFeedUris.map(uri => (
|
||||||
|
<FeedCard key={uri} config={{default: false, uri}} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Continue to next step`)}
|
||||||
|
onPress={saveFeeds}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={saving ? Loader : ChevronRight} position="right" />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {Portal} from '#/components/Portal'
|
||||||
|
|
||||||
|
import {Context, initialState, reducer} from '#/screens/Onboarding/state'
|
||||||
|
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||||
|
import {StepInterests} from '#/screens/Onboarding/StepInterests'
|
||||||
|
import {StepSuggestedAccounts} from '#/screens/Onboarding/StepSuggestedAccounts'
|
||||||
|
import {StepFollowingFeed} from '#/screens/Onboarding/StepFollowingFeed'
|
||||||
|
import {StepAlgoFeeds} from '#/screens/Onboarding/StepAlgoFeeds'
|
||||||
|
import {StepTopicalFeeds} from '#/screens/Onboarding/StepTopicalFeeds'
|
||||||
|
import {StepFinished} from '#/screens/Onboarding/StepFinished'
|
||||||
|
import {StepModeration} from '#/screens/Onboarding/StepModeration'
|
||||||
|
|
||||||
|
export function Onboarding() {
|
||||||
|
const [state, dispatch] = React.useReducer(reducer, {...initialState})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<OnboardingControls.Provider>
|
||||||
|
<Context.Provider
|
||||||
|
value={React.useMemo(() => ({state, dispatch}), [state, dispatch])}>
|
||||||
|
<Layout>
|
||||||
|
{state.activeStep === 'interests' && <StepInterests />}
|
||||||
|
{state.activeStep === 'suggestedAccounts' && (
|
||||||
|
<StepSuggestedAccounts />
|
||||||
|
)}
|
||||||
|
{state.activeStep === 'followingFeed' && <StepFollowingFeed />}
|
||||||
|
{state.activeStep === 'algoFeeds' && <StepAlgoFeeds />}
|
||||||
|
{state.activeStep === 'topicalFeeds' && <StepTopicalFeeds />}
|
||||||
|
{state.activeStep === 'moderation' && <StepModeration />}
|
||||||
|
{state.activeStep === 'finished' && <StepFinished />}
|
||||||
|
</Layout>
|
||||||
|
</Context.Provider>
|
||||||
|
</OnboardingControls.Provider>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {ApiResponseMap} from '#/screens/Onboarding/StepInterests/data'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
|
||||||
|
export type OnboardingState = {
|
||||||
|
hasPrev: boolean
|
||||||
|
totalSteps: number
|
||||||
|
activeStep:
|
||||||
|
| 'interests'
|
||||||
|
| 'suggestedAccounts'
|
||||||
|
| 'followingFeed'
|
||||||
|
| 'algoFeeds'
|
||||||
|
| 'topicalFeeds'
|
||||||
|
| 'moderation'
|
||||||
|
| 'finished'
|
||||||
|
activeStepIndex: number
|
||||||
|
|
||||||
|
interestsStepResults: {
|
||||||
|
selectedInterests: string[]
|
||||||
|
apiResponse: ApiResponseMap
|
||||||
|
}
|
||||||
|
suggestedAccountsStepResults: {
|
||||||
|
accountDids: string[]
|
||||||
|
}
|
||||||
|
algoFeedsStepResults: {
|
||||||
|
feedUris: string[]
|
||||||
|
}
|
||||||
|
topicalFeedsStepResults: {
|
||||||
|
feedUris: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingAction =
|
||||||
|
| {
|
||||||
|
type: 'next'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'prev'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'finish'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'setInterestsStepResults'
|
||||||
|
selectedInterests: string[]
|
||||||
|
apiResponse: ApiResponseMap
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'setSuggestedAccountsStepResults'
|
||||||
|
accountDids: string[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'setAlgoFeedsStepResults'
|
||||||
|
feedUris: string[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'setTopicalFeedsStepResults'
|
||||||
|
feedUris: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: OnboardingState = {
|
||||||
|
hasPrev: false,
|
||||||
|
totalSteps: 7,
|
||||||
|
activeStep: 'interests',
|
||||||
|
activeStepIndex: 1,
|
||||||
|
|
||||||
|
interestsStepResults: {
|
||||||
|
selectedInterests: [],
|
||||||
|
apiResponse: {
|
||||||
|
interests: [],
|
||||||
|
suggestedAccountDids: {},
|
||||||
|
suggestedFeedUris: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
suggestedAccountsStepResults: {
|
||||||
|
accountDids: [],
|
||||||
|
},
|
||||||
|
algoFeedsStepResults: {
|
||||||
|
feedUris: [],
|
||||||
|
},
|
||||||
|
topicalFeedsStepResults: {
|
||||||
|
feedUris: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Context = React.createContext<{
|
||||||
|
state: OnboardingState
|
||||||
|
dispatch: React.Dispatch<OnboardingAction>
|
||||||
|
}>({
|
||||||
|
state: {...initialState},
|
||||||
|
dispatch: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function reducer(
|
||||||
|
s: OnboardingState,
|
||||||
|
a: OnboardingAction,
|
||||||
|
): OnboardingState {
|
||||||
|
let next = {...s}
|
||||||
|
|
||||||
|
switch (a.type) {
|
||||||
|
case 'next': {
|
||||||
|
if (s.activeStep === 'interests') {
|
||||||
|
next.activeStep = 'suggestedAccounts'
|
||||||
|
next.activeStepIndex = 2
|
||||||
|
} else if (s.activeStep === 'suggestedAccounts') {
|
||||||
|
next.activeStep = 'followingFeed'
|
||||||
|
next.activeStepIndex = 3
|
||||||
|
} else if (s.activeStep === 'followingFeed') {
|
||||||
|
next.activeStep = 'algoFeeds'
|
||||||
|
next.activeStepIndex = 4
|
||||||
|
} else if (s.activeStep === 'algoFeeds') {
|
||||||
|
next.activeStep = 'topicalFeeds'
|
||||||
|
next.activeStepIndex = 5
|
||||||
|
} else if (s.activeStep === 'topicalFeeds') {
|
||||||
|
next.activeStep = 'moderation'
|
||||||
|
next.activeStepIndex = 6
|
||||||
|
} else if (s.activeStep === 'moderation') {
|
||||||
|
next.activeStep = 'finished'
|
||||||
|
next.activeStepIndex = 7
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'prev': {
|
||||||
|
if (s.activeStep === 'suggestedAccounts') {
|
||||||
|
next.activeStep = 'interests'
|
||||||
|
next.activeStepIndex = 1
|
||||||
|
} else if (s.activeStep === 'followingFeed') {
|
||||||
|
next.activeStep = 'suggestedAccounts'
|
||||||
|
next.activeStepIndex = 2
|
||||||
|
} else if (s.activeStep === 'algoFeeds') {
|
||||||
|
next.activeStep = 'followingFeed'
|
||||||
|
next.activeStepIndex = 3
|
||||||
|
} else if (s.activeStep === 'topicalFeeds') {
|
||||||
|
next.activeStep = 'algoFeeds'
|
||||||
|
next.activeStepIndex = 4
|
||||||
|
} else if (s.activeStep === 'moderation') {
|
||||||
|
next.activeStep = 'topicalFeeds'
|
||||||
|
next.activeStepIndex = 5
|
||||||
|
} else if (s.activeStep === 'finished') {
|
||||||
|
next.activeStep = 'moderation'
|
||||||
|
next.activeStepIndex = 6
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'finish': {
|
||||||
|
next = initialState
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'setInterestsStepResults': {
|
||||||
|
next.interestsStepResults = {
|
||||||
|
selectedInterests: a.selectedInterests,
|
||||||
|
apiResponse: a.apiResponse,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'setSuggestedAccountsStepResults': {
|
||||||
|
next.suggestedAccountsStepResults = {
|
||||||
|
accountDids: next.suggestedAccountsStepResults.accountDids.concat(
|
||||||
|
a.accountDids,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'setAlgoFeedsStepResults': {
|
||||||
|
next.algoFeedsStepResults = {
|
||||||
|
feedUris: a.feedUris,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'setTopicalFeedsStepResults': {
|
||||||
|
next.topicalFeedsStepResults = {
|
||||||
|
feedUris: next.topicalFeedsStepResults.feedUris.concat(a.feedUris),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
...next,
|
||||||
|
hasPrev: next.activeStep !== 'interests',
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`onboarding`, {
|
||||||
|
hasPrev: state.hasPrev,
|
||||||
|
activeStep: state.activeStep,
|
||||||
|
activeStepIndex: state.activeStepIndex,
|
||||||
|
interestsStepResults: {
|
||||||
|
selectedInterests: state.interestsStepResults.selectedInterests,
|
||||||
|
},
|
||||||
|
suggestedAccountsStepResults: state.suggestedAccountsStepResults,
|
||||||
|
algoFeedsStepResults: state.algoFeedsStepResults,
|
||||||
|
topicalFeedsStepResults: state.topicalFeedsStepResults,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (s.activeStep !== state.activeStep) {
|
||||||
|
logger.info(`onboarding: step changed`, {activeStep: state.activeStep})
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
import {AppBskyGraphFollow, AppBskyGraphGetFollows} from '@atproto/api'
|
||||||
|
|
||||||
|
import {until} from '#/lib/async/until'
|
||||||
|
import {getAgent} from '#/state/session'
|
||||||
|
|
||||||
|
function shuffle(array: any) {
|
||||||
|
let currentIndex = array.length,
|
||||||
|
randomIndex
|
||||||
|
|
||||||
|
// While there remain elements to shuffle.
|
||||||
|
while (currentIndex > 0) {
|
||||||
|
// Pick a remaining element.
|
||||||
|
randomIndex = Math.floor(Math.random() * currentIndex)
|
||||||
|
currentIndex--
|
||||||
|
|
||||||
|
// And swap it with the current element.
|
||||||
|
;[array[currentIndex], array[randomIndex]] = [
|
||||||
|
array[randomIndex],
|
||||||
|
array[currentIndex],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aggregateInterestItems(
|
||||||
|
interests: string[],
|
||||||
|
map: {[key: string]: string[]},
|
||||||
|
fallbackItems: string[],
|
||||||
|
) {
|
||||||
|
const selected = interests.length
|
||||||
|
const all = interests
|
||||||
|
.map(i => {
|
||||||
|
const suggestions = shuffle(map[i])
|
||||||
|
|
||||||
|
if (selected === 1) {
|
||||||
|
return suggestions // return all
|
||||||
|
} else if (selected === 2) {
|
||||||
|
return suggestions.slice(0, 5) // return 5
|
||||||
|
} else {
|
||||||
|
return suggestions.slice(0, 3) // return 3
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
// dedupe suggestions
|
||||||
|
const results = Array.from(new Set(all))
|
||||||
|
|
||||||
|
// backfill
|
||||||
|
if (results.length < 20) {
|
||||||
|
results.push(...shuffle(fallbackItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
// dedupe and return 20
|
||||||
|
return Array.from(new Set(results)).slice(0, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkWriteFollows(dids: string[]) {
|
||||||
|
const session = getAgent().session
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`bulkWriteFollows failed: no session`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const followRecords: AppBskyGraphFollow.Record[] = dids.map(did => {
|
||||||
|
return {
|
||||||
|
$type: 'app.bsky.graph.follow',
|
||||||
|
subject: did,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const followWrites = followRecords.map(r => ({
|
||||||
|
$type: 'com.atproto.repo.applyWrites#create',
|
||||||
|
collection: 'app.bsky.graph.follow',
|
||||||
|
value: r,
|
||||||
|
}))
|
||||||
|
|
||||||
|
await getAgent().com.atproto.repo.applyWrites({
|
||||||
|
repo: session.did,
|
||||||
|
writes: followWrites,
|
||||||
|
})
|
||||||
|
await whenFollowsIndexed(session.did, res => !!res.data.follows.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function whenFollowsIndexed(
|
||||||
|
actor: string,
|
||||||
|
fn: (res: AppBskyGraphGetFollows.Response) => boolean,
|
||||||
|
) {
|
||||||
|
await until(
|
||||||
|
5, // 5 tries
|
||||||
|
1e3, // 1s delay between tries
|
||||||
|
fn,
|
||||||
|
() =>
|
||||||
|
getAgent().app.bsky.graph.getFollows({
|
||||||
|
actor,
|
||||||
|
limit: 1,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kinda hacky, but we want For Your or Discover to appear as the first pinned
|
||||||
|
* feed after Following
|
||||||
|
*/
|
||||||
|
export function sortPrimaryAlgorithmFeeds(uris: string[]) {
|
||||||
|
return uris.sort(uri => {
|
||||||
|
return uri.includes('the-algorithm')
|
||||||
|
? -1
|
||||||
|
: uri.includes('whats-hot')
|
||||||
|
? 0
|
||||||
|
: 1
|
||||||
|
})
|
||||||
|
}
|
|
@ -48,4 +48,5 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
|
||||||
feedViewPrefs: DEFAULT_HOME_FEED_PREFS,
|
feedViewPrefs: DEFAULT_HOME_FEED_PREFS,
|
||||||
threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
|
threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
|
||||||
userAge: 13, // TODO(pwi)
|
userAge: 13, // TODO(pwi)
|
||||||
|
interests: {tags: []},
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,17 @@ import {
|
||||||
BskyFeedViewPreference,
|
BskyFeedViewPreference,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
export type ConfigurableLabelGroup =
|
export const configurableLabelGroups = [
|
||||||
| 'nsfw'
|
'nsfw',
|
||||||
| 'nudity'
|
'nudity',
|
||||||
| 'suggestive'
|
'suggestive',
|
||||||
| 'gore'
|
'gore',
|
||||||
| 'hate'
|
'hate',
|
||||||
| 'spam'
|
'spam',
|
||||||
| 'impersonation'
|
'impersonation',
|
||||||
|
] as const
|
||||||
|
export type ConfigurableLabelGroup = (typeof configurableLabelGroups)[number]
|
||||||
|
|
||||||
export type LabelGroup =
|
export type LabelGroup =
|
||||||
| ConfigurableLabelGroup
|
| ConfigurableLabelGroup
|
||||||
| 'illegal'
|
| 'illegal'
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {STALE} from '#/state/queries'
|
||||||
import {track} from '#/lib/analytics/analytics'
|
import {track} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
export const RQKEY = (did: string) => ['profile', did]
|
export const RQKEY = (did: string) => ['profile', did]
|
||||||
|
export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
|
||||||
|
|
||||||
export function useProfileQuery({did}: {did: string | undefined}) {
|
export function useProfileQuery({did}: {did: string | undefined}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
@ -45,6 +46,17 @@ export function useProfileQuery({did}: {did: string | undefined}) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useProfilesQuery({handles}: {handles: string[]}) {
|
||||||
|
return useQuery({
|
||||||
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
|
queryKey: profilesQueryKey(handles),
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getAgent().getProfiles({actors: handles})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
interface ProfileUpdateParams {
|
interface ProfileUpdateParams {
|
||||||
profile: AppBskyActorDefs.ProfileView
|
profile: AppBskyActorDefs.ProfileView
|
||||||
updates:
|
updates:
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from '#/components/Button'
|
} from '#/components/Button'
|
||||||
import {H1} from '#/components/Typography'
|
import {H1} from '#/components/Typography'
|
||||||
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
|
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
|
||||||
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
|
|
||||||
export function Buttons() {
|
export function Buttons() {
|
||||||
|
@ -91,14 +92,16 @@ export function Buttons() {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_wrap, a.gap_md, a.align_start]}>
|
||||||
<Button
|
<Button
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
color="gradient_sky"
|
color="gradient_sky"
|
||||||
size="large"
|
size="large"
|
||||||
label="Link out">
|
label="Link out">
|
||||||
<ButtonText>Link out</ButtonText>
|
<ButtonText>Link out</ButtonText>
|
||||||
<ButtonIcon icon={ArrowTopRight} />
|
<ButtonIcon icon={ArrowTopRight} position="right" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -107,7 +110,7 @@ export function Buttons() {
|
||||||
size="small"
|
size="small"
|
||||||
label="Link out">
|
label="Link out">
|
||||||
<ButtonText>Link out</ButtonText>
|
<ButtonText>Link out</ButtonText>
|
||||||
<ButtonIcon icon={ArrowTopRight} />
|
<ButtonIcon icon={ArrowTopRight} position="right" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -115,8 +118,86 @@ export function Buttons() {
|
||||||
color="gradient_sky"
|
color="gradient_sky"
|
||||||
size="small"
|
size="small"
|
||||||
label="Link out">
|
label="Link out">
|
||||||
<ButtonIcon icon={Globe} />
|
<ButtonText>Link xxxxxx</ButtonText>
|
||||||
<ButtonText>See the world</ButtonText>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="small"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={Globe} position="left" />
|
||||||
|
<ButtonText>Link out</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.gap_md, a.align_start]}>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sunset"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.gap_md, a.align_start]}>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
shape="square"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sunset"
|
||||||
|
size="small"
|
||||||
|
shape="square"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
shape="square"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
shape="square"
|
||||||
|
label="Link out">
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -209,6 +209,23 @@ export function Forms() {
|
||||||
Show
|
Show
|
||||||
</ToggleButton.Button>
|
</ToggleButton.Button>
|
||||||
</ToggleButton.Group>
|
</ToggleButton.Group>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<ToggleButton.Group
|
||||||
|
label="Preferences"
|
||||||
|
values={toggleGroupDValues}
|
||||||
|
onChange={setToggleGroupDValues}>
|
||||||
|
<ToggleButton.Button name="hide" label="Hide">
|
||||||
|
Hide
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="warn" label="Warn">
|
||||||
|
Warn
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="show" label="Show">
|
||||||
|
Show
|
||||||
|
</ToggleButton.Button>
|
||||||
|
</ToggleButton.Group>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,38 +1,39 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
import {ButtonText} from '#/components/Button'
|
import {ButtonText} from '#/components/Button'
|
||||||
import {Link} from '#/components/Link'
|
import {InlineLink, Link} from '#/components/Link'
|
||||||
import {H1, H3} from '#/components/Typography'
|
import {H1, H3, Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function Links() {
|
export function Links() {
|
||||||
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View style={[a.gap_md, a.align_start]}>
|
<View style={[a.gap_md, a.align_start]}>
|
||||||
<H1>Links</H1>
|
<H1>Links</H1>
|
||||||
|
|
||||||
<View style={[a.gap_md, a.align_start]}>
|
<View style={[a.gap_md, a.align_start]}>
|
||||||
<Link
|
<InlineLink
|
||||||
to="https://blueskyweb.xyz"
|
to="https://blueskyweb.xyz"
|
||||||
warnOnMismatchingTextChild
|
warnOnMismatchingTextChild
|
||||||
style={[a.text_md]}>
|
style={[a.text_md]}>
|
||||||
External
|
External
|
||||||
</Link>
|
</InlineLink>
|
||||||
<Link to="https://blueskyweb.xyz" style={[a.text_md]}>
|
<InlineLink to="https://blueskyweb.xyz" style={[a.text_md]}>
|
||||||
<H3>External with custom children</H3>
|
<H3>External with custom children</H3>
|
||||||
</Link>
|
</InlineLink>
|
||||||
<Link
|
<InlineLink
|
||||||
to="https://blueskyweb.xyz"
|
to="https://blueskyweb.xyz"
|
||||||
warnOnMismatchingTextChild
|
warnOnMismatchingTextChild
|
||||||
style={[a.text_lg]}>
|
style={[a.text_lg]}>
|
||||||
https://blueskyweb.xyz
|
https://blueskyweb.xyz
|
||||||
</Link>
|
</InlineLink>
|
||||||
<Link
|
<InlineLink
|
||||||
to="https://bsky.app/profile/bsky.app"
|
to="https://bsky.app/profile/bsky.app"
|
||||||
warnOnMismatchingTextChild
|
warnOnMismatchingTextChild
|
||||||
style={[a.text_md]}>
|
style={[a.text_md]}>
|
||||||
Internal
|
Internal
|
||||||
</Link>
|
</InlineLink>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
@ -42,6 +43,29 @@ export function Links() {
|
||||||
to="https://bsky.app/profile/bsky.app">
|
to="https://bsky.app/profile/bsky.app">
|
||||||
<ButtonText>Link as a button</ButtonText>
|
<ButtonText>Link as a button</ButtonText>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
label="View @bsky.app's profile"
|
||||||
|
to="https://bsky.app/profile/bsky.app">
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_md,
|
||||||
|
a.rounded_md,
|
||||||
|
a.p_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 32, height: 32},
|
||||||
|
a.rounded_full,
|
||||||
|
t.atoms.bg_contrast_200,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Text>View @bsky.app's profile</Text>
|
||||||
|
</View>
|
||||||
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {View} from 'react-native'
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography'
|
import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography'
|
||||||
|
import {RichText} from '#/components/RichText'
|
||||||
|
|
||||||
export function Typography() {
|
export function Typography() {
|
||||||
return (
|
return (
|
||||||
|
@ -25,6 +26,16 @@ export function Typography() {
|
||||||
<Text style={[a.text_sm]}>atoms.text_sm</Text>
|
<Text style={[a.text_sm]}>atoms.text_sm</Text>
|
||||||
<Text style={[a.text_xs]}>atoms.text_xs</Text>
|
<Text style={[a.text_xs]}>atoms.text_xs</Text>
|
||||||
<Text style={[a.text_2xs]}>atoms.text_2xs</Text>
|
<Text style={[a.text_2xs]}>atoms.text_2xs</Text>
|
||||||
|
|
||||||
|
<RichText
|
||||||
|
resolveFacets
|
||||||
|
value={`This is rich text. It can have mentions like @bsky.app or links like https://blueskyweb.xyz`}
|
||||||
|
/>
|
||||||
|
<RichText
|
||||||
|
resolveFacets
|
||||||
|
value={`This is rich text. It can have mentions like @bsky.app or links like https://blueskyweb.xyz`}
|
||||||
|
style={[a.text_xl]}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {PWI_ENABLED} from '#/lib/build-flags'
|
import {PWI_ENABLED, NEW_ONBOARDING_ENABLED} from '#/lib/build-flags'
|
||||||
|
|
||||||
// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
|
// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
|
||||||
// MIT License
|
// MIT License
|
||||||
|
@ -38,6 +38,7 @@ import {isWeb} from 'platform/detection'
|
||||||
import {Deactivated} from '#/screens/Deactivated'
|
import {Deactivated} from '#/screens/Deactivated'
|
||||||
import {LoggedOut} from '../com/auth/LoggedOut'
|
import {LoggedOut} from '../com/auth/LoggedOut'
|
||||||
import {Onboarding} from '../com/auth/Onboarding'
|
import {Onboarding} from '../com/auth/Onboarding'
|
||||||
|
import {Onboarding as NewOnboarding} from '#/screens/Onboarding'
|
||||||
|
|
||||||
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
||||||
requireAuth?: boolean
|
requireAuth?: boolean
|
||||||
|
@ -111,8 +112,12 @@ function NativeStackNavigator({
|
||||||
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
||||||
}
|
}
|
||||||
if (onboardingState.isActive) {
|
if (onboardingState.isActive) {
|
||||||
|
if (NEW_ONBOARDING_ENABLED) {
|
||||||
|
return <NewOnboarding />
|
||||||
|
} else {
|
||||||
return <Onboarding />
|
return <Onboarding />
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const newDescriptors: typeof descriptors = {}
|
const newDescriptors: typeof descriptors = {}
|
||||||
for (let key in descriptors) {
|
for (let key in descriptors) {
|
||||||
const descriptor = descriptors[key]
|
const descriptor = descriptors[key]
|
||||||
|
|
|
@ -40,26 +40,7 @@
|
||||||
/* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
|
/* 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%;
|
-webkit-text-size-adjust: 100%;
|
||||||
height: calc(100% + env(safe-area-inset-top));
|
height: calc(100% + env(safe-area-inset-top));
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove autofill styles on Webkit */
|
|
||||||
input:-webkit-autofill,
|
|
||||||
input:-webkit-autofill:hover,
|
|
||||||
input:-webkit-autofill:focus,
|
|
||||||
textarea:-webkit-autofill,
|
|
||||||
textarea:-webkit-autofill:hover,
|
|
||||||
textarea:-webkit-autofill:focus,
|
|
||||||
select:-webkit-autofill,
|
|
||||||
select:-webkit-autofill:hover,
|
|
||||||
select:-webkit-autofill:focus {
|
|
||||||
border: 0;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
}
|
|
||||||
/* Force left-align date/time inputs on iOS mobile */
|
|
||||||
input::-webkit-date-and-time-value {
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color theming */
|
/* Color theming */
|
||||||
|
@ -71,7 +52,7 @@
|
||||||
html.colorMode--dark {
|
html.colorMode--dark {
|
||||||
--text: white;
|
--text: white;
|
||||||
--background: hsl(211, 20%, 4%);
|
--background: hsl(211, 20%, 4%);
|
||||||
--backgroundLight: hsl(211, 20%, 10%);
|
--backgroundLight: hsl(211, 20%, 20%);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
@ -85,11 +66,33 @@
|
||||||
html.colorMode--system {
|
html.colorMode--system {
|
||||||
--text: white;
|
--text: white;
|
||||||
--background: hsl(211, 20%, 4%);
|
--background: hsl(211, 20%, 4%);
|
||||||
--backgroundLight: hsl(211, 20%, 10%);
|
--backgroundLight: hsl(211, 20%, 20%);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--backgroundLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* Allows you to scroll below the viewport; default value is visible */
|
/* Allows you to scroll below the viewport; default value is visible */
|
||||||
|
|