diff --git a/.github/workflows/mobile-build-nightly.yml b/.github/workflows/mobile-build-nightly.yml new file mode 100644 index 0000000000..3ba4f57436 --- /dev/null +++ b/.github/workflows/mobile-build-nightly.yml @@ -0,0 +1,52 @@ +name: Build Tlon Mobile Nightly +on: + schedule: + - cron: "0 8 * * *" + +jobs: + deploy: + runs-on: ubuntu-latest + name: Create mobile builds + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check for changes in develop + id: changes + run: | + git fetch origin develop + if [ $(git rev-list --since="24 hours ago" origin/develop --count) -eq 0 ]; then + echo "No changes in develop in the past 24 hours" + echo "::set-output name=changes::0" + exit 0 + else + echo "::set-output name=changes::1" + fi + - name: Set up Node.js + if: steps.changes.outputs.changes != '0' + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + - name: Set up Expo and EAS + if: steps.changes.outputs.changes != '0' + uses: expo/expo-github-action@v8 + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + - name: Setup PNPM + if: steps.changes.outputs.changes != '0' + uses: pnpm/action-setup@v3 + - name: Install dependencies + if: steps.changes.outputs.changes != '0' + run: pnpm install --frozen-lockfile + - name: Build for selected platforms + if: steps.changes.outputs.changes != '0' + working-directory: ./apps/tlon-mobile + run: | + eas build --profile preview --platform all --non-interactive --auto-submit + env: + EXPO_APPLE_ID: ${{ secrets.EXPO_APPLE_ID }} + EXPO_APPLE_PASSWORD: ${{ secrets.EXPO_APPLE_PASSWORD }} + NOTIFY_PROVIDER: "binnec-dozzod-marnus" + NOTIFY_SERVICE: "tlon-preview-release" diff --git a/apps/tlon-mobile/.env.sample b/apps/tlon-mobile/.env.sample index 7141de8b6f..2f3f03903a 100644 --- a/apps/tlon-mobile/.env.sample +++ b/apps/tlon-mobile/.env.sample @@ -6,6 +6,7 @@ NOTIFY_SERVICE=tlon-preview-debug # Prefills auth flow for quicker logins DEFAULT_TLON_LOGIN_EMAIL= DEFAULT_TLON_LOGIN_PASSWORD= +DEFAULT_INVITE_LINK_URL= DEFAULT_SHIP_LOGIN_URL= DEFAULT_SHIP_LOGIN_ACCESS_CODE= diff --git a/apps/tlon-mobile/README.md b/apps/tlon-mobile/README.md index d2abc94a82..667511662d 100644 --- a/apps/tlon-mobile/README.md +++ b/apps/tlon-mobile/README.md @@ -115,6 +115,7 @@ To streamline testing the login flow, you can use env variables to prepopulate f ``` DEFAULT_TLON_LOGIN_EMAIL= DEFAULT_TLON_LOGIN_PASSWORD= +DEFAULT_INVITE_LINK_URL= DEFAULT_SHIP_LOGIN_URL= DEFAULT_SHIP_LOGIN_ACCESS_CODE= ``` diff --git a/apps/tlon-mobile/android/app/build.gradle b/apps/tlon-mobile/android/app/build.gradle index 8572d5c316..6603e28a5f 100644 --- a/apps/tlon-mobile/android/app/build.gradle +++ b/apps/tlon-mobile/android/app/build.gradle @@ -88,7 +88,7 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion compileSdk rootProject.ext.compileSdkVersion versionCode 108 - versionName "4.0.1" + versionName "4.0.3" buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString()) } diff --git a/apps/tlon-mobile/android/app/src/main/ic_launcher-playstore.png b/apps/tlon-mobile/android/app/src/main/ic_launcher-playstore.png index 7404b4b8ad..6ba91a6664 100644 Binary files a/apps/tlon-mobile/android/app/src/main/ic_launcher-playstore.png and b/apps/tlon-mobile/android/app/src/main/ic_launcher-playstore.png differ diff --git a/apps/tlon-mobile/android/app/src/main/res/drawable/ic_launcher_background.xml b/apps/tlon-mobile/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..dbfab5b176 --- /dev/null +++ b/apps/tlon-mobile/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 1a4bfc9386..c4a603d4cc 100644 --- a/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - + - \ No newline at end of file diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 4ae7d12378..c4a603d4cc 100644 --- a/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/apps/tlon-mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 3bf1858e28..3bdb07fcfd 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index f7098bb639..689d863494 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index f2667dee87..6b6aba3e4a 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 09bdf5321b..1d65813fe7 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index bbcc495eb4..28f889dfa8 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index d778996a6d..e79652444b 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 54c42abd5e..dd9aa9cdea 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index 77f8893a0c..aca7b0d56e 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index d860b0020b..735bb49be6 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 68045defad..cc71c7437b 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index a265b5ab8d..806a8ac7a6 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 77260788a8..1e316d7ed6 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 076d9cfc2b..7f2aa9199f 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index fd59597c9d..193f32caf6 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 7aa9493ace..c5467f17fc 100644 Binary files a/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/apps/tlon-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/apps/tlon-mobile/app.config.ts b/apps/tlon-mobile/app.config.ts index 97f909c678..0c2b6eb1f2 100644 --- a/apps/tlon-mobile/app.config.ts +++ b/apps/tlon-mobile/app.config.ts @@ -30,6 +30,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ defaultPriorityToken: process.env.DEFAULT_PRIORITY_TOKEN, defaultTlonLoginEmail: process.env.DEFAULT_TLON_LOGIN_EMAIL, defaultTlonLoginPassword: process.env.DEFAULT_TLON_LOGIN_PASSWORD, + defaultInviteLinkUrl: process.env.DEFAULT_INVITE_LINK_URL, defaultShipLoginUrl: process.env.DEFAULT_SHIP_LOGIN_URL, defaultShipLoginAccessCode: process.env.DEFAULT_SHIP_LOGIN_ACCESS_CODE, recaptchaSiteKeyAndroid: process.env.RECAPTCHA_SITE_KEY_ANDROID, @@ -37,11 +38,15 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ enabledLoggers: process.env.ENABLED_LOGGERS, ignoreCosmos: process.env.IGNORE_COSMOS, TlonEmployeeGroup: process.env.TLON_EMPLOYEE_GROUP, - branchKey: process.env.BRANCH_KEY, - branchDomain: process.env.BRANCH_DOMAIN, + branchKey: isPreview + ? process.env.BRANCH_KEY_TEST + : process.env.BRANCH_KEY_PROD, + branchDomain: isPreview + ? process.env.BRANCH_DOMAIN_TEST + : process.env.BRANCH_DOMAIN_PROD, }, ios: { - runtimeVersion: '4.0.1', + runtimeVersion: '4.0.2', // demo builds triggered by GitHub require this to be explicitly set rather than handled // elsewhere bundleIdentifier: @@ -51,7 +56,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ }, }, android: { - runtimeVersion: '4.0.1', + runtimeVersion: '4.0.2', }, plugins: [ '@react-native-firebase/app', diff --git a/apps/tlon-mobile/assets/images/faces-dark.png b/apps/tlon-mobile/assets/images/faces-dark.png new file mode 100644 index 0000000000..320adc43f7 Binary files /dev/null and b/apps/tlon-mobile/assets/images/faces-dark.png differ diff --git a/apps/tlon-mobile/assets/images/faces.png b/apps/tlon-mobile/assets/images/faces.png new file mode 100644 index 0000000000..836701efc4 Binary files /dev/null and b/apps/tlon-mobile/assets/images/faces.png differ diff --git a/apps/tlon-mobile/assets/images/welcome-icon.png b/apps/tlon-mobile/assets/images/welcome-icon.png new file mode 100644 index 0000000000..5ebb8479e5 Binary files /dev/null and b/apps/tlon-mobile/assets/images/welcome-icon.png differ diff --git a/apps/tlon-mobile/cosmos.imports.ts b/apps/tlon-mobile/cosmos.imports.ts index 5e5e00059c..41b9a78dda 100644 --- a/apps/tlon-mobile/cosmos.imports.ts +++ b/apps/tlon-mobile/cosmos.imports.ts @@ -8,54 +8,56 @@ import * as fixture1 from './src/fixtures/ViewReactionsSheet.fixture'; import * as fixture2 from './src/fixtures/VideoEmbed.fixture'; import * as fixture3 from './src/fixtures/UserProfileScreen.fixture'; import * as fixture4 from './src/fixtures/Text.fixture'; -import * as fixture5 from './src/fixtures/SearchBar.fixture'; -import * as fixture6 from './src/fixtures/ScreenHeader.fixture'; -import * as fixture7 from './src/fixtures/ReferenceSkeleton.fixture'; -import * as fixture8 from './src/fixtures/ProfileSheet.fixture'; -import * as fixture9 from './src/fixtures/ProfileBlock.fixture'; -import * as fixture10 from './src/fixtures/PostScreen.fixture'; -import * as fixture11 from './src/fixtures/PostReference.fixture'; -import * as fixture12 from './src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture'; -import * as fixture13 from './src/fixtures/OutsideEmbed.fixture'; -import * as fixture14 from './src/fixtures/MetaEditorScreen.fixture'; -import * as fixture15 from './src/fixtures/MessageInput.fixture'; -import * as fixture16 from './src/fixtures/MessageActions.fixture'; -import * as fixture17 from './src/fixtures/InviteUsersSheet.fixture'; -import * as fixture18 from './src/fixtures/InputToolbar.fixture'; -import * as fixture19 from './src/fixtures/Input.fixture'; -import * as fixture20 from './src/fixtures/ImageViewer.fixture'; -import * as fixture21 from './src/fixtures/GroupListItem.fixture'; -import * as fixture22 from './src/fixtures/GroupList.fixture'; -import * as fixture23 from './src/fixtures/GalleryPost.fixture'; -import * as fixture24 from './src/fixtures/Form.fixture'; -import * as fixture25 from './src/fixtures/FindGroups.fixture'; -import * as fixture26 from './src/fixtures/CreateGroup.fixture'; -import * as fixture27 from './src/fixtures/ContactList.fixture'; -import * as fixture28 from './src/fixtures/ChatMessage.fixture'; -import * as fixture29 from './src/fixtures/ChannelSwitcherSheet.fixture'; -import * as fixture30 from './src/fixtures/ChannelHeader.fixture'; -import * as fixture31 from './src/fixtures/ChannelDivider.fixture'; -import * as fixture32 from './src/fixtures/Channel.fixture'; -import * as fixture33 from './src/fixtures/Button.fixture'; -import * as fixture34 from './src/fixtures/BlockSectionList.fixture'; -import * as fixture35 from './src/fixtures/Avatar.fixture'; -import * as fixture36 from './src/fixtures/AudioEmbed.fixture'; -import * as fixture37 from './src/fixtures/AttachmentPreviewList.fixture'; -import * as fixture38 from './src/fixtures/AddGroupSheet.fixture'; -import * as fixture39 from './src/fixtures/Activity.fixture'; -import * as fixture40 from './src/fixtures/DetailView/NotebookDetailView.fixture'; -import * as fixture41 from './src/fixtures/DetailView/GalleryDetailView.fixture'; -import * as fixture42 from './src/fixtures/DetailView/ChatDetailView.fixture'; -import * as fixture43 from './src/fixtures/ActionSheet/SendPostRetrySheet.fixture'; -import * as fixture44 from './src/fixtures/ActionSheet/ProfileSheet.fixture'; -import * as fixture45 from './src/fixtures/ActionSheet/GroupPreviewSheet.fixture'; -import * as fixture46 from './src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture'; -import * as fixture47 from './src/fixtures/ActionSheet/GenericActionSheet.fixture'; -import * as fixture48 from './src/fixtures/ActionSheet/EditSectionNameSheet.fixture'; -import * as fixture49 from './src/fixtures/ActionSheet/DeleteSheet.fixture'; -import * as fixture50 from './src/fixtures/ActionSheet/CreateChannelSheet.fixture'; -import * as fixture51 from './src/fixtures/ActionSheet/AttachmentSheet.fixture'; -import * as fixture52 from './src/fixtures/ActionSheet/AddGalleryPostSheet.fixture'; +import * as fixture5 from './src/fixtures/SetNicknameScreen.fixture'; +import * as fixture6 from './src/fixtures/SearchBar.fixture'; +import * as fixture7 from './src/fixtures/ScreenHeader.fixture'; +import * as fixture8 from './src/fixtures/ReferenceSkeleton.fixture'; +import * as fixture9 from './src/fixtures/ProfileSheet.fixture'; +import * as fixture10 from './src/fixtures/ProfileBlock.fixture'; +import * as fixture11 from './src/fixtures/PostScreen.fixture'; +import * as fixture12 from './src/fixtures/PostReference.fixture'; +import * as fixture13 from './src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture'; +import * as fixture14 from './src/fixtures/OutsideEmbed.fixture'; +import * as fixture15 from './src/fixtures/Onboarding.fixture'; +import * as fixture16 from './src/fixtures/MetaEditorScreen.fixture'; +import * as fixture17 from './src/fixtures/MessageInput.fixture'; +import * as fixture18 from './src/fixtures/MessageActions.fixture'; +import * as fixture19 from './src/fixtures/InviteUsersSheet.fixture'; +import * as fixture20 from './src/fixtures/InputToolbar.fixture'; +import * as fixture21 from './src/fixtures/Input.fixture'; +import * as fixture22 from './src/fixtures/ImageViewer.fixture'; +import * as fixture23 from './src/fixtures/GroupListItem.fixture'; +import * as fixture24 from './src/fixtures/GroupList.fixture'; +import * as fixture25 from './src/fixtures/GalleryPost.fixture'; +import * as fixture26 from './src/fixtures/Form.fixture'; +import * as fixture27 from './src/fixtures/FindGroups.fixture'; +import * as fixture28 from './src/fixtures/CreateGroup.fixture'; +import * as fixture29 from './src/fixtures/ContactList.fixture'; +import * as fixture30 from './src/fixtures/ChatMessage.fixture'; +import * as fixture31 from './src/fixtures/ChannelSwitcherSheet.fixture'; +import * as fixture32 from './src/fixtures/ChannelHeader.fixture'; +import * as fixture33 from './src/fixtures/ChannelDivider.fixture'; +import * as fixture34 from './src/fixtures/Channel.fixture'; +import * as fixture35 from './src/fixtures/Button.fixture'; +import * as fixture36 from './src/fixtures/BlockSectionList.fixture'; +import * as fixture37 from './src/fixtures/Avatar.fixture'; +import * as fixture38 from './src/fixtures/AudioEmbed.fixture'; +import * as fixture39 from './src/fixtures/AttachmentPreviewList.fixture'; +import * as fixture40 from './src/fixtures/AddGroupSheet.fixture'; +import * as fixture41 from './src/fixtures/Activity.fixture'; +import * as fixture42 from './src/fixtures/DetailView/NotebookDetailView.fixture'; +import * as fixture43 from './src/fixtures/DetailView/GalleryDetailView.fixture'; +import * as fixture44 from './src/fixtures/DetailView/ChatDetailView.fixture'; +import * as fixture45 from './src/fixtures/ActionSheet/SendPostRetrySheet.fixture'; +import * as fixture46 from './src/fixtures/ActionSheet/ProfileSheet.fixture'; +import * as fixture47 from './src/fixtures/ActionSheet/GroupPreviewSheet.fixture'; +import * as fixture48 from './src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture'; +import * as fixture49 from './src/fixtures/ActionSheet/GenericActionSheet.fixture'; +import * as fixture50 from './src/fixtures/ActionSheet/EditSectionNameSheet.fixture'; +import * as fixture51 from './src/fixtures/ActionSheet/DeleteSheet.fixture'; +import * as fixture52 from './src/fixtures/ActionSheet/CreateChannelSheet.fixture'; +import * as fixture53 from './src/fixtures/ActionSheet/AttachmentSheet.fixture'; +import * as fixture54 from './src/fixtures/ActionSheet/AddGalleryPostSheet.fixture'; import * as decorator0 from './src/fixtures/cosmos.decorator'; @@ -70,54 +72,56 @@ const fixtures = { 'src/fixtures/VideoEmbed.fixture.tsx': { module: fixture2 }, 'src/fixtures/UserProfileScreen.fixture.tsx': { module: fixture3 }, 'src/fixtures/Text.fixture.tsx': { module: fixture4 }, - 'src/fixtures/SearchBar.fixture.tsx': { module: fixture5 }, - 'src/fixtures/ScreenHeader.fixture.tsx': { module: fixture6 }, - 'src/fixtures/ReferenceSkeleton.fixture.tsx': { module: fixture7 }, - 'src/fixtures/ProfileSheet.fixture.tsx': { module: fixture8 }, - 'src/fixtures/ProfileBlock.fixture.tsx': { module: fixture9 }, - 'src/fixtures/PostScreen.fixture.tsx': { module: fixture10 }, - 'src/fixtures/PostReference.fixture.tsx': { module: fixture11 }, - 'src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture.tsx': { module: fixture12 }, - 'src/fixtures/OutsideEmbed.fixture.tsx': { module: fixture13 }, - 'src/fixtures/MetaEditorScreen.fixture.tsx': { module: fixture14 }, - 'src/fixtures/MessageInput.fixture.tsx': { module: fixture15 }, - 'src/fixtures/MessageActions.fixture.tsx': { module: fixture16 }, - 'src/fixtures/InviteUsersSheet.fixture.tsx': { module: fixture17 }, - 'src/fixtures/InputToolbar.fixture.tsx': { module: fixture18 }, - 'src/fixtures/Input.fixture.tsx': { module: fixture19 }, - 'src/fixtures/ImageViewer.fixture.tsx': { module: fixture20 }, - 'src/fixtures/GroupListItem.fixture.tsx': { module: fixture21 }, - 'src/fixtures/GroupList.fixture.tsx': { module: fixture22 }, - 'src/fixtures/GalleryPost.fixture.tsx': { module: fixture23 }, - 'src/fixtures/Form.fixture.tsx': { module: fixture24 }, - 'src/fixtures/FindGroups.fixture.tsx': { module: fixture25 }, - 'src/fixtures/CreateGroup.fixture.tsx': { module: fixture26 }, - 'src/fixtures/ContactList.fixture.tsx': { module: fixture27 }, - 'src/fixtures/ChatMessage.fixture.tsx': { module: fixture28 }, - 'src/fixtures/ChannelSwitcherSheet.fixture.tsx': { module: fixture29 }, - 'src/fixtures/ChannelHeader.fixture.tsx': { module: fixture30 }, - 'src/fixtures/ChannelDivider.fixture.tsx': { module: fixture31 }, - 'src/fixtures/Channel.fixture.tsx': { module: fixture32 }, - 'src/fixtures/Button.fixture.tsx': { module: fixture33 }, - 'src/fixtures/BlockSectionList.fixture.tsx': { module: fixture34 }, - 'src/fixtures/Avatar.fixture.tsx': { module: fixture35 }, - 'src/fixtures/AudioEmbed.fixture.tsx': { module: fixture36 }, - 'src/fixtures/AttachmentPreviewList.fixture.tsx': { module: fixture37 }, - 'src/fixtures/AddGroupSheet.fixture.tsx': { module: fixture38 }, - 'src/fixtures/Activity.fixture.tsx': { module: fixture39 }, - 'src/fixtures/DetailView/NotebookDetailView.fixture.tsx': { module: fixture40 }, - 'src/fixtures/DetailView/GalleryDetailView.fixture.tsx': { module: fixture41 }, - 'src/fixtures/DetailView/ChatDetailView.fixture.tsx': { module: fixture42 }, - 'src/fixtures/ActionSheet/SendPostRetrySheet.fixture.tsx': { module: fixture43 }, - 'src/fixtures/ActionSheet/ProfileSheet.fixture.tsx': { module: fixture44 }, - 'src/fixtures/ActionSheet/GroupPreviewSheet.fixture.tsx': { module: fixture45 }, - 'src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture.tsx': { module: fixture46 }, - 'src/fixtures/ActionSheet/GenericActionSheet.fixture.tsx': { module: fixture47 }, - 'src/fixtures/ActionSheet/EditSectionNameSheet.fixture.tsx': { module: fixture48 }, - 'src/fixtures/ActionSheet/DeleteSheet.fixture.tsx': { module: fixture49 }, - 'src/fixtures/ActionSheet/CreateChannelSheet.fixture.tsx': { module: fixture50 }, - 'src/fixtures/ActionSheet/AttachmentSheet.fixture.tsx': { module: fixture51 }, - 'src/fixtures/ActionSheet/AddGalleryPostSheet.fixture.tsx': { module: fixture52 } + 'src/fixtures/SetNicknameScreen.fixture.tsx': { module: fixture5 }, + 'src/fixtures/SearchBar.fixture.tsx': { module: fixture6 }, + 'src/fixtures/ScreenHeader.fixture.tsx': { module: fixture7 }, + 'src/fixtures/ReferenceSkeleton.fixture.tsx': { module: fixture8 }, + 'src/fixtures/ProfileSheet.fixture.tsx': { module: fixture9 }, + 'src/fixtures/ProfileBlock.fixture.tsx': { module: fixture10 }, + 'src/fixtures/PostScreen.fixture.tsx': { module: fixture11 }, + 'src/fixtures/PostReference.fixture.tsx': { module: fixture12 }, + 'src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture.tsx': { module: fixture13 }, + 'src/fixtures/OutsideEmbed.fixture.tsx': { module: fixture14 }, + 'src/fixtures/Onboarding.fixture.tsx': { module: fixture15 }, + 'src/fixtures/MetaEditorScreen.fixture.tsx': { module: fixture16 }, + 'src/fixtures/MessageInput.fixture.tsx': { module: fixture17 }, + 'src/fixtures/MessageActions.fixture.tsx': { module: fixture18 }, + 'src/fixtures/InviteUsersSheet.fixture.tsx': { module: fixture19 }, + 'src/fixtures/InputToolbar.fixture.tsx': { module: fixture20 }, + 'src/fixtures/Input.fixture.tsx': { module: fixture21 }, + 'src/fixtures/ImageViewer.fixture.tsx': { module: fixture22 }, + 'src/fixtures/GroupListItem.fixture.tsx': { module: fixture23 }, + 'src/fixtures/GroupList.fixture.tsx': { module: fixture24 }, + 'src/fixtures/GalleryPost.fixture.tsx': { module: fixture25 }, + 'src/fixtures/Form.fixture.tsx': { module: fixture26 }, + 'src/fixtures/FindGroups.fixture.tsx': { module: fixture27 }, + 'src/fixtures/CreateGroup.fixture.tsx': { module: fixture28 }, + 'src/fixtures/ContactList.fixture.tsx': { module: fixture29 }, + 'src/fixtures/ChatMessage.fixture.tsx': { module: fixture30 }, + 'src/fixtures/ChannelSwitcherSheet.fixture.tsx': { module: fixture31 }, + 'src/fixtures/ChannelHeader.fixture.tsx': { module: fixture32 }, + 'src/fixtures/ChannelDivider.fixture.tsx': { module: fixture33 }, + 'src/fixtures/Channel.fixture.tsx': { module: fixture34 }, + 'src/fixtures/Button.fixture.tsx': { module: fixture35 }, + 'src/fixtures/BlockSectionList.fixture.tsx': { module: fixture36 }, + 'src/fixtures/Avatar.fixture.tsx': { module: fixture37 }, + 'src/fixtures/AudioEmbed.fixture.tsx': { module: fixture38 }, + 'src/fixtures/AttachmentPreviewList.fixture.tsx': { module: fixture39 }, + 'src/fixtures/AddGroupSheet.fixture.tsx': { module: fixture40 }, + 'src/fixtures/Activity.fixture.tsx': { module: fixture41 }, + 'src/fixtures/DetailView/NotebookDetailView.fixture.tsx': { module: fixture42 }, + 'src/fixtures/DetailView/GalleryDetailView.fixture.tsx': { module: fixture43 }, + 'src/fixtures/DetailView/ChatDetailView.fixture.tsx': { module: fixture44 }, + 'src/fixtures/ActionSheet/SendPostRetrySheet.fixture.tsx': { module: fixture45 }, + 'src/fixtures/ActionSheet/ProfileSheet.fixture.tsx': { module: fixture46 }, + 'src/fixtures/ActionSheet/GroupPreviewSheet.fixture.tsx': { module: fixture47 }, + 'src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture.tsx': { module: fixture48 }, + 'src/fixtures/ActionSheet/GenericActionSheet.fixture.tsx': { module: fixture49 }, + 'src/fixtures/ActionSheet/EditSectionNameSheet.fixture.tsx': { module: fixture50 }, + 'src/fixtures/ActionSheet/DeleteSheet.fixture.tsx': { module: fixture51 }, + 'src/fixtures/ActionSheet/CreateChannelSheet.fixture.tsx': { module: fixture52 }, + 'src/fixtures/ActionSheet/AttachmentSheet.fixture.tsx': { module: fixture53 }, + 'src/fixtures/ActionSheet/AddGalleryPostSheet.fixture.tsx': { module: fixture54 } }; const decorators = { diff --git a/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj b/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj index 0758dac6f3..76206dad3c 100644 --- a/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj +++ b/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj @@ -1395,7 +1395,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.0.1; + MARKETING_VERSION = 4.0.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1433,7 +1433,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.0.1; + MARKETING_VERSION = 4.0.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1657,7 +1657,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.0.1; + MARKETING_VERSION = 4.0.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1700,7 +1700,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.0.1; + MARKETING_VERSION = 4.0.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/120.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/120.png deleted file mode 100644 index c7e4c08980..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/120.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/180.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/180.png deleted file mode 100644 index 54b81b6271..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/180.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/40.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/40.png deleted file mode 100644 index 53e167140b..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/40.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/58.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index ea12cb15fe..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/58.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/60.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/60.png deleted file mode 100644 index 5421dbb358..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/60.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/80.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index 195d2a2656..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/80.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/87.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/87.png deleted file mode 100644 index 148cecadd5..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/87.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Contents.json b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Contents.json index d8507e5d58..b3f44eb295 100644 --- a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Contents.json +++ b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Contents.json @@ -1,57 +1,9 @@ { "images" : [ { - "filename" : "40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "Icon-App-iTunes.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "filename" : "Icon.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Icon-App-iTunes.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Icon-App-iTunes.png deleted file mode 100644 index f249c3e686..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Icon-App-iTunes.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Icon.png b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Icon.png new file mode 100644 index 0000000000..2773759206 Binary files /dev/null and b/apps/tlon-mobile/ios/Landscape/Images-preview.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Contents.json index 3c16f0a0fe..b3f44eb295 100644 --- a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,57 +1,9 @@ { "images" : [ { - "filename" : "Icon_40x40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "Icon_60×60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "Icon_58x58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "Icon_87×87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "Icon_80x80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "Icon_120×120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "Icon_120x120.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "Icon_180×180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "Icon-App-iTunes.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "filename" : "Icon.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon-App-iTunes.png b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon-App-iTunes.png deleted file mode 100644 index cd17434023..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon-App-iTunes.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon.png b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon.png new file mode 100644 index 0000000000..0d7bf11ebf Binary files /dev/null and b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_120x120.png b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_120x120.png deleted file mode 100644 index 1eafe686b9..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_120x120.png and /dev/null differ diff --git "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_120\303\227120.png" "b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_120\303\227120.png" deleted file mode 100644 index 158729d6c9..0000000000 Binary files "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_120\303\227120.png" and /dev/null differ diff --git "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_180\303\227180.png" "b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_180\303\227180.png" deleted file mode 100644 index 8d3788407c..0000000000 Binary files "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_180\303\227180.png" and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_40x40.png b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_40x40.png deleted file mode 100644 index 25c6f30d16..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_40x40.png and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_58x58.png b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_58x58.png deleted file mode 100644 index 6f00a485eb..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_58x58.png and /dev/null differ diff --git "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_60\303\22760.png" "b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_60\303\22760.png" deleted file mode 100644 index 7222e650e5..0000000000 Binary files "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_60\303\22760.png" and /dev/null differ diff --git a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_80x80.png b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_80x80.png deleted file mode 100644 index 3a2d27fb41..0000000000 Binary files a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_80x80.png and /dev/null differ diff --git "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_87\303\22787.png" "b/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_87\303\22787.png" deleted file mode 100644 index f7d3f647f5..0000000000 Binary files "a/apps/tlon-mobile/ios/Landscape/Images.xcassets/AppIcon.appiconset/Icon_87\303\22787.png" and /dev/null differ diff --git a/apps/tlon-mobile/src/App.main.tsx b/apps/tlon-mobile/src/App.main.tsx index 25cca6511c..a8fe41e7ae 100644 --- a/apps/tlon-mobile/src/App.main.tsx +++ b/apps/tlon-mobile/src/App.main.tsx @@ -9,20 +9,23 @@ import { NavigationContainerRefWithCurrent, useNavigationContainerRef, } from '@react-navigation/native'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; import ErrorBoundary from '@tloncorp/app/ErrorBoundary'; import { BranchProvider, useBranch } from '@tloncorp/app/contexts/branch'; import { ShipProvider, useShip } from '@tloncorp/app/contexts/ship'; import { SignupProvider } from '@tloncorp/app/contexts/signup'; import { useIsDarkMode } from '@tloncorp/app/hooks/useIsDarkMode'; -import { useScreenOptions } from '@tloncorp/app/hooks/useScreenOptions'; import { useMigrations } from '@tloncorp/app/lib/nativeDb'; import { Provider as TamaguiProvider } from '@tloncorp/app/provider'; import { FeatureFlagConnectedInstrumentationProvider } from '@tloncorp/app/utils/perf'; import { posthogAsync } from '@tloncorp/app/utils/posthog'; import { QueryClientProvider, queryClient } from '@tloncorp/shared/dist/api'; -import { LoadingSpinner, PortalProvider, Text, View } from '@tloncorp/ui'; -import { usePreloadedEmojis } from '@tloncorp/ui'; +import { + LoadingSpinner, + PortalProvider, + Text, + View, + usePreloadedEmojis, +} from '@tloncorp/ui'; import { PostHogProvider } from 'posthog-react-native'; import type { PropsWithChildren } from 'react'; import { useEffect, useState } from 'react'; @@ -30,31 +33,14 @@ import { StatusBar } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { OnboardingStack } from './OnboardingStack'; import AuthenticatedApp from './components/AuthenticatedApp'; -import { CheckVerifyScreen } from './screens/Onboarding/CheckVerifyScreen'; -import { EULAScreen } from './screens/Onboarding/EULAScreen'; -import { InventoryCheckScreen } from './screens/Onboarding/InventoryCheckScreen'; -import { InviteLinkScreen } from './screens/Onboarding/InviteLinkScreen'; -import { JoinWaitListScreen } from './screens/Onboarding/JoinWaitListScreen'; -import { RequestPhoneVerifyScreen } from './screens/Onboarding/RequestPhoneVerifyScreen'; -import { ReserveShipScreen } from './screens/Onboarding/ReserveShipScreen'; -import { ResetPasswordScreen } from './screens/Onboarding/ResetPasswordScreen'; -import { SetNicknameScreen } from './screens/Onboarding/SetNicknameScreen'; -import { SetTelemetryScreen } from './screens/Onboarding/SetTelemetryScreen'; -import { ShipLoginScreen } from './screens/Onboarding/ShipLoginScreen'; -import { SignUpEmailScreen } from './screens/Onboarding/SignUpEmailScreen'; -import { SignUpPasswordScreen } from './screens/Onboarding/SignUpPasswordScreen'; -import { TlonLoginScreen } from './screens/Onboarding/TlonLoginScreen'; -import { WelcomeScreen } from './screens/Onboarding/WelcomeScreen'; -import type { OnboardingStackParamList } from './types'; type Props = { wer?: string; channelId?: string; }; -const OnboardingStack = createNativeStackNavigator(); - // Android notification tap handler passes initial params here const App = ({ wer: notificationPath, @@ -65,12 +51,6 @@ const App = ({ const { isLoading, isAuthenticated } = useShip(); const [connected, setConnected] = useState(true); const { lure, priorityToken } = useBranch(); - const screenOptions = useScreenOptions(); - - const onboardingScreenOptions = { - ...screenOptions, - headerShown: false, - }; usePreloadedEmojis(); @@ -101,66 +81,7 @@ const App = ({ }} /> ) : ( - - - - - - - - - - - - - - - - - + ) ) : ( (); + +export function OnboardingStack() { + const screenOptions = useScreenOptions(); + + const onboardingScreenOptions = { + ...screenOptions, + headerShown: false, + }; + + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/tlon-mobile/src/components/AuthenticatedApp.tsx b/apps/tlon-mobile/src/components/AuthenticatedApp.tsx index 939b6a9b91..a31c3b2746 100644 --- a/apps/tlon-mobile/src/components/AuthenticatedApp.tsx +++ b/apps/tlon-mobile/src/components/AuthenticatedApp.tsx @@ -42,7 +42,6 @@ function AuthenticatedApp({ shipName: ship ?? '', shipUrl: shipUrl ?? '', onReset: () => sync.syncStart(), - verbose: __DEV__, onChannelReset: () => sync.handleDiscontinuity(), onChannelStatusChange: sync.handleChannelStatusChange, }); diff --git a/apps/tlon-mobile/src/fixtures/DetailView/detailViewFixtureBase.tsx b/apps/tlon-mobile/src/fixtures/DetailView/detailViewFixtureBase.tsx index 408a7a4bc1..f0deeb7638 100644 --- a/apps/tlon-mobile/src/fixtures/DetailView/detailViewFixtureBase.tsx +++ b/apps/tlon-mobile/src/fixtures/DetailView/detailViewFixtureBase.tsx @@ -39,6 +39,7 @@ export const DetailViewFixture = ({ channel, }} posts={replies} + isLoadingPosts={false} channel={channel} sendReply={async () => {}} onPressRetry={() => {}} diff --git a/apps/tlon-mobile/src/fixtures/Onboarding.fixture.tsx b/apps/tlon-mobile/src/fixtures/Onboarding.fixture.tsx new file mode 100644 index 0000000000..02922c502f --- /dev/null +++ b/apps/tlon-mobile/src/fixtures/Onboarding.fixture.tsx @@ -0,0 +1,236 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { + Context as BranchContext, + LureData, +} from '@tloncorp/app/contexts/branch'; +import { + DeepLinkData, + QueryClientProvider, + queryClient, +} from '@tloncorp/shared/dist'; +import { PropsWithChildren, useState } from 'react'; + +import { OnboardingStack, OnboardingStackNavigator } from '../OnboardingStack'; +import { OnboardingProvider } from '../lib/OnboardingContext'; +import { CheckVerifyScreen } from '../screens/Onboarding/CheckVerifyScreen'; +import { EULAScreen } from '../screens/Onboarding/EULAScreen'; +import { InventoryCheckScreen } from '../screens/Onboarding/InventoryCheckScreen'; +import { JoinWaitListScreen } from '../screens/Onboarding/JoinWaitListScreen'; +import { PasteInviteLinkScreen } from '../screens/Onboarding/PasteInviteLinkScreen'; +import { RequestPhoneVerifyScreen } from '../screens/Onboarding/RequestPhoneVerifyScreen'; +import { ReserveShipScreen } from '../screens/Onboarding/ReserveShipScreen'; +import { SetNicknameScreen } from '../screens/Onboarding/SetNicknameScreen'; +import { SetTelemetryScreen } from '../screens/Onboarding/SetTelemetryScreen'; +import { ShipLoginScreen } from '../screens/Onboarding/ShipLoginScreen'; +import { SignUpEmailScreen } from '../screens/Onboarding/SignUpEmailScreen'; +import { SignUpPasswordScreen } from '../screens/Onboarding/SignUpPasswordScreen'; +import { TlonLoginScreen } from '../screens/Onboarding/TlonLoginScreen'; +import { WelcomeScreen } from '../screens/Onboarding/WelcomeScreen'; +import { OnboardingStackParamList, User } from '../types'; +import { exampleContacts } from './contentHelpers'; +import { group } from './fakeData'; + +const sampleUser = { + id: '1', + nickname: 'test', + email: 'dan@tlon.io', + ships: [], + admin: false, + verified: false, + requirePhoneNumberVerification: false, +}; + +function OnboardingFixture({ + hasGroupInvite, + children, +}: PropsWithChildren<{ hasGroupInvite: boolean }>) { + const [lure, setLure] = useState( + hasGroupInvite + ? { + id: group.id, + shouldAutoJoin: true, + inviterUserId: exampleContacts.ed.id, + inviterNickname: exampleContacts.ed.nickname, + invitedGroupId: group.id, + invitedGroupTitle: group.title ?? undefined, + invitedGroupDescription: group.description ?? undefined, + invitedGroupIconImageUrl: group.iconImage ?? undefined, + invitedGroupiconImageColor: group.iconImageColor ?? undefined, + } + : undefined + ); + return ( + + Promise.resolve('abc'), + execRecaptchaLogin: () => Promise.resolve('abc'), + getLandscapeAuthCookie: () => Promise.resolve('abc'), + //@ts-expect-error partial implementation + hostingApi: { + signUpHostingUser: async () => Promise.resolve({}), + logInHostingUser: () => Promise.resolve(sampleUser), + getHostingAvailability: async () => + Promise.resolve({ enabled: true, validEmail: true }), + getHostingUser: async () => Promise.resolve(sampleUser as User), + getReservableShips: async () => + Promise.resolve([ + { id: '~solfer-magfed', readyForDistribution: true }, + ]), + getShipAccessCode: async () => Promise.resolve({ code: 'xyz' }), + allocateReservedShip: async () => Promise.resolve({}), + getShipsWithStatus: async () => + Promise.resolve({ + shipId: '~solfer-magfed', + status: 'Ready', + }), + reserveShip: async () => + Promise.resolve({ + id: '~solfer-magfed', + reservedBy: '1', + }), + checkPhoneVerify: async () => Promise.resolve({ verified: true }), + verifyEmailDigits: async () => Promise.resolve({ verified: true }), + requestPhoneVerify: async () => Promise.resolve({}), + }, + }} + > + void, + clearLure: () => setLure(undefined), + clearDeepLink: () => {}, + deepLinkPath: undefined, + priorityToken: undefined, + }} + > + + {children ?? } + + + + + ); +} + +function SingleScreenFixture({ + routeName, + params, + Component, +}: { + routeName: T; + params?: OnboardingStackParamList[T]; + Component: React.ComponentType; +}) { + return ( + + + + + + ); +} + +export default { + Stack: ( + + + + ), + StackWithGroupInvite: ( + + + + ), + Nickname: ( + + ), + Password: ( + + ), + JoinWaitlist: ( + + ), + RequestPhoneVerify: ( + + ), + CheckVerify: ( + + ), + ReserveShip: ( + + ), + SetNickname: ( + + ), + SetTelemetry: ( + + ), + Welcome: ( + + ), + InventoryCheck: ( + + ), + SignUpEmail: ( + + ), + EULA: , + PasteInviteLink: ( + + ), + TlonLogin: ( + + ), + ShipLogin: ( + + ), +}; diff --git a/apps/tlon-mobile/src/fixtures/PostScreen.fixture.tsx b/apps/tlon-mobile/src/fixtures/PostScreen.fixture.tsx index 6c35fe4861..9a81df5f73 100644 --- a/apps/tlon-mobile/src/fixtures/PostScreen.fixture.tsx +++ b/apps/tlon-mobile/src/fixtures/PostScreen.fixture.tsx @@ -20,6 +20,7 @@ export default ( > {}} + isLoadingPosts={false} editPost={async () => {}} onPressRetry={() => {}} onPressDelete={() => {}} diff --git a/apps/tlon-mobile/src/fixtures/SetNicknameScreen.fixture.tsx b/apps/tlon-mobile/src/fixtures/SetNicknameScreen.fixture.tsx new file mode 100644 index 0000000000..1d2ea0911f --- /dev/null +++ b/apps/tlon-mobile/src/fixtures/SetNicknameScreen.fixture.tsx @@ -0,0 +1,34 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { SetNicknameScreen } from '../screens/Onboarding/SetNicknameScreen'; +import { OnboardingStackParamList, User } from '../types'; +import { FixtureWrapper } from './FixtureWrapper'; + +const GroupMetaScreenFixture = () => { + const mockNavigation = { + navigate: () => {}, + goBack: () => {}, + addListener: () => {}, + } as unknown as NativeStackNavigationProp< + OnboardingStackParamList, + 'SetNickname' + >; + + return ( + + + + + + ); +}; + +export default GroupMetaScreenFixture; diff --git a/apps/tlon-mobile/src/fixtures/fakeData.ts b/apps/tlon-mobile/src/fixtures/fakeData.ts index 383f724db5..27c2967f98 100644 --- a/apps/tlon-mobile/src/fixtures/fakeData.ts +++ b/apps/tlon-mobile/src/fixtures/fakeData.ts @@ -571,7 +571,7 @@ export const group: db.Group = { coverImage: null, coverImageColor: '#000000', iconImage: 'https://tlon.io/local-icon.svg', - iconImageColor: '#FFFFFF', + iconImageColor: '#000000', currentUserIsMember: true, lastPostAt: null, lastPostId: null, diff --git a/apps/tlon-mobile/src/lib/OnboardingContext.tsx b/apps/tlon-mobile/src/lib/OnboardingContext.tsx new file mode 100644 index 0000000000..790d5c028c --- /dev/null +++ b/apps/tlon-mobile/src/lib/OnboardingContext.tsx @@ -0,0 +1,26 @@ +import { + RecaptchaAction, + execute, + initClient, +} from '@google-cloud/recaptcha-enterprise-react-native'; +import * as hostingApi from '@tloncorp/app/lib/hostingApi'; +import { getLandscapeAuthCookie } from '@tloncorp/shared/dist/api'; +import { createContext, useContext } from 'react'; + +interface OnboardingContextValue { + hostingApi: typeof hostingApi; + initRecaptcha: typeof initClient; + execRecaptchaLogin: () => Promise; + getLandscapeAuthCookie: typeof getLandscapeAuthCookie; +} + +export const OnboardingContext = createContext({ + initRecaptcha: initClient, + execRecaptchaLogin: () => execute(RecaptchaAction.LOGIN(), 10_000), + getLandscapeAuthCookie, + hostingApi, +}); + +export const OnboardingProvider = OnboardingContext.Provider; + +export const useOnboardingContext = () => useContext(OnboardingContext); diff --git a/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx index 963c57ccba..3079822b90 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx @@ -1,27 +1,19 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { - checkPhoneVerify, - requestPhoneVerify, - resendEmailVerification, - verifyEmailDigits, -} from '@tloncorp/app/lib/hostingApi'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; -import { formatPhoneNumber } from '@tloncorp/app/utils/string'; import { - Button, Field, ScreenHeader, - SizableText, - Text, TextInput, + TlonText, View, XStack, YStack, } from '@tloncorp/ui'; -import { createRef, useMemo, useState } from 'react'; +import { createRef, useCallback, useMemo, useState } from 'react'; import type { TextInputKeyPressEventData } from 'react-native'; import { TextInput as RNTextInput } from 'react-native'; +import { useOnboardingContext } from '../../lib/OnboardingContext'; import type { OnboardingStackParamList } from '../../types'; type Props = NativeStackScreenProps; @@ -40,77 +32,53 @@ export const CheckVerifyScreen = ({ const [code, setCode] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(); - const inputRefs = useMemo( - () => - Array.from({ length: codeLength }).map(() => createRef()), - [] - ); + const { hostingApi } = useOnboardingContext(); - const handleKeyPress = async ( - index: number, - key: TextInputKeyPressEventData['key'] - ) => { - if (key === 'Backspace' && !code[index] && index > 0) { - inputRefs[index - 1].current?.focus(); - } - }; - - const handleChangeText = (index: number, text: string) => { - const nextCode = [...code]; - if (text.length === 0) { - nextCode[index] = ''; - } else { - for (let i = 0; i < text.length; i += 1) { - nextCode[index + i] = text.charAt(i); - } - } + const handleSubmit = useCallback( + async (code: string) => { + setIsSubmitting(true); - if (nextCode.length === codeLength && nextCode.every(Boolean)) { - handleSubmit(nextCode.join('')); - } else if (index < inputRefs.length - 1 && nextCode[index]) { - for (let i = index + 1; i < inputRefs.length; i += 1) { - if (!nextCode[i]) { - inputRefs[i].current?.focus(); - break; + try { + if (isEmail) { + await hostingApi.verifyEmailDigits(user.email, code); + } else { + await hostingApi.checkPhoneVerify(user.id, code); } - } - } - - setCode(nextCode.slice(0, codeLength)); - }; - const handleSubmit = async (code: string) => { - setIsSubmitting(true); + trackOnboardingAction({ + actionName: 'Verification Submitted', + }); - try { - if (isEmail) { - await verifyEmailDigits(user.email, code); - } else { - await checkPhoneVerify(user.id, code); + navigation.navigate('ReserveShip', { user }); + } catch (err) { + console.error('Error submitting verification:', err); + if (err instanceof Error) { + setError(err.message); + trackError(err); + } } - trackOnboardingAction({ - actionName: 'Verification Submitted', - }); + setIsSubmitting(false); + }, + [hostingApi, isEmail, navigation, user] + ); - navigation.navigate('ReserveShip', { user }); - } catch (err) { - console.error('Error submitting verification:', err); - if (err instanceof Error) { - setError(err.message); - trackError(err); + const handleCodeChanged = useCallback( + (nextCode: string[]) => { + setCode(nextCode); + if (nextCode.length === codeLength && nextCode.every(Boolean)) { + handleSubmit(nextCode.join('')); } - } - - setIsSubmitting(false); - }; + }, + [codeLength, handleSubmit] + ); const handleResend = async () => { try { if (isEmail) { - await resendEmailVerification(user.id); + await hostingApi.resendEmailVerification(user.id); } else { - await requestPhoneVerify(user.id, user.phoneNumber ?? ''); + await hostingApi.requestPhoneVerify(user.id, user.phoneNumber ?? ''); } } catch (err) { console.error('Error resending verification code:', err); @@ -122,43 +90,104 @@ export const CheckVerifyScreen = ({ }; return ( - + navigation.goBack()} isLoading={isSubmitting} /> - - - We’ve sent a confirmation code to{' '} - {isEmail ? user.email : formatPhoneNumber(user.phoneNumber ?? '')}. - - - - {Array.from({ length: codeLength }).map((_, i) => ( - - handleKeyPress(i, nativeEvent.key) - } - onChangeText={(text) => handleChangeText(i, text)} - value={code.length > i ? code[i] : ''} - keyboardType="numeric" - maxLength={1} - /> - ))} - - - - Didn’t receive a code? - - + + + + Request a new code + ); }; + +function CodeInput({ + length, + value, + onChange, + error, +}: { + length: number; + value: string[]; + onChange?: (value: string[]) => void; + error?: string; +}) { + const inputRefs = useMemo( + () => Array.from({ length }).map(() => createRef()), + [length] + ); + + const handleChangeText = useCallback( + (index: number, text: string) => { + const nextCode = [...value]; + if (text.length === 0) { + nextCode[index] = ''; + } else { + for (let i = 0; i < text.length; i += 1) { + nextCode[index + i] = text.charAt(i); + } + } + if (index < inputRefs.length - 1 && nextCode[index]) { + for (let i = index + 1; i < inputRefs.length; i += 1) { + if (!nextCode[i]) { + inputRefs[i].current?.focus(); + break; + } + } + } + onChange?.(nextCode.slice(0, length)); + }, + [onChange, value, inputRefs, length] + ); + + const handleKeyPress = async ( + index: number, + key: TextInputKeyPressEventData['key'] + ) => { + if (key === 'Backspace' && !value[index] && index > 0) { + inputRefs[index - 1].current?.focus(); + } + }; + + return ( + + + {Array.from({ length }).map((_, i) => ( + handleKeyPress(i, nativeEvent.key)} + placeholder="5" + onChangeText={(text) => handleChangeText(i, text)} + value={value.length > i ? value[i] : ''} + keyboardType="numeric" + paddingHorizontal="$xl" + paddingVertical="$xl" + width="$4xl" + /> + ))} + + + ); +} diff --git a/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx index 01d237c9e8..fda895295d 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx @@ -8,7 +8,7 @@ type Props = NativeStackScreenProps; export const EULAScreen = ({ navigation }: Props) => { return ( - + ; @@ -22,19 +22,20 @@ type Props = NativeStackScreenProps; export const InventoryCheckScreen = ({ navigation }: Props) => { const signupParams = useSignupParams(); const [isChecking, setIsChecking] = useState(false); + const { hostingApi } = useOnboardingContext(); const checkAvailability = async () => { setIsChecking(true); try { - const { enabled } = await getHostingAvailability({ + const { enabled } = await hostingApi.getHostingAvailability({ lure: signupParams.lureId, priorityToken: signupParams.priorityToken, }); if (enabled) { navigation.navigate('SignUpEmail'); } else { - navigation.navigate('InviteLink'); + navigation.navigate('PasteInviteLink'); } } catch (err) { console.error('Error checking hosting availability:', err); @@ -49,7 +50,7 @@ export const InventoryCheckScreen = ({ navigation }: Props) => { return ( navigation.goBack()} isLoading={isChecking} @@ -57,11 +58,7 @@ export const InventoryCheckScreen = ({ navigation }: Props) => { - + diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx deleted file mode 100644 index 1967b29c9d..0000000000 --- a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import Clipboard from '@react-native-clipboard/clipboard'; -import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { BRANCH_DOMAIN, BRANCH_KEY } from '@tloncorp/app/constants'; -import { useBranch, useLureMetadata } from '@tloncorp/app/contexts/branch'; -import { - DeepLinkData, - createInviteLinkRegex, - extractNormalizedInviteLink, - getMetadaFromInviteLink, -} from '@tloncorp/shared/dist'; -import { - AppInviteDisplay, - Field, - PrimaryButton, - ScreenHeader, - SizableText, - TextInputWithButton, - View, - YStack, -} from '@tloncorp/ui'; -import { useCallback, useEffect, useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { Keyboard } from 'react-native'; - -import type { OnboardingStackParamList } from '../../types'; - -const INVITE_LINK_REGEX = createInviteLinkRegex(BRANCH_DOMAIN); - -type Props = NativeStackScreenProps; - -type FormData = { - inviteLink: string; -}; - -export const InviteLinkScreen = ({ navigation }: Props) => { - const lureMeta = useLureMetadata(); - const { setLure } = useBranch(); - const [hasInvite, setHasInvite] = useState(Boolean(lureMeta)); - - const { - control, - formState: { errors }, - setValue, - watch, - trigger, - } = useForm(); - - // watch for changes to the input & check for valid invite links - const inviteLinkValue = watch('inviteLink'); - useEffect(() => { - async function handleInviteLinkChange() { - const extractedLink = extractNormalizedInviteLink( - inviteLinkValue, - BRANCH_DOMAIN - ); - if (extractedLink) { - const inviteLinkMeta = await getMetadaFromInviteLink( - extractedLink, - BRANCH_KEY - ); - if (inviteLinkMeta) { - setLure(inviteLinkMeta as DeepLinkData); - return; - } - } - trigger('inviteLink'); - } - handleInviteLinkChange(); - }, [inviteLinkValue, setLure, trigger]); - - // if at any point we have invite metadata, notify & allow them to proceed - // to signup - useEffect(() => { - if (lureMeta) { - setHasInvite(true); - } - }, [lureMeta]); - - // handle paste button click - const onHandlePasteClick = useCallback(async () => { - const clipboardContents = await Clipboard.getString(); - setValue('inviteLink', clipboardContents); - }, [setValue]); - - return ( - - navigation.goBack()} - /> - Keyboard.dismiss()} - flex={1} - > - {!hasInvite ? ( - <> - - - We're growing slowly. Invites let you skip the waitlist - because we know someone wants to talk to you here. - - - Click your invite link now or paste it below. - - - ( - - - - )} - /> - - navigation.navigate('JoinWaitList', {})} - > - I don't have an invite - - - ) : ( - <> - - Invite found! - - - navigation.navigate('SignUpEmail')}> - Sign up - - - )} - - - ); -}; diff --git a/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx index 93ca184470..a3240829c7 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx @@ -4,15 +4,15 @@ import { addUserToWaitlist } from '@tloncorp/app/lib/hostingApi'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { Field, - PrimaryButton, ScreenHeader, - SizableText, TextInput, + TlonText, View, YStack, } from '@tloncorp/ui'; import { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { Alert } from 'react-native'; import type { OnboardingStackParamList } from '../../types'; @@ -36,9 +36,15 @@ export const JoinWaitListScreen = ({ navigation }: Props) => { trackOnboardingAction({ actionName: 'Waitlist Joined', }); - navigation.popToTop(); + Alert.alert('Success', 'You have been added to the waitlist.', [ + { + text: 'OK', + onPress: () => navigation.popToTop(), + }, + ]); } catch (err) { console.error('Error joining waitlist:', err); + Alert.alert('Failed', 'Unable to add you to the waitlist.'); if (err instanceof Error) { setRemoteError(err.message); trackError(err); @@ -47,18 +53,26 @@ export const JoinWaitListScreen = ({ navigation }: Props) => { }; return ( - + navigation.goBack()} + rightControls={ + + Submit + + } /> - - - We’ve given out all available accounts for today, but - we’ll have more soon. If you’d like, we can let you know - via email when they’re ready. - + + + + We’ll let you know as soon as space is available. + + { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { )} /> {remoteError ? ( - + {remoteError} - + ) : null} - - {isValid && ( - - Notify Me - - )} - ); diff --git a/apps/tlon-mobile/src/screens/Onboarding/PasteInviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/PasteInviteLinkScreen.tsx new file mode 100644 index 0000000000..e72dd8222a --- /dev/null +++ b/apps/tlon-mobile/src/screens/Onboarding/PasteInviteLinkScreen.tsx @@ -0,0 +1,180 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { + BRANCH_DOMAIN, + BRANCH_KEY, + DEFAULT_INVITE_LINK_URL, +} from '@tloncorp/app/constants'; +import { useBranch, useLureMetadata } from '@tloncorp/app/contexts/branch'; +import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; +import { + DeepLinkData, + createInviteLinkRegex, + extractNormalizedInviteLink, + getMetadaFromInviteLink, +} from '@tloncorp/shared/dist'; +import { + Field, + ScreenHeader, + TextInputWithButton, + TlonText, + View, + YStack, +} from '@tloncorp/ui'; +import { useCallback, useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Keyboard } from 'react-native'; + +import type { OnboardingStackParamList } from '../../types'; + +const INVITE_LINK_REGEX = createInviteLinkRegex(BRANCH_DOMAIN); + +type Props = NativeStackScreenProps< + OnboardingStackParamList, + 'PasteInviteLink' +>; + +type FormData = { + inviteLink: string; +}; + +export const PasteInviteLinkScreen = ({ navigation }: Props) => { + const lureMeta = useLureMetadata(); + const { setLure } = useBranch(); + + const { + control, + formState: { errors }, + setValue, + watch, + trigger, + } = useForm({ + defaultValues: { + inviteLink: DEFAULT_INVITE_LINK_URL, + }, + }); + + const [metadataError, setMetadataError] = useState(null); + + // watch for changes to the input & check for valid invite links + const inviteLinkValue = watch('inviteLink'); + useEffect(() => { + async function handleInviteLinkChange() { + const extractedLink = extractNormalizedInviteLink( + inviteLinkValue, + BRANCH_DOMAIN + ); + setMetadataError(null); + if (extractedLink) { + try { + const inviteLinkMeta = await getMetadaFromInviteLink( + extractedLink, + BRANCH_KEY + ); + if (inviteLinkMeta) { + setLure(inviteLinkMeta as DeepLinkData); + return; + } else { + throw new Error('Failed to retrieve invite metadata'); + } + } catch (e) { + trackError({ + message: e.message, + properties: { + inviteLink: extractedLink, + branchDomain: BRANCH_DOMAIN, + branchKey: BRANCH_KEY, + }, + }); + setMetadataError('Unable to load invite'); + } + } + trigger('inviteLink'); + } + handleInviteLinkChange(); + }, [inviteLinkValue, setLure, trigger]); + + // if at any point we have invite metadata, notify & allow them to proceed + // to signup + useEffect(() => { + if (lureMeta) { + trackOnboardingAction({ + actionName: 'Invite Link Added', + lure: lureMeta.id, + }); + + navigation.reset({ + index: 0, + routes: [{ name: 'Welcome' }, { name: 'SignUpEmail' }], + }); + } + }, [lureMeta, navigation]); + + // handle paste button click + const onHandlePasteClick = useCallback(async () => { + const clipboardContents = await Clipboard.getString(); + setValue('inviteLink', clipboardContents); + }, [setValue]); + + return ( + + navigation.goBack()} + rightControls={ + navigation.navigate('SignUpEmail')} + > + Next + + } + /> + Keyboard.dismiss()} + flex={1} + > + + + We're growing slowly. {'\n\n'}Invites let you skip the waitlist + because we know someone wants to talk to you here. + {'\n\n'} + Click your invite link now or paste it below. + + + ( + + + + )} + /> + + + ); +}; diff --git a/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx index 46f5bbf06b..2886f7f15e 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx @@ -4,8 +4,9 @@ import { requestPhoneVerify } from '@tloncorp/app/lib/hostingApi'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { Field, + OnboardingTextBlock, ScreenHeader, - SizableText, + TlonText, View, YStack, useTheme, @@ -75,7 +76,7 @@ export const RequestPhoneVerifyScreen = ({ }); return ( - + } /> - - - Tlon is a platform for humans. We want to make sure you’re one - too. We’ll send you a verification code to the phone number you - enter below. - - {remoteError ? ( - - {remoteError} - - ) : null} - + + + + Tlon is a platform for humans. We want to make sure you’re one + too. We’ll send you a verification code to the phone number + you enter below. + + {remoteError ? ( + + {remoteError} + + ) : null} + + + ; @@ -35,13 +25,15 @@ export const ReserveShipScreen = ({ }>({ state: 'loading', }); + const { hostingApi, getLandscapeAuthCookie } = useOnboardingContext(); const signupContext = useSignupContext(); const { setShip } = useShip(); const startShip = useCallback( async (shipIds: string[]) => { // Fetch statuses for the user's ships and start any required booting/resuming - const shipsWithStatus = await getShipsWithStatus(shipIds); + const shipsWithStatus = await hostingApi.getShipsWithStatus(shipIds); + console.log('shipsWithStatus', shipsWithStatus); if (!shipsWithStatus) { // you can only have gotten to this screen if a new hosting account was created and ship // was reserved. If we don't see the ship status, assume it's still booting @@ -56,7 +48,7 @@ export const ReserveShipScreen = ({ signupContext.telemetry === undefined ) { return navigation.navigate('SetNickname', { - user: await getHostingUser(user.id), + user: await hostingApi.getHostingUser(user.id), }); } @@ -66,7 +58,7 @@ export const ReserveShipScreen = ({ } // If it's ready, fetch the access code and auth cookie - const { code: accessCode } = await getShipAccessCode(shipId); + const { code: accessCode } = await hostingApi.getShipAccessCode(shipId); const shipUrl = getShipUrl(shipId); const authCookie = await getLandscapeAuthCookie(shipUrl, accessCode); if (!authCookie) { @@ -87,6 +79,8 @@ export const ReserveShipScreen = ({ }); }, [ + getLandscapeAuthCookie, + hostingApi, navigation, setShip, signupContext.nickname, @@ -103,7 +97,7 @@ export const ReserveShipScreen = ({ if (shipIds.length === 0) { try { // Get list of reservable ships and choose one that's ready for distribution - const ships = await getReservableShips(user.id); + const ships = await hostingApi.getReservableShips(user.id); const ship = ships.find( ({ id, readyForDistribution }) => id !== skipShipId && readyForDistribution @@ -117,13 +111,14 @@ export const ReserveShipScreen = ({ } // Reserve this ship and check it was successful - const { reservedBy } = await reserveShipApi(user.id, ship.id); + const { reservedBy } = await hostingApi.reserveShip(user.id, ship.id); + console.log('reserved', user, reservedBy); if (reservedBy !== user.id) { return reserveShip(ship.id); } // Finish allocating this ship to the user - await allocateReservedShip(user.id); + await hostingApi.allocateReservedShip(user.id); shipIds.push(ship.id); trackOnboardingAction({ actionName: 'Urbit ID Selected', diff --git a/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx index bc35e9952a..8e789ce9ec 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx @@ -4,11 +4,14 @@ import { requestNotificationToken } from '@tloncorp/app/lib/notifications'; import { trackError } from '@tloncorp/app/utils/posthog'; import { Field, + Image, ScreenHeader, - SizableText, TextInput, + TlonText, View, + XStack, YStack, + useTheme, } from '@tloncorp/ui'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -28,10 +31,16 @@ export const SetNicknameScreen = ({ params: { user }, }, }: Props) => { + const theme = useTheme(); + + const facesImage = theme.dark + ? require('../../../assets/images/faces-dark.png') + : require('../../../assets/images/faces.png'); + const { control, handleSubmit, - formState: { errors }, + formState: { errors, isValid }, setValue, } = useForm({ defaultValues: { @@ -82,29 +91,40 @@ export const SetNicknameScreen = ({ }, [setValue]); return ( - + + Next } /> - - + + + + + + Choose the nickname you want to use on the Tlon network. By default, you will use a pseudonymous identifier. - + ( - - - - We’re trying to make the app better and knowing how people use - the app really helps. - + + + Next + + } + /> + + + + We’re trying to make the app better and knowing how people use + the app really helps. + + + These stats are anonymous, for product development purposes only, + and we don’t share them with anyone. + + + - Next ); diff --git a/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx index 84d6f2093f..28b35f10e9 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx @@ -5,22 +5,20 @@ import { DEFAULT_SHIP_LOGIN_URL, } from '@tloncorp/app/constants'; import { useShip } from '@tloncorp/app/contexts/ship'; -import { isEulaAgreed, setEulaAgreed } from '@tloncorp/app/utils/eula'; import { getShipFromCookie } from '@tloncorp/app/utils/ship'; import { transformShipURL } from '@tloncorp/app/utils/string'; import { getLandscapeAuthCookie } from '@tloncorp/shared/dist/api'; import { - CheckboxInput, Field, - Icon, KeyboardAvoidingView, - ListItem, + OnboardingTextBlock, ScreenHeader, - SizableText, TextInput, + TlonText, View, YStack, } from '@tloncorp/ui'; +import { setEulaAgreed } from '@tloncorp/app/utils/eula'; import { useCallback, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -47,7 +45,6 @@ export const ShipLoginScreen = ({ navigation }: Props) => { formState: { errors, isValid }, setValue, trigger, - watch, } = useForm({ defaultValues: { shipUrl: DEFAULT_SHIP_LOGIN_URL, @@ -70,17 +67,15 @@ export const ShipLoginScreen = ({ navigation }: Props) => { return true; }, []); - const handleEula = () => { + const handlePressEula = useCallback(() => { navigation.navigate('EULA'); - }; + }, [navigation]); const onSubmit = handleSubmit(async (params) => { const { shipUrl: rawShipUrl, accessCode } = params; setIsSubmitting(true); - if (params.eulaAgreed) { - await setEulaAgreed(); - } + setEulaAgreed(); const shipUrl = transformShipURL(rawShipUrl); setFormattedShipUrl(shipUrl); @@ -91,17 +86,11 @@ export const ShipLoginScreen = ({ navigation }: Props) => { ); if (authCookie) { const shipId = getShipFromCookie(authCookie); - if (await isEulaAgreed()) { - setShip({ - ship: shipId, - shipUrl, - authCookie, - }); - } else { - setRemoteError( - 'Please agree to the End User License Agreement to continue.' - ); - } + setShip({ + ship: shipId, + shipUrl, + authCookie, + }); } else { setRemoteError( "Sorry, we couldn't log in to your ship. It may be busy or offline." @@ -129,114 +118,112 @@ export const ShipLoginScreen = ({ navigation }: Props) => { backAction={() => navigation.goBack()} isLoading={isSubmitting} rightControls={ - isValid && - watch('eulaAgreed') && ( - - Connect - - ) + + Connect + } /> - - - Connect a self-hosted ship by entering its URL and access code. - - {remoteError ? ( - {remoteError} - ) : null} - - { - const urlValidation = isValidUrl(value); - if (urlValidation === false) { - return 'Please enter a valid URL.'; - } - if (urlValidation === 'hosted') { - return 'Please log in to your hosted Tlon ship using email and password.'; - } - return true; - }, - }} - render={({ field: { onChange, onBlur, value } }) => ( - - { - onBlur(); - trigger('shipUrl'); - }} - onChangeText={onChange} - onSubmitEditing={() => setFocus('accessCode')} - value={value} - keyboardType="url" - autoCapitalize="none" - autoCorrect={false} - returnKeyType="next" - enablesReturnKeyAutomatically - /> - - )} - /> - ( - - { - onBlur(); - trigger('accessCode'); - }} - onChangeText={onChange} - onSubmitEditing={onSubmit} - value={value} - secureTextEntry - autoCapitalize="none" - autoCorrect={false} - returnKeyType="send" - enablesReturnKeyAutomatically - /> - - )} - /> - ( - + + + Connect a self-hosted ship by entering its URL and access code. + + {remoteError ? ( + + {remoteError} + + ) : null} + + + { + const urlValidation = isValidUrl(value); + if (urlValidation === false) { + return 'Please enter a valid URL.'; + } + if (urlValidation === 'hosted') { + return 'Please log in to your hosted Tlon ship using email and password.'; + } + return true; + }, + }} + render={({ field: { onChange, onBlur, value } }) => ( + + { + onBlur(); + trigger('shipUrl'); + }} + onChangeText={onChange} + onSubmitEditing={() => setFocus('accessCode')} + value={value} + keyboardType="url" + autoCapitalize="none" + autoCorrect={false} + returnKeyType="next" + enablesReturnKeyAutomatically + /> + + )} + /> + ( + + { + onBlur(); + trigger('accessCode'); + }} + onChangeText={onChange} + onSubmitEditing={onSubmit} + value={value} + secureTextEntry + autoCapitalize="none" + autoCorrect={false} + returnKeyType="send" + enablesReturnKeyAutomatically + /> + + )} + /> + + + + By logging in you agree to Tlon’s{' '} + onChange(!value)} - /> - )} - /> - - - End User License Agreement - - - - - + textDecorationLine="underline" + textDecorationDistance={10} + onPress={handlePressEula} + > + Terms of Service + + + diff --git a/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx index 7a45acf8bd..03b2689b71 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx @@ -4,14 +4,12 @@ import { useLureMetadata, useSignupParams, } from '@tloncorp/app/contexts/branch'; -import { getHostingAvailability } from '@tloncorp/app/lib/hostingApi'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { - AppInviteDisplay, Field, KeyboardAvoidingView, + OnboardingInviteBlock, ScreenHeader, - SizableText, TextInput, View, YStack, @@ -19,6 +17,7 @@ import { import { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useOnboardingContext } from '../../lib/OnboardingContext'; import type { OnboardingStackParamList } from '../../types'; type Props = NativeStackScreenProps; @@ -29,6 +28,7 @@ type FormData = { export const SignUpEmailScreen = ({ navigation, route: { params } }: Props) => { const [isSubmitting, setIsSubmitting] = useState(false); + const { hostingApi } = useOnboardingContext(); const signupParams = useSignupParams(); const lureMeta = useLureMetadata(); @@ -45,7 +45,7 @@ export const SignUpEmailScreen = ({ navigation, route: { params } }: Props) => { setIsSubmitting(true); try { - const { enabled, validEmail } = await getHostingAvailability({ + const { enabled, validEmail } = await hostingApi.getHostingAvailability({ email, lure: signupParams.lureId, priorityToken: signupParams.priorityToken, @@ -85,27 +85,21 @@ export const SignUpEmailScreen = ({ navigation, route: { params } }: Props) => { }); return ( - + navigation.goBack()} isLoading={isSubmitting} rightControls={ - isValid && ( - - Next - - ) + + Next + } /> - - {lureMeta ? : null} - - Enter your email address. You’ll use it to log in to Tlon and - we’ll email you the occasional service update. - + + {lureMeta ? : null} { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { onBlur(); trigger('email'); }} + backgroundColor={'$background'} onChangeText={onChange} onSubmitEditing={onSubmit} value={value} diff --git a/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx index 81514a199c..3df706fa2e 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx @@ -1,37 +1,28 @@ -import { - RecaptchaAction, - execute, - initClient, -} from '@google-cloud/recaptcha-enterprise-react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { RECAPTCHA_SITE_KEY } from '@tloncorp/app/constants'; -import { - useLureMetadata, - useSignupParams, -} from '@tloncorp/app/contexts/branch'; +import { useSignupParams } from '@tloncorp/app/contexts/branch'; import { useSignupContext } from '@tloncorp/app/contexts/signup'; +import { setEulaAgreed } from '@tloncorp/app/utils/eula'; +import { trackOnboardingAction } from '@tloncorp/app/utils/posthog'; +import { createDevLogger } from '@tloncorp/shared'; import { - logInHostingUser, - signUpHostingUser, -} from '@tloncorp/app/lib/hostingApi'; -import { isEulaAgreed, setEulaAgreed } from '@tloncorp/app/utils/eula'; -import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; -import { - AppInviteDisplay, - CheckboxInput, + Button, Field, - Icon, KeyboardAvoidingView, ListItem, + Modal, ScreenHeader, - SizableText, TextInput, + TlonText, View, YStack, } from '@tloncorp/ui'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useWindowDimensions } from 'react-native'; +import { getTokenValue } from 'tamagui'; +import { useOnboardingContext } from '../../lib/OnboardingContext'; import type { OnboardingStackParamList } from '../../types'; type Props = NativeStackScreenProps; @@ -42,6 +33,8 @@ type FormData = { eulaAgreed: boolean; }; +const logger = createDevLogger('SignUpPassword', true); + export const SignUpPasswordScreen = ({ navigation, route: { @@ -49,27 +42,30 @@ export const SignUpPasswordScreen = ({ }, }: Props) => { const [isSubmitting, setIsSubmitting] = useState(false); + const [recaptchaError, setRecaptchaError] = useState(null); + const [recaptchaReInitError, setRecaptchaReInitError] = + useState(null); const signupContext = useSignupContext(); const signupParams = useSignupParams(); - const lureMeta = useLureMetadata(); + const { initRecaptcha, execRecaptchaLogin, hostingApi } = + useOnboardingContext(); const { control, setFocus, handleSubmit, formState: { errors, isValid }, setError, - trigger, - watch, } = useForm({ defaultValues: { eulaAgreed: false, }, - mode: 'onChange', + mode: 'onBlur', }); + const { height } = useWindowDimensions(); - const handleEula = () => { + const handlePressEula = useCallback(() => { navigation.navigate('EULA'); - }; + }, [navigation]); const onSubmit = handleSubmit(async (params) => { const { password } = params; @@ -77,38 +73,26 @@ export const SignUpPasswordScreen = ({ let recaptchaToken: string | undefined; try { - recaptchaToken = await execute(RecaptchaAction.LOGIN(), 10_000); + recaptchaToken = await execRecaptchaLogin(); } catch (err) { console.error('Error executing reCAPTCHA:', err); if (err instanceof Error) { - setError('password', { - type: 'custom', - message: err.message, + setRecaptchaError(err); + logger.trackError('Error executing reCAPTCHA', { + thrownErrorMessage: err.message, }); - trackError(err); } } - if (params.eulaAgreed) { - await setEulaAgreed(); - } + await setEulaAgreed(); - if (!recaptchaToken) { - setIsSubmitting(false); - return; - } - - if (!isEulaAgreed()) { - setError('eulaAgreed', { - type: 'custom', - message: 'Please agree to the End User License Agreement to continue.', - }); + if (!recaptchaToken || recaptchaError || recaptchaReInitError) { setIsSubmitting(false); return; } try { - await signUpHostingUser({ + await hostingApi.signUpHostingUser({ email, password, recaptchaToken, @@ -123,7 +107,9 @@ export const SignUpPasswordScreen = ({ type: 'custom', message: err.message, }); - trackError(err); + logger.trackError('Error signing up user', { + thrownErrorMessage: err.message, + }); } setIsSubmitting(false); return; @@ -136,7 +122,7 @@ export const SignUpPasswordScreen = ({ }); try { - const user = await logInHostingUser({ + const user = await hostingApi.logInHostingUser({ email, password, }); @@ -152,7 +138,9 @@ export const SignUpPasswordScreen = ({ type: 'custom', message: err.message, }); - trackError(err); + logger.trackError('Error logging in user', { + thrownErrorMessage: err.message, + }); } } @@ -163,128 +151,163 @@ export const SignUpPasswordScreen = ({ useEffect(() => { (async () => { try { - await initClient(RECAPTCHA_SITE_KEY, 10_000); + await initRecaptcha(RECAPTCHA_SITE_KEY, 10_000); } catch (err) { console.error('Error initializing reCAPTCHA client:', err); if (err instanceof Error) { - setError('password', { - type: 'custom', - message: err.message, + setRecaptchaError(err); + logger.trackError('Error initializing reCAPTCHA client', { + thrownErrorMessage: err.message, + siteKey: RECAPTCHA_SITE_KEY, }); - trackError(err); } } })(); }, []); + // Re-initialize reCAPTCHA client if an error occurred + useEffect(() => { + if (recaptchaError && !recaptchaReInitError) { + (async () => { + try { + await initRecaptcha(RECAPTCHA_SITE_KEY, 10_000); + setRecaptchaError(null); + await onSubmit(); + } catch (err) { + console.error('Error re-initializing reCAPTCHA client:', err); + if (err instanceof Error) { + logger.trackError('Error re-initializing reCAPTCHA client', { + thrownErrorMessage: err.message, + siteKey: RECAPTCHA_SITE_KEY, + }); + setRecaptchaReInitError(err); + } + } + })(); + } + }, [recaptchaError]); + return ( - + navigation.goBack()} isLoading={isSubmitting} rightControls={ - isValid && - watch('eulaAgreed') && ( - - Next - - ) + + Next + } /> - - {lureMeta ? : null} - - Please set a strong password with at least 8 characters. - - ( - - { - onBlur(); - trigger('password'); - }} - onChangeText={onChange} - onSubmitEditing={() => setFocus('confirmPassword')} - value={value} - secureTextEntry - autoCapitalize="none" - autoCorrect={false} - returnKeyType="next" - enablesReturnKeyAutomatically - /> - - )} - /> - - value === password || 'Passwords must match.', - }} - render={({ field: { onChange, onBlur, value } }) => ( - - { - onBlur(); - trigger('confirmPassword'); - }} - onChangeText={onChange} - onSubmitEditing={onSubmit} - value={value} - secureTextEntry - autoCapitalize="none" - autoCorrect={false} - returnKeyType="send" - enablesReturnKeyAutomatically - /> - - )} - /> - ( - + {/* {lureMeta ? : null} */} + + + Please set a strong password with at least 8 characters. + + + + ( + + setFocus('confirmPassword')} + value={value} + secureTextEntry + autoCapitalize="none" + autoCorrect={false} + returnKeyType="next" + enablesReturnKeyAutomatically + /> + + )} + /> + + value === password || 'Passwords must match.', + }} + render={({ field: { onChange, onBlur, value, ref } }) => ( + + + + )} + /> + + + + By registering you agree to Tlon’s{' '} + onChange(!value)} - /> - )} - /> - - - End User License Agreement - - - - - + textDecorationLine="underline" + textDecorationDistance={10} + onPress={handlePressEula} + > + Terms of Service + + + + + + + + We encountered an error reaching Google's reCAPTCHA service. + + + This may be due to a network issue or a problem with the service + itself. + + + A retry may resolve the issue. If the problem persists, please + contact support. + + + + + ); }; diff --git a/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx index 56aa9e3b3e..aee9c307f1 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx @@ -15,18 +15,16 @@ import { isEulaAgreed, setEulaAgreed } from '@tloncorp/app/utils/eula'; import { getShipUrl } from '@tloncorp/app/utils/ship'; import { getLandscapeAuthCookie } from '@tloncorp/shared/dist/api'; import { - CheckboxInput, Field, - Icon, KeyboardAvoidingView, - ListItem, + OnboardingTextBlock, ScreenHeader, - SizableText, TextInput, + TlonText, View, YStack, } from '@tloncorp/ui'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import type { OnboardingStackParamList } from '../../types'; @@ -65,16 +63,14 @@ export const TlonLoginScreen = ({ navigation }: Props) => { navigation.navigate('ResetPassword', { email }); }; - const handleEula = () => { + const handlePressEula = useCallback(() => { navigation.navigate('EULA'); - }; + }, [navigation]); const onSubmit = handleSubmit(async (params) => { setIsSubmitting(true); - if (params.eulaAgreed) { - await setEulaAgreed(); - } + await setEulaAgreed(); try { const user = await logInHostingUser(params); @@ -148,117 +144,112 @@ export const TlonLoginScreen = ({ navigation }: Props) => { return ( navigation.goBack()} isLoading={isSubmitting} rightControls={ - isValid && - watch('eulaAgreed') && ( - - Connect - - ) + + Submit + } /> - - - Enter the email and password associated with your Tlon account. - - {remoteError ? ( - {remoteError} - ) : null} + + + + Enter the email and password associated with your Tlon account. + + + {remoteError} + + - ( - - { - onBlur(); - trigger('email'); - }} - onChangeText={onChange} - onSubmitEditing={() => setFocus('password')} - value={value} - keyboardType="email-address" - autoCapitalize="none" - autoCorrect={false} - returnKeyType="next" - enablesReturnKeyAutomatically - /> - - )} - name="email" - /> - ( - - { - onBlur(); - trigger('password'); - }} - onChangeText={onChange} - onSubmitEditing={onSubmit} - value={value} - secureTextEntry - autoCapitalize="none" - autoCorrect={false} - returnKeyType="send" - enablesReturnKeyAutomatically - /> - - )} - name="password" - /> - ( - onChange(!value)} - /> - )} - /> - - - - End User License Agreement - - - - - - - - Forgot password? - - - - - + + ( + + { + onBlur(); + trigger('email'); + }} + onChangeText={onChange} + onSubmitEditing={() => setFocus('password')} + value={value} + keyboardType="email-address" + autoCapitalize="none" + autoCorrect={false} + returnKeyType="next" + enablesReturnKeyAutomatically + /> + + )} + name="email" + /> + ( + + { + onBlur(); + trigger('password'); + }} + onChangeText={onChange} + onSubmitEditing={onSubmit} + value={value} + secureTextEntry + autoCapitalize="none" + autoCorrect={false} + returnKeyType="send" + enablesReturnKeyAutomatically + /> + + )} + name="password" + /> + + Forgot password? + + + + By logging in you agree to Tlon’s{' '} + + Terms of Service + + + diff --git a/apps/tlon-mobile/src/screens/Onboarding/WelcomeScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/WelcomeScreen.tsx index bd1d885778..cdee8e26b4 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/WelcomeScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/WelcomeScreen.tsx @@ -1,68 +1,114 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { useLureMetadata } from '@tloncorp/app/contexts/branch'; -import { useIsDarkMode } from '@tloncorp/app/hooks/useIsDarkMode'; +import { setDidShowBenefitsSheet } from '@tloncorp/shared/dist/db'; +import { useDidShowBenefitsSheet } from '@tloncorp/shared/dist/store'; import { ActionSheet, - AppInviteDisplay, - PrimaryButton, + Button, + Image, + OnboardingButton, + OnboardingInviteBlock, SizableText, + TlonText, View, + XStack, YStack, } from '@tloncorp/ui'; -import { useState } from 'react'; -import { ImageBackground, Pressable } from 'react-native'; +import { OnboardingBenefitsSheet } from '@tloncorp/ui/src/components/Onboarding/OnboardingBenefitsSheet'; +import { useCallback, useState } from 'react'; +import { Pressable } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import type { OnboardingStackParamList } from '../../types'; +export const Text = TlonText.Text; + type Props = NativeStackScreenProps; export const WelcomeScreen = ({ navigation }: Props) => { const lureMeta = useLureMetadata(); - const isDarkMode = useIsDarkMode(); - const { bottom } = useSafeAreaInsets(); + const { bottom, top } = useSafeAreaInsets(); const [open, setOpen] = useState(false); + const { data: didShowBenefitsSheet } = useDidShowBenefitsSheet(); + + const handleBenefitsSheetOpenChange = useCallback((open: boolean) => { + if (!open) { + setTimeout(() => { + setDidShowBenefitsSheet(true); + }, 1000); + } + setOpen(open); + }, []); - const bgSource = isDarkMode - ? require('../../../assets/images/welcome-bg-dark.png') - : require('../../../assets/images/welcome-bg.png'); + const handlePressInvite = useCallback(() => { + navigation.navigate('SignUpEmail'); + }, [navigation]); return ( - - + + + Tlon Messenger + + - {lureMeta ? ( - - ) : null} - - { - navigation.navigate('InventoryCheck'); - }} - > - Sign Up with Email - - setOpen(true)}> - - Have an account? Log in - - + + + {lureMeta ? ( + + + + Join with new account + + + ) : ( + <> + { + navigation.navigate('PasteInviteLink'); + }} + > + Claim invite + + { + navigation.navigate('JoinWaitList', {}); + }} + > + Join waitlist + + + )} + + + setOpen(true)}> + + Have an account? Log in + + + - + @@ -87,6 +133,10 @@ export const WelcomeScreen = ({ navigation }: Props) => { + ); }; diff --git a/apps/tlon-mobile/src/types.ts b/apps/tlon-mobile/src/types.ts index b339b177ce..542b970958 100644 --- a/apps/tlon-mobile/src/types.ts +++ b/apps/tlon-mobile/src/types.ts @@ -101,7 +101,7 @@ export type OnboardingStackParamList = { SignUpEmail: undefined; EULA: undefined; SignUpPassword: { email: string }; - InviteLink: undefined; + PasteInviteLink: undefined; JoinWaitList: { email?: string }; RequestPhoneVerify: { user: User }; CheckVerify: { user: User }; diff --git a/apps/tlon-web/package.json b/apps/tlon-web/package.json index f0417db966..03efcbc7a8 100644 --- a/apps/tlon-web/package.json +++ b/apps/tlon-web/package.json @@ -53,7 +53,7 @@ "@radix-ui/react-toggle": "^1.0.2", "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.0", - "@tamagui/vite-plugin": "~1.112.12", + "@tamagui/vite-plugin": "~1.101.3", "@tanstack/react-query": "^4.28.0", "@tanstack/react-query-devtools": "^4.28.0", "@tanstack/react-query-persist-client": "^4.28.0", @@ -84,10 +84,10 @@ "@tiptap/pm": "^2.6.6", "@tiptap/react": "^2.6.6", "@tiptap/suggestion": "^2.6.6", + "@tloncorp/app": "workspace:*", "@tloncorp/mock-http-api": "^1.2.0", "@tloncorp/shared": "workspace:*", "@tloncorp/ui": "workspace:*", - "@tloncorp/app": "workspace:*", "@types/marked": "^4.3.0", "@urbit/api": "^2.2.0", "@urbit/aura": "^1.0.0", diff --git a/apps/tlon-web/src/logic/branch.ts b/apps/tlon-web/src/logic/branch.ts index 4f8a6b146d..581939572f 100644 --- a/apps/tlon-web/src/logic/branch.ts +++ b/apps/tlon-web/src/logic/branch.ts @@ -23,7 +23,24 @@ export const getDeepLink = async (alias: string) => { }; export type DeepLinkType = 'lure' | 'wer'; -interface DeepLinkData { +export interface DeepLinkMetadata { + $og_title?: string; + $og_description?: string; + $og_image_url?: string; + $twitter_title?: string; + $twitter_description?: string; + $twitter_image_url?: string; + $twitter_card?: string; + inviterUserId?: string; + inviterNickname?: string; + inviterAvatarImage?: string; + invitedGroupId?: string; + invitedGroupTitle?: string; + invitedGroupDescription?: string; + invitedGroupIconImageUrl?: string; + invitedGroupiconImageColor?: string; +} +export interface DeepLinkData extends DeepLinkMetadata { $desktop_url: string; $canonical_url: string; lure?: string; @@ -40,7 +57,8 @@ export async function getDmLink(): Promise { export const createDeepLink = async ( fallbackUrl: string | undefined, type: DeepLinkType, - path: string + path: string, + metadata?: DeepLinkMetadata ) => { if (!fallbackUrl || !path) { return undefined; @@ -64,6 +82,7 @@ export const createDeepLink = async ( const token = parsedURL.pathname.split('/').pop(); const alias = token || path.replace('~', '').replace('/', '-'); const data: DeepLinkData = { + ...(metadata || {}), $desktop_url: fallbackUrl, $canonical_url: fallbackUrl, }; @@ -73,7 +92,7 @@ export const createDeepLink = async ( data.wer = path; } - let url = await getDeepLink(alias).catch(() => fallbackUrl); + let url = await getDeepLink(alias).catch(() => null); if (!url) { console.log(`No existing deeplink for ${alias}, creating new one`); const response = await fetchBranchApi('/v1/url', { diff --git a/apps/tlon-web/src/state/lure/lure.ts b/apps/tlon-web/src/state/lure/lure.ts index 9c4e36bf32..3141e79241 100644 --- a/apps/tlon-web/src/state/lure/lure.ts +++ b/apps/tlon-web/src/state/lure/lure.ts @@ -1,12 +1,13 @@ import { useQuery } from '@tanstack/react-query'; import { GroupMeta } from '@tloncorp/shared/dist/urbit/groups'; import produce from 'immer'; +import { Contact } from 'packages/shared/dist/urbit'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import create from 'zustand'; import { persist } from 'zustand/middleware'; import api from '@/api'; -import { createDeepLink } from '@/logic/branch'; +import { DeepLinkMetadata, createDeepLink } from '@/logic/branch'; import { getPreviewTracker } from '@/logic/subscriptionTracking'; import { asyncWithDefault, @@ -18,6 +19,7 @@ import { stringToTa, } from '@/logic/utils'; +import { useContact } from '../contact'; import { useGroup } from '../groups'; import { useLocalState } from '../local'; @@ -44,9 +46,17 @@ type Lures = Record; interface LureState { bait: Bait | null; lures: Lures; - fetchLure: (flag: string, fetchIfData?: boolean) => Promise; - describe: (flag: string, metadata: LureMetadata) => Promise; - toggle: (flag: string, metadata: GroupMeta) => Promise; + fetchLure: (flag: string, linkMetadata: DeepLinkMetadata) => Promise; + describe: ( + flag: string, + lureMetadata: LureMetadata, + linkMetadata: DeepLinkMetadata + ) => Promise; + toggle: ( + flag: string, + lureMetadata: LureMetadata, + linkMetadata: DeepLinkMetadata + ) => Promise; start: () => Promise; } @@ -69,19 +79,19 @@ export const useLureState = create( (set, get) => ({ bait: null, lures: {}, - describe: async (flag, metadata) => { + describe: async (flag, lureMetadata, linkMetadata) => { await api.poke({ app: 'reel', mark: 'reel-describe', json: { token: flag, - metadata, + metadata: lureMetadata, }, }); - return get().fetchLure(flag); + return get().fetchLure(flag, linkMetadata); }, - toggle: async (flag, meta) => { + toggle: async (flag, lureMetadata, linkMetadata) => { const { name } = getFlagParts(flag); const lure = get().lures[flag]; const enabled = !lure?.enabled; @@ -94,7 +104,7 @@ export const useLureState = create( }, }); } else { - get().describe(flag, groupsDescribe(meta)); + get().describe(flag, lureMetadata, linkMetadata); } set( @@ -112,7 +122,7 @@ export const useLureState = create( json: name, }); - return get().fetchLure(flag); + return get().fetchLure(flag, linkMetadata); }, start: async () => { const bait = await api.scry({ @@ -126,7 +136,7 @@ export const useLureState = create( }) ); }, - fetchLure: async (flag) => { + fetchLure: async (flag, linkMetadata) => { const prevLure = get().lures[flag]; const [enabled, url, metadata] = await Promise.all([ // enabled @@ -176,7 +186,7 @@ export const useLureState = create( let deepLinkUrl: string | undefined; if (enabled && url) { - deepLinkUrl = await createDeepLink(url, 'lure', flag); + deepLinkUrl = await createDeepLink(url, 'lure', flag, linkMetadata); } set( @@ -200,6 +210,44 @@ export const useLureState = create( ) ); +function getLureMetadata(flag: string, meta: GroupMeta, profile: Contact) { + const title = `Join ${meta.title || flag}`; + const description = meta.description || ''; + const image = meta.cover || meta.image || undefined; + const iconIsColor = meta.image ? meta.image.startsWith('#') : false; + + return { + $og_title: title, + $og_description: description, + $og_image_url: image, + $twitter_title: title, + $twitter_description: description, + $twitter_image_url: image, + $twitter_card: meta.cover + ? 'summary_large_image' + : meta.image + ? 'summary' + : undefined, + inviterUserId: window.our, + inviterNickname: profile.nickname || undefined, + inviterAvatarImage: profile.avatar || undefined, + invitedGroupId: flag, + invitedGroupTitle: title, + invitedGroupDescription: title, + invitedGroupIconImageUrl: + meta.image && !iconIsColor ? meta.image : undefined, + invitedGroupiconImageColor: + meta.image && iconIsColor ? meta.image : undefined, + }; +} + +const emptyMeta = { + title: '', + description: '', + image: '', + cover: '', +}; + const selLure = (flag: string) => (s: LureState) => ({ lure: s.lures[flag] || { fetched: false, url: '' }, bait: s.bait, @@ -208,31 +256,39 @@ const { shouldLoad, newAttempt, finished } = getPreviewTracker(30 * 1000); export function useLure(flag: string, disableLoading = false) { const { bait, lure } = useLureState(selLure(flag)); const group = useGroup(flag); + const contact = useContact(window.our); + const linkMetadata = useMemo(() => { + return getLureMetadata(flag, group?.meta || emptyMeta, contact); + }, [group, contact]); useEffect(() => { - if (!bait || disableLoading || !shouldLoad(flag)) { + if (!bait || disableLoading || !shouldLoad(flag) || !group) { return; } newAttempt(flag); useLureState .getState() - .fetchLure(flag) + .fetchLure(flag, linkMetadata) .finally(() => finished(flag)); - }, [bait, flag, disableLoading]); + }, [bait, group, linkMetadata, flag, disableLoading]); const toggle = useCallback( (meta: GroupMeta) => async () => { - return useLureState.getState().toggle(flag, meta); + return useLureState + .getState() + .toggle(flag, groupsDescribe(meta), linkMetadata); }, - [flag] + [flag, linkMetadata] ); const describe = useCallback( (meta: GroupMeta) => { - return useLureState.getState().describe(flag, groupsDescribe(meta)); + return useLureState + .getState() + .describe(flag, groupsDescribe(meta), linkMetadata); }, - [flag] + [flag, linkMetadata] ); useEffect(() => { diff --git a/desk/app/chat.hoon b/desk/app/chat.hoon index ad52669097..db609efccc 100644 --- a/desk/app/chat.hoon +++ b/desk/app/chat.hoon @@ -1748,6 +1748,7 @@ :: ++ di-ingest-diff |= =diff:dm:c + =? net.dm &(?=(%inviting net.dm) !from-self) %done =/ =wire /contacts/(scot %p ship) =/ =cage [act:mar:contacts !>(`action:contacts`[%heed ~[ship]])] =. cor (emit %pass wire %agent [our.bowl %contacts] %poke cage) diff --git a/desk/app/groups.hoon b/desk/app/groups.hoon index 47071086be..6d91854ceb 100644 --- a/desk/app/groups.hoon +++ b/desk/app/groups.hoon @@ -2039,7 +2039,8 @@ =/ =action:g [flag now.bowl %cordon %shut %del-ships %ask ships] (poke-host /rescind act:mar:g !>(action)) ++ get-preview - =/ =wire (welp ga-area /preview) + |= invite=? + =/ =wire (welp ga-area ?:(invite /preview/invite /preview)) =/ =dock [p.flag dap.bowl] =/ =path /groups/(scot %p p.flag)/[q.flag]/preview =/ watch [%pass wire %agent dock %watch path] @@ -2075,23 +2076,23 @@ ++ ga-watch |= =(pole knot) ^+ ga-core - =. cor get-preview:ga-pass + =. cor (get-preview:ga-pass |) ga-core :: ++ ga-give-update (give %fact ~[/gangs/updates] gangs+!>((~(put by xeno) flag gang))) ++ ga-agent - |= [=wire =sign:agent:gall] + |= [=(pole knot) =sign:agent:gall] ^+ ga-core - ?+ wire ~|(bad-agent-take/wire !!) + ?+ pole ~|(bad-agent-take/pole !!) [%invite ~] ?> ?=(%poke-ack -.sign) :: ?~ p.sign ga-core :: %- (slog leaf/"Failed to invite {}" u.p.sign) ga-core :: - [%preview ~] - ?+ -.sign ~|(weird-take/[wire -.sign] !!) + [%preview inv=?(~ [%invite ~])] + ?+ -.sign ~|(weird-take/[pole -.sign] !!) %kick ga-core :: kick for single response sub, just take it %watch-ack ?~ p.sign ga-core :: TODO: report retreival failure @@ -2111,19 +2112,10 @@ ?: from-self ga-core ?~ pev.gang ga-core ?~ vit.gang ga-core - =/ link /find - =/ =new-yarn:ha - %- spin - :* [`flag ~ q.byk.bowl /(scot %p p.flag)/[q.flag]/invite] - link - `['Join Group' link] - :~ [%ship src.bowl] - ' sent you an invite to ' - [%emph title.meta.u.pev.gang] - == - == - =? cor !(~(has by groups) flag) - (emit (pass-hark new-yarn)) + :: only send invites if this came from ga-invite and we + :: aren't already in the group somehow + ?~ inv.pole ga-core + ?: (~(has by groups) flag) ga-core (ga-activity %group-invite src.bowl) :: == @@ -2176,6 +2168,9 @@ ++ ga-invite |= =invite:g ^+ ga-core + :: prevent spamming invites + ?. =(~ vit.gang) ga-core + ?: (~(has by groups) p.invite) ga-core %- (log |.("received invite: {}")) ?: &(?=(^ cam.gang) ?=(%knocking progress.u.cam.gang)) %- (log |.("was knocking: {}")) @@ -2183,7 +2178,7 @@ ?> =(p.flag src.bowl) (ga-start-join join-all.u.cam.gang) =. vit.gang `invite - =. cor get-preview:ga-pass + =. cor (get-preview:ga-pass &) =. cor ga-give-update ga-core :: diff --git a/desk/app/notify.hoon b/desk/app/notify.hoon index cdb647d363..a1b49023ec 100644 --- a/desk/app/notify.hoon +++ b/desk/app/notify.hoon @@ -165,10 +165,12 @@ :: ++ on-init :_ this - :~ (~(watch-our pass:io /activity) %activity /notifications) + :* (~(watch-our pass:io /activity) %activity /notifications) (~(wait pass:io /clear) (add now.bowl clear-interval)) - [%pass / %agent [our.bowl %notify] %poke %provider-state-message !>(0)] [%pass /eyre %arvo %e %connect [~ /apps/groups/~/notify] dap.bowl] + :: + ?. =(~rivfur-livmet our.bowl) ~ + [%pass / %agent [our.bowl %notify] %poke %provider-state-message !>(0)]~ == :: ++ on-save !>(state) @@ -181,11 +183,11 @@ [%pass /eyre %arvo %e %connect [~ /apps/groups/~/notify] dap.bowl]~ =/ migrated (migrate-state old-state) :_ this(state migrated) - :- [%pass / %agent [our.bowl %notify] %poke %provider-state-message !>(0)] ?: (~(has by wex.bowl) [/activity our.bowl %activity]) caz - :_ caz - [(~(watch-our pass:io /activity) %activity /notifications)] + :- (~(watch-our pass:io /activity) %activity /notifications) + ?. =(~rivfur-livmet our.bowl) caz + [[%pass / %agent [our.bowl %notify] %poke %provider-state-message !>(0)] caz] :: ++ on-poke |= [=mark =vase] diff --git a/desk/desk.docket-0 b/desk/desk.docket-0 index d4843cc0f0..2ad732bf4c 100644 --- a/desk/desk.docket-0 +++ b/desk/desk.docket-0 @@ -2,9 +2,9 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v6.slci9.jv3n5.rrkgt.a6plj.jnc26.glob' 0v6.slci9.jv3n5.rrkgt.a6plj.jnc26] + glob-http+['https://bootstrap.urbit.org/glob-0v5.ensqi.86b0e.93jfb.65ah6.l4h30.glob' 0v5.ensqi.86b0e.93jfb.65ah6.l4h30] base+'groups' - version+[6 4 1] + version+[6 4 2] website+'https://tlon.io' license+'MIT' == diff --git a/packages/app/.eslintrc.cjs b/packages/app/.eslintrc.cjs new file mode 100644 index 0000000000..25a6334fc8 --- /dev/null +++ b/packages/app/.eslintrc.cjs @@ -0,0 +1,8 @@ +module.exports = { + rules: { + 'no-restricted-imports': [ + 'error', + { patterns: ['tlon-mobile', 'tlon-web'] }, + ], + }, +}; diff --git a/packages/app/constants.ts b/packages/app/constants.ts index 752d7eeadd..6edcd1b889 100644 --- a/packages/app/constants.ts +++ b/packages/app/constants.ts @@ -29,6 +29,7 @@ export const DEFAULT_LURE = extra.defaultLure ?? '~nibset-napwyn/tlon'; export const DEFAULT_PRIORITY_TOKEN = extra.defaultPriorityToken ?? 'mobile'; export const DEFAULT_TLON_LOGIN_EMAIL = extra.defaultTlonLoginEmail ?? ''; export const DEFAULT_TLON_LOGIN_PASSWORD = extra.defaultTlonLoginPassword ?? ''; +export const DEFAULT_INVITE_LINK_URL = extra.defaultInviteLinkUrl ?? ''; export const DEFAULT_SHIP_LOGIN_URL = extra.defaultShipLoginUrl ?? ''; export const DEFAULT_SHIP_LOGIN_ACCESS_CODE = extra.defaultShipLoginAccessCode ?? ''; diff --git a/packages/app/contexts/branch.tsx b/packages/app/contexts/branch.tsx index 9b49c63680..b77e49c902 100644 --- a/packages/app/contexts/branch.tsx +++ b/packages/app/contexts/branch.tsx @@ -15,7 +15,7 @@ import storage from '../lib/storage'; import { getPathFromWer } from '../utils/string'; import { useShip } from './ship'; -interface LureData extends DeepLinkMetadata { +export interface LureData extends DeepLinkMetadata { id: string; shouldAutoJoin: boolean; } @@ -61,7 +61,7 @@ const getSavedLure = async () => { const clearSavedLure = async () => storage.remove({ key: STORAGE_KEY }); -const Context = createContext({} as ContextValue); +export const Context = createContext({} as ContextValue); export const useBranch = () => { const context = useContext(Context); diff --git a/packages/app/features/groups/GroupMembersScreen.tsx b/packages/app/features/groups/GroupMembersScreen.tsx index cda6db9464..e63aecab14 100644 --- a/packages/app/features/groups/GroupMembersScreen.tsx +++ b/packages/app/features/groups/GroupMembersScreen.tsx @@ -1,18 +1,20 @@ +import { CommonActions } from '@react-navigation/native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import * as store from '@tloncorp/shared/dist/store'; import { GroupMembersScreenView } from '@tloncorp/ui'; +import { useCallback } from 'react'; import { useCurrentUserId } from '../../hooks/useCurrentUser'; import { useGroupContext } from '../../hooks/useGroupContext'; -import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { GroupSettingsStackParamList } from '../../navigation/types'; - type Props = NativeStackScreenProps< GroupSettingsStackParamList, 'GroupMembers' >; -export function GroupMembersScreen(props: Props) { - const { groupId } = props.route.params; +export function GroupMembersScreen({ route, navigation }: Props) { + const { groupId } = route.params; const { groupMembers, groupRoles, @@ -30,9 +32,28 @@ export function GroupMembersScreen(props: Props) { const currentUserId = useCurrentUserId(); + const handleGoToDm = useCallback( + async (participants: string[]) => { + const dmChannel = await store.upsertDmChannel({ + participants, + }); + navigation.dispatch( + CommonActions.reset({ + index: 1, + routes: [ + { name: 'ChatList' }, + { name: 'Channel', params: { channel: dmChannel } }, + ], + }) + ); + }, + [navigation] + ); + return ( navigation.goBack()} + onPressGoToDm={(contactId: string) => handleGoToDm([contactId])} members={groupMembers} roles={groupRoles} currentUserId={currentUserId} diff --git a/packages/app/features/groups/GroupMetaScreen.tsx b/packages/app/features/groups/GroupMetaScreen.tsx index a68289eb58..e6c0d6e6ec 100644 --- a/packages/app/features/groups/GroupMetaScreen.tsx +++ b/packages/app/features/groups/GroupMetaScreen.tsx @@ -14,7 +14,12 @@ import { BRANCH_DOMAIN, BRANCH_KEY } from '../../constants'; import { useGroupContext } from '../../hooks/useGroupContext'; import { GroupSettingsStackParamList } from '../../navigation/types'; -type Props = NativeStackScreenProps; +type Props = NativeStackScreenProps< + GroupSettingsStackParamList, + 'GroupMeta' +> & { + navigateToHome: () => void; +}; export function GroupMetaScreen(props: Props) { const { groupId } = props.route.params; @@ -49,6 +54,11 @@ export function GroupMetaScreen(props: Props) { setShowDeleteSheet(true); }, []); + const handleDeleteGroup = useCallback(() => { + deleteGroup(); + props.navigateToHome(); + }, [deleteGroup, props]); + return ( diff --git a/packages/app/features/settings/EditProfileScreen.tsx b/packages/app/features/settings/EditProfileScreen.tsx index a472b474d3..804af84fa9 100644 --- a/packages/app/features/settings/EditProfileScreen.tsx +++ b/packages/app/features/settings/EditProfileScreen.tsx @@ -1,5 +1,5 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack'; import * as api from '@tloncorp/shared/dist/api'; -import * as db from '@tloncorp/shared/dist/db'; import * as store from '@tloncorp/shared/dist/store'; import { AttachmentProvider, @@ -7,8 +7,6 @@ import { GroupsProvider, } from '@tloncorp/ui'; import { useCallback } from 'react'; -import { NativeStackScreenProps } from '@react-navigation/native-stack'; - import { RootStackParamList } from '../../navigation/types'; @@ -18,21 +16,23 @@ export function EditProfileScreen({ navigation }: Props) { const { data: groups } = store.useGroups({ includeUnjoined: true }); const onSaveProfile = useCallback( - (update: { - profile: api.ProfileUpdate | null; - pinnedGroups?: db.Group[] | null; - }) => { - if (update.profile) { - store.updateCurrentUserProfile(update.profile); - } - if (update.pinnedGroups) { - store.updateProfilePinnedGroups(update.pinnedGroups); + (update: api.ProfileUpdate | null) => { + if (update) { + store.updateCurrentUserProfile(update); } navigation.goBack(); }, [navigation] ); + const onUpdateCoverImage = useCallback((coverImage: string) => { + store.updateCurrentUserProfile({ coverImage }); + }, []); + + const onUpdateAvatarImage = useCallback((avatarImage: string) => { + store.updateCurrentUserProfile({ avatarImage }); + }, []); + const canUpload = store.useCanUpload(); return ( @@ -41,6 +41,9 @@ export function EditProfileScreen({ navigation }: Props) { navigation.goBack()} onSaveProfile={onSaveProfile} + onUpdatePinnedGroups={store.updateProfilePinnedGroups} + onUpdateCoverImage={onUpdateCoverImage} + onUpdateAvatarImage={onUpdateAvatarImage} /> diff --git a/packages/app/features/top/ChannelSearchScreen.tsx b/packages/app/features/top/ChannelSearchScreen.tsx index e9039c5c20..4d62e81bdb 100644 --- a/packages/app/features/top/ChannelSearchScreen.tsx +++ b/packages/app/features/top/ChannelSearchScreen.tsx @@ -43,6 +43,7 @@ export default function ChannelSearchScreen(props: Props) { - + + + {buttonText} + + + ); }); diff --git a/packages/ui/src/components/GalleryPost/GalleryPost.tsx b/packages/ui/src/components/GalleryPost/GalleryPost.tsx index ac75687e9b..7aa8d4bbe4 100644 --- a/packages/ui/src/components/GalleryPost/GalleryPost.tsx +++ b/packages/ui/src/components/GalleryPost/GalleryPost.tsx @@ -107,6 +107,8 @@ export function GalleryPostDetailView({ post }: { post: db.Post }) { const formattedDate = useMemo(() => { return makePrettyShortDate(new Date(post.receivedAt)); }, [post.receivedAt]); + const content = usePostContent(post); + const isImagePost = content.some((block) => block.type === 'image'); return ( @@ -114,14 +116,16 @@ export function GalleryPostDetailView({ post }: { post: db.Post }) { - + {post.title && {post.title}} - {formattedDate} + Added {formattedDate} + + {isImagePost && } ); @@ -264,6 +268,18 @@ const noWrapperPadding = { }, } as const; +const CaptionContentRenderer = createContentRenderer({ + blockSettings: { + paragraph: { + size: '$body', + ...noWrapperPadding, + }, + image: { + display: 'none', + }, + }, +}); + const LargeContentRenderer = createContentRenderer({ blockSettings: { blockWrapper: { diff --git a/packages/ui/src/components/GroupChannelsScreenView.tsx b/packages/ui/src/components/GroupChannelsScreenView.tsx index f87cddbc76..b89ff31221 100644 --- a/packages/ui/src/components/GroupChannelsScreenView.tsx +++ b/packages/ui/src/components/GroupChannelsScreenView.tsx @@ -70,6 +70,14 @@ export function GroupChannelsScreenView({ [group] ); + const titleWidth = useCallback(() => { + if (isGroupAdmin) { + return 55; + } else { + return 75; + } + }, [isGroupAdmin]); + return ( diff --git a/packages/ui/src/components/GroupMembersScreenView.tsx b/packages/ui/src/components/GroupMembersScreenView.tsx index 33986a7382..bbee14dc92 100644 --- a/packages/ui/src/components/GroupMembersScreenView.tsx +++ b/packages/ui/src/components/GroupMembersScreenView.tsx @@ -24,6 +24,7 @@ export function GroupMembersScreenView({ onPressUnban, onPressAcceptJoinRequest, onPressRejectJoinRequest, + onPressGoToDm, }: { goBack: () => void; members: db.ChatMember[]; @@ -37,6 +38,7 @@ export function GroupMembersScreenView({ onPressUnban: (contactId: string) => void; onPressAcceptJoinRequest: (contactId: string) => void; onPressRejectJoinRequest: (contactId: string) => void; + onPressGoToDm: (contactId: string) => void; }) { const { bottom } = useSafeAreaInsets(); const [selectedContact, setSelectedContact] = useState(null); @@ -207,6 +209,7 @@ export function GroupMembersScreenView({ onPressKick={() => onPressKick(selectedContact)} onPressBan={() => onPressBan(selectedContact)} onPressUnban={() => onPressUnban(selectedContact)} + onPressGoToDm={() => onPressGoToDm(selectedContact)} /> )} {selectedContact !== null && selectedIsRequest && ( diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index fdf1eb9b65..7365ac7d7c 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -53,23 +53,9 @@ export function ChannelListItem({ return ( - + - - {title} - + {title} {customSubtitle ? ( {customSubtitle} ) : ( diff --git a/packages/ui/src/components/ListItem/GroupListItem.tsx b/packages/ui/src/components/ListItem/GroupListItem.tsx index ae42a5825d..fa7a379000 100644 --- a/packages/ui/src/components/ListItem/GroupListItem.tsx +++ b/packages/ui/src/components/ListItem/GroupListItem.tsx @@ -28,20 +28,9 @@ export const GroupListItem = ({ onPress={useBoundHandler(model, onPress)} onLongPress={useBoundHandler(model, onLongPress)} > - + - - {title} - + {title} {customSubtitle && ( {customSubtitle} )} diff --git a/packages/ui/src/components/ListItem/ListItem.tsx b/packages/ui/src/components/ListItem/ListItem.tsx index b8e5009c30..df282fc4d6 100644 --- a/packages/ui/src/components/ListItem/ListItem.tsx +++ b/packages/ui/src/components/ListItem/ListItem.tsx @@ -175,28 +175,26 @@ const ListItemCount = ({ return ( - {muted ? ( - - ) : ( - ); }; -const ListItemCountNumber = styled(SizableText, { +const ListItemCountNumber = styled(XStack, { name: 'ListItemCountNumber', - size: '$s', - color: '$secondaryText', - textAlign: 'center', + gap: '$s', + alignItems: 'center', variants: { hidden: { true: { diff --git a/packages/ui/src/components/MessageInput/MessageInputBase.tsx b/packages/ui/src/components/MessageInput/MessageInputBase.tsx index bbdac3fcfd..5c7a7d7188 100644 --- a/packages/ui/src/components/MessageInput/MessageInputBase.tsx +++ b/packages/ui/src/components/MessageInput/MessageInputBase.tsx @@ -1,8 +1,8 @@ import type { EditorBridge } from '@10play/tentap-editor'; -import { useCurrentSession } from '@tloncorp/shared/dist'; import * as db from '@tloncorp/shared/dist/db'; import { JSONContent, Story } from '@tloncorp/shared/dist/urbit'; import { ImagePickerAsset } from 'expo-image-picker'; +import { memo } from 'react'; import { PropsWithChildren } from 'react'; import { SpaceTokens } from 'tamagui'; import { ThemeTokens, View, XStack, YStack } from 'tamagui'; @@ -12,6 +12,7 @@ import { Button } from '../Button'; import { FloatingActionButton } from '../FloatingActionButton'; import { Icon } from '../Icon'; import { LoadingSpinner } from '../LoadingSpinner'; +import { GalleryDraftType } from '../draftInputs/shared'; import AttachmentButton from './AttachmentButton'; import InputMentionPopup from './InputMentionPopup'; @@ -25,9 +26,9 @@ export interface MessageInputProps { ) => Promise; channelId: string; groupMembers: db.ChatMember[]; - storeDraft: (draft: JSONContent) => void; - clearDraft: () => void; - getDraft: () => Promise; + storeDraft: (draft: JSONContent, draftType?: GalleryDraftType) => void; + clearDraft: (draftType?: GalleryDraftType) => void; + getDraft: (draftType?: GalleryDraftType) => Promise; editingPost?: db.Post; setEditingPost?: (post: db.Post | undefined) => void; editPost?: ( @@ -43,6 +44,7 @@ export interface MessageInputProps { backgroundColor?: ThemeTokens; placeholder?: string; bigInput?: boolean; + draftType?: GalleryDraftType; title?: string; image?: ImagePickerAsset; showInlineAttachments?: boolean; @@ -53,56 +55,54 @@ export interface MessageInputProps { // for external access to height setHeight?: (height: number) => void; goBack?: () => void; + shouldAutoFocus?: boolean; ref?: React.RefObject<{ editor: EditorBridge | null; setEditor: (editor: EditorBridge) => void; }>; } -export const MessageInputContainer = ({ - children, - onPressSend, - setShouldBlur, - containerHeight, - showMentionPopup = false, - showAttachmentButton = true, - floatingActionButton = false, - disableSend = false, - mentionText, - groupMembers, - onSelectMention, - isSending, - isEditing = false, - cancelEditing, - onPressEdit, - goBack, -}: PropsWithChildren<{ - setShouldBlur: (shouldBlur: boolean) => void; - onPressSend: () => void; - containerHeight: number; - showMentionPopup?: boolean; - showAttachmentButton?: boolean; - floatingActionButton?: boolean; - disableSend?: boolean; - mentionText?: string; - groupMembers: db.ChatMember[]; - onSelectMention: (contact: db.Contact) => void; - isEditing?: boolean; - isSending?: boolean; - cancelEditing?: () => void; - onPressEdit?: () => void; - goBack?: () => void; -}>) => { - const currentSession = useCurrentSession(); - const isDisconnected = - !currentSession || currentSession.isReconnecting === true; - const { canUpload } = useAttachmentContext(); - if (isEditing) { +export const MessageInputContainer = memo( + ({ + children, + onPressSend, + setShouldBlur, + containerHeight, + showMentionPopup = false, + showAttachmentButton = true, + floatingActionButton = false, + disableSend = false, + mentionText, + groupMembers, + onSelectMention, + isSending, + isEditing = false, + cancelEditing, + onPressEdit, + goBack, + }: PropsWithChildren<{ + setShouldBlur: (shouldBlur: boolean) => void; + onPressSend: () => void; + containerHeight: number; + showMentionPopup?: boolean; + showAttachmentButton?: boolean; + floatingActionButton?: boolean; + disableSend?: boolean; + mentionText?: string; + groupMembers: db.ChatMember[]; + onSelectMention: (contact: db.Contact) => void; + isEditing?: boolean; + isSending?: boolean; + cancelEditing?: () => void; + onPressEdit?: () => void; + goBack?: () => void; + }>) => { + const { canUpload } = useAttachmentContext(); + return ( - + {goBack ? ( + + + + ) : null} + + {isEditing ? ( + // using $2xs instead of $xs to match the padding of the attachment button + // might need to update the close icon? + + + + ) : null} + {canUpload && showAttachmentButton ? ( + + + + ) : null} {children} - - - + + ) : ( + + + + )} ); } +); - return ( - - - - {goBack ? ( - - - - ) : null} - {canUpload && showAttachmentButton ? ( - - - - ) : null} - {children} - {floatingActionButton ? ( - - {disableSend ? null : ( - } - /> - )} - - ) : ( - - - - )} - - - ); -}; +MessageInputContainer.displayName = 'MessageInputContainer'; diff --git a/packages/ui/src/components/MessageInput/helpers.ts b/packages/ui/src/components/MessageInput/helpers.ts index b42fd10e79..854cd917fe 100644 --- a/packages/ui/src/components/MessageInput/helpers.ts +++ b/packages/ui/src/components/MessageInput/helpers.ts @@ -4,6 +4,7 @@ import { createDevLogger, tiptap } from '@tloncorp/shared/dist'; import { Block, Inline, + JSONContent, constructStory, isInline, } from '@tloncorp/shared/dist/urbit'; @@ -14,10 +15,12 @@ const logger = createDevLogger('processReference', true); export async function processReferenceAndUpdateEditor({ editor, + editorJson, pastedText, matchRegex, processMatch, }: { + editorJson: JSONContent; editor: EditorBridge | Editor; pastedText: string; matchRegex: RegExp; @@ -29,14 +32,13 @@ export async function processReferenceAndUpdateEditor({ if (match) { logger.log('found match', match[0]); - const attachment = await processMatch(match[0]); + const attachment = processMatch(match[0]); if (attachment) { logger.log('extracted attachment', attachment); // remove the attachments corresponding text from the editor - const json = await editor.getJSON(); - const filteredJson = filterRegexFromJson(json, matchRegex); + const filteredJson = filterRegexFromJson(editorJson, matchRegex); logger.log(`updating editor`, filteredJson); if ('setContent' in editor) { diff --git a/packages/ui/src/components/MessageInput/index.native.tsx b/packages/ui/src/components/MessageInput/index.native.tsx index c5e93fcee5..58a3c2b7ad 100644 --- a/packages/ui/src/components/MessageInput/index.native.tsx +++ b/packages/ui/src/components/MessageInput/index.native.tsx @@ -133,10 +133,12 @@ export const MessageInput = forwardRef( initialHeight = DEFAULT_MESSAGE_INPUT_HEIGHT, placeholder = 'Message', bigInput = false, + draftType, title, image, channelType, setHeight, + shouldAutoFocus, goBack, onSend, }, @@ -209,9 +211,7 @@ export const MessageInput = forwardRef( const editor = useEditorBridge({ customSource: editorHtml, - // setting autofocus to true if we have editPost here doesn't seem to work - // so we're using a useEffect to set it - autofocus: false, + autofocus: shouldAutoFocus || false, bridgeExtensions, }); const editorState = useBridgeState(editor); @@ -236,11 +236,13 @@ export const MessageInput = forwardRef( } }, [editor, ref]); + const lastEditingPost = useRef(editingPost); + useEffect(() => { if (!hasSetInitialContent && editorState.isReady) { try { - getDraft().then((draft) => { - if (draft) { + getDraft(draftType).then((draft) => { + if (!editingPost && draft) { const inlines = tiptap.JSONToInlines(draft); const newInlines = inlines .map((inline) => { @@ -255,12 +257,14 @@ export const MessageInput = forwardRef( .filter((inline) => inline !== null) as Inline[]; const newStory = constructStory(newInlines); const tiptapContent = tiptap.diaryMixedToJSON(newStory); + messageInputLogger.log('Setting draft content', tiptapContent); // @ts-expect-error setContent does accept JSONContent editor.setContent(tiptapContent); setEditorIsEmpty(false); } - if (editingPost?.content) { + if (editingPost && editingPost.content) { + messageInputLogger.log('Editing post', editingPost); const { story, references: postReferences, @@ -303,8 +307,13 @@ export const MessageInput = forwardRef( (c) => !('type' in c) && !('block' in c && 'image' in c.block) ) as Story ); + messageInputLogger.log( + 'Setting edit post content', + tiptapContent + ); // @ts-expect-error setContent does accept JSONContent editor.setContent(tiptapContent); + setEditorIsEmpty(false); } if (editingPost?.image) { @@ -327,6 +336,7 @@ export const MessageInput = forwardRef( }, [ editor, getDraft, + draftType, hasSetInitialContent, editorState.isReady, editingPost, @@ -335,10 +345,18 @@ export const MessageInput = forwardRef( ]); useEffect(() => { - if (editor && !shouldBlur && !editorState.isFocused && !!editingPost) { + if (editingPost && lastEditingPost.current?.id !== editingPost.id) { + messageInputLogger.log('Editing post changed', editingPost); + lastEditingPost.current = editingPost; + setHasSetInitialContent(false); + } + }, [editingPost]); + + useEffect(() => { + if (editor && !shouldBlur && shouldAutoFocus && !editorState.isFocused) { editor.focus(); } - }, [shouldBlur, editor, editorState, editingPost]); + }, [shouldAutoFocus, editor, editorState, shouldBlur]); useEffect(() => { if (editor && shouldBlur && editorState.isFocused) { @@ -348,6 +366,8 @@ export const MessageInput = forwardRef( }, [shouldBlur, editor, editorState, setShouldBlur]); useEffect(() => { + messageInputLogger.log('Checking if editor is empty'); + editor.getJSON().then((json: JSONContent) => { const inlines = tiptap .JSONToInlines(json) @@ -372,13 +392,21 @@ export const MessageInput = forwardRef( blocks.length === 0 && attachments.length === 0; + messageInputLogger.log('Editor is empty?', isEmpty); + if (isEmpty !== editorIsEmpty) { + messageInputLogger.log('Setting editorIsEmpty', isEmpty); setEditorIsEmpty(isEmpty); + setContainerHeight(initialHeight); } }); - }, [editor, attachments, editorIsEmpty]); + }, [editor, attachments, editorIsEmpty, initialHeight]); editor._onContentUpdate = async () => { + messageInputLogger.log( + 'Content updated, update draft and check for mention text' + ); + const json = await editor.getJSON(); const inlines = ( tiptap @@ -393,25 +421,32 @@ export const MessageInput = forwardRef( (inline) => typeof inline === 'string' && inline.match(/\B[~@]/) ) as string | undefined; // extract the mention text from the mention inline - const mentionText = mentionInline + const mentionTextFromInline = mentionInline ? mentionInline.slice((mentionInline.match(/\B[~@]/)?.index ?? -1) + 1) : null; - if (mentionText !== null) { + if (mentionTextFromInline !== null) { + messageInputLogger.log('Mention text', mentionTextFromInline); // if we have a mention text, we show the mention popup setShowMentionPopup(true); - setMentionText(mentionText); + setMentionText(mentionTextFromInline); } else { setShowMentionPopup(false); + setMentionText(''); } - storeDraft(json); + messageInputLogger.log('Storing draft', json); + + storeDraft(json, draftType); }; const handlePaste = useCallback( async (pastedText: string) => { + messageInputLogger.log('Pasted text', pastedText); // check for ref from pasted cite paths + const editorJson = await editor.getJSON(); const citePathAttachment = await processReferenceAndUpdateEditor({ editor, + editorJson, pastedText, matchRegex: tiptap.REF_REGEX, processMatch: async (match) => { @@ -433,6 +468,7 @@ export const MessageInput = forwardRef( const DEEPLINK_REGEX = new RegExp(`^(https?://)?${branchDomain}/\\S+$`); const deepLinkAttachment = await processReferenceAndUpdateEditor({ editor, + editorJson, pastedText, matchRegex: DEEPLINK_REGEX, processMatch: async (deeplink) => { @@ -458,6 +494,7 @@ export const MessageInput = forwardRef( /^(https?:\/\/)?(tlon\.network\/lure\/)(0v[^/]+)$/; const lureLinkAttachment = await processReferenceAndUpdateEditor({ editor, + editorJson, pastedText, matchRegex: TLON_LURE_REGEX, processMatch: async (tlonLure) => { @@ -486,6 +523,7 @@ export const MessageInput = forwardRef( const onSelectMention = useCallback( async (contact: db.Contact) => { + messageInputLogger.log('Selected mention', contact); const json = await editor.getJSON(); const inlines = tiptap.JSONToInlines(json); @@ -540,13 +578,27 @@ export const MessageInput = forwardRef( const newJson = tiptap.diaryMixedToJSON(newStory); + // insert empty text node after mention + newJson.content?.map((node) => { + const containsMention = node.content?.some( + (n) => n.type === 'mention' + ); + if (containsMention) { + node.content?.push({ + type: 'text', + text: ' ', + }); + } + }); + + messageInputLogger.log('onSelectMention, setting new content', newJson); // @ts-expect-error setContent does accept JSONContent editor.setContent(newJson); - storeDraft(newJson); + storeDraft(newJson, draftType); setMentionText(''); setShowMentionPopup(false); }, - [editor, storeDraft] + [editor, storeDraft, draftType] ); const sendMessage = useCallback( @@ -644,7 +696,7 @@ export const MessageInput = forwardRef( onSend?.(); editor.setContent(''); clearAttachments(); - clearDraft(); + clearDraft(draftType); setShowBigInput?.(false); }, [ @@ -662,6 +714,7 @@ export const MessageInput = forwardRef( channelType, send, channelId, + draftType, ] ); @@ -844,6 +897,13 @@ export const MessageInput = forwardRef( const titleIsEmpty = useMemo(() => !title || title.length === 0, [title]); + const handleCancelEditing = useCallback(() => { + setEditingPost?.(undefined); + editor.setContent(''); + clearDraft(draftType); + clearAttachments(); + }, [setEditingPost, editor, clearDraft, clearAttachments, draftType]); + return ( ( showMentionPopup={showMentionPopup} isEditing={!!editingPost} isSending={isSending} - cancelEditing={() => setEditingPost?.(undefined)} + cancelEditing={handleCancelEditing} showAttachmentButton={showAttachmentButton} floatingActionButton={floatingActionButton} disableSend={ diff --git a/packages/ui/src/components/MetaEditorScreenView.tsx b/packages/ui/src/components/MetaEditorScreenView.tsx index 575f08e533..ba66c0af9b 100644 --- a/packages/ui/src/components/MetaEditorScreenView.tsx +++ b/packages/ui/src/components/MetaEditorScreenView.tsx @@ -56,7 +56,7 @@ export function MetaEditorScreenView({ }, [chat, modelLoaded, reset, defaultValues]); const runSubmit = useCallback( - () => handleSubmit(onSubmit), + () => handleSubmit(onSubmit)(), [handleSubmit, onSubmit] ); diff --git a/packages/ui/src/components/Onboarding/OnboardingBenefitsSheet.tsx b/packages/ui/src/components/Onboarding/OnboardingBenefitsSheet.tsx new file mode 100644 index 0000000000..2ec013305f --- /dev/null +++ b/packages/ui/src/components/Onboarding/OnboardingBenefitsSheet.tsx @@ -0,0 +1,94 @@ +import { ActionSheet, Icon, Image, View, XStack, YStack } from '@tloncorp/ui'; + +import { Text } from '../TextV2'; + +export function OnboardingBenefitsSheet({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (isOpen: boolean) => void; +}) { + return ( + + + + + + + + + + Welcome to TM + + + A messenger you can actually trust. + + + + + + + + + + + + Tlon operates on a peer-to-peer network. + + + Practically, this means your free account is a cloud computer. + You can run it yourself, or we can run it for you. + + + + + + + + + + + + Hassle-free messaging you can trust. + + We’ll make sure your computer is online and up-to-date. + Interested in self-hosting? You can always change your mind. + + + + + + + + + + + + Sign up with your email address. + + We’ll ask you a few questions to get you set up. + + + + + + + ); +} diff --git a/packages/ui/src/components/Onboarding/OnboardingButton.tsx b/packages/ui/src/components/Onboarding/OnboardingButton.tsx new file mode 100644 index 0000000000..e43a53820b --- /dev/null +++ b/packages/ui/src/components/Onboarding/OnboardingButton.tsx @@ -0,0 +1,36 @@ +import { ComponentProps, PropsWithChildren } from 'react'; +import { View } from 'tamagui'; + +import { Button } from '../Button'; + +export function OnboardingButton({ + secondary, + ...props +}: ComponentProps & { + secondary?: boolean; +}) { + const color = secondary ? '$secondaryText' : '$primaryText'; + return ( +