diff --git a/.github/workflows/pull-request-commit.yml b/.github/workflows/pull-request-commit.yml new file mode 100644 index 00000000..72381a32 --- /dev/null +++ b/.github/workflows/pull-request-commit.yml @@ -0,0 +1,185 @@ +# Credit https://github.com/expo/expo +# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml +--- +name: PR labeler + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-suite-fingerprint: + runs-on: ubuntu-22.04 + if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }} + # REQUIRED: limit concurrency when pushing main(default) branch to prevent conflict for this action to update its fingerprint database + concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }} + permissions: + # REQUIRED: Allow comments of PRs + pull-requests: write + # REQUIRED: Allow updating fingerprint in acton caches + actions: write + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 100 + + - name: ⬇️ Fetch commits from base branch + run: git fetch origin main:main --depth 100 + if: github.event_name == 'pull_request' + + - name: 🔧 Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: yarn + + - name: ⚙️ Install Dependencies + run: yarn install + + - name: Get the base commit + id: base-commit + run: | + # Since we limit this pr-labeler workflow only triggered from limited paths, we should use custom base commit + echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT" + + - name: 📷 Check fingerprint + id: fingerprint + uses: expo/expo-github-action/fingerprint@main + with: + previous-git-commit: ${{ steps.base-commit.outputs.base-commit }} + + - name: 👀 Debug fingerprint + run: | + echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}" + echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}" + + - name: 🏷️ Labeling PR + uses: actions/github-script@v6 + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }} + with: + script: | + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['bot: fingerprint changed'] + }) + } catch (e) { + if (e.status != 404) { + throw e; + } + } + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['bot: fingerprint compatible'] + }) + + - name: 🏷️ Labeling PR + uses: actions/github-script@v6 + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }} + with: + script: | + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['bot: fingerprint compatible'] + }) + } catch (e) { + if (e.status != 404) { + throw e; + } + } + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['bot: fingerprint changed'] + }) + + - name: 🔍 Find old comment if it exists + uses: peter-evans/find-comment@v2 + if: ${{ github.event_name == 'pull_request' }} + id: old_comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'expo-bot' + body-includes: + + - name: 💬 Add comment with fingerprint + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id == '' }} + uses: actions/github-script@v6 + with: + script: | + const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2); + const body = ` + The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }} +
Fingerprint diff + + \`\`\`json + ${diff} + \`\`\` + +
+ + --- + *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + + - name: 💬 Update comment with fingerprint + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id != '' }} + uses: actions/github-script@v6 + with: + script: | + const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2); + const body = ` + The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }} +
Fingerprint diff + + \`\`\`json + ${diff} + \`\`\` + +
+ + --- + *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* + `; + + github.rest.issues.updateComment({ + issue_number: context.issue.number, + comment_id: '${{ steps.old_comment.outputs.comment-id }}', + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + + - name: 💬 Delete comment with fingerprint + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' && steps.old_comment.outputs.comment-id != '' }} + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + comment_id: '${{ steps.old_comment.outputs.comment-id }}', + owner: context.repo.owner, + repo: context.repo.repo, + }); \ No newline at end of file diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 5f6edeac..0da2919c 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -23,6 +23,7 @@ import { DialogInnerProps, } from '#/components/Dialog/types' import {Context} from '#/components/Dialog/context' +import {isNative} from 'platform/detection' export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export * from '#/components/Dialog/types' @@ -221,7 +222,8 @@ export function ScrollableInner({children, style}: DialogInnerProps) { borderTopRightRadius: 40, }, flatten(style), - ]}> + ]} + contentContainerStyle={isNative ? a.pb_4xl : undefined}> {children} diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 1b9348d9..b81b2070 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -3,7 +3,6 @@ import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {isNative} from '#/platform/detection' import {useTheme, atoms as a, useBreakpoints} from '#/alf' import {Text} from '#/components/Typography' import {Button, ButtonColor, ButtonText} from '#/components/Button' @@ -86,7 +85,6 @@ export function Actions({children}: React.PropsWithChildren<{}>) { gtMobile ? [a.flex_row, a.flex_row_reverse, a.justify_start] : [a.flex_col], - isNative && [a.pb_4xl], ]}> {children} diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx new file mode 100644 index 00000000..62c95c78 --- /dev/null +++ b/src/components/dialogs/BirthDateSettings.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import {useLingui} from '@lingui/react' +import {Trans, msg} from '@lingui/macro' + +import * as Dialog from '#/components/Dialog' +import {Text} from '../Typography' +import {DateInput} from '#/view/com/util/forms/DateInput' +import {logger} from '#/logger' +import { + usePreferencesSetBirthDateMutation, + UsePreferencesQueryResponse, +} from '#/state/queries/preferences' +import {Button, ButtonText} from '../Button' +import {atoms as a, useTheme} from '#/alf' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import {cleanError} from '#/lib/strings/errors' +import {ActivityIndicator, View} from 'react-native' +import {isIOS, isWeb} from '#/platform/detection' + +export function BirthDateSettingsDialog({ + control, + preferences, +}: { + control: Dialog.DialogControlProps + preferences: UsePreferencesQueryResponse | undefined +}) { + const {_} = useLingui() + const {isPending, isError, error, mutateAsync} = + usePreferencesSetBirthDateMutation() + + return ( + + + + {preferences && !isPending ? ( + + ) : ( + + )} + + + ) +} + +function BirthdayInner({ + control, + preferences, + isError, + error, + setBirthDate, +}: { + control: Dialog.DialogControlProps + preferences: UsePreferencesQueryResponse + isError: boolean + error: unknown + setBirthDate: (args: {birthDate: Date}) => Promise +}) { + const {_} = useLingui() + const [date, setDate] = React.useState(preferences.birthDate || new Date()) + const t = useTheme() + + const hasChanged = date !== preferences.birthDate + + const onSave = React.useCallback(async () => { + try { + // skip if date is the same + if (hasChanged) { + await setBirthDate({birthDate: date}) + } + control.close() + } catch (e) { + logger.error(`setBirthDate failed`, {message: e}) + } + }, [date, setBirthDate, control, hasChanged]) + + return ( + + + + My Birthday + + + This information is not shared with other users. + + + + + + {isError ? ( + + ) : undefined} + + + + + + ) +} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index a18f6c87..db5be0b8 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -135,10 +135,6 @@ export interface PostLanguagesSettingsModal { name: 'post-languages-settings' } -export interface BirthDateSettingsModal { - name: 'birth-date-settings' -} - export interface VerifyEmailModal { name: 'verify-email' showReminder?: boolean @@ -179,7 +175,6 @@ export type Modal = | ChangeHandleModal | DeleteAccountModal | EditProfileModal - | BirthDateSettingsModal | VerifyEmailModal | ChangeEmailModal | ChangePasswordModal diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx deleted file mode 100644 index 1cab9598..00000000 --- a/src/view/com/modals/BirthDateSettings.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, {useState} from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {Text} from '../util/text/Text' -import {DateInput} from '../util/forms/DateInput' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {s, colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {cleanError} from 'lib/strings/errors' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' -import { - usePreferencesQuery, - usePreferencesSetBirthDateMutation, - UsePreferencesQueryResponse, -} from '#/state/queries/preferences' -import {logger} from '#/logger' - -export const snapPoints = ['50%', '90%'] - -function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() - const {_} = useLingui() - const { - isPending, - isError, - error, - mutateAsync: setBirthDate, - } = usePreferencesSetBirthDateMutation() - const [date, setDate] = useState(preferences.birthDate || new Date()) - const {closeModal} = useModalControls() - - const onSave = React.useCallback(async () => { - try { - await setBirthDate({birthDate: date}) - closeModal() - } catch (e) { - logger.error(`setBirthDate failed`, {message: e}) - } - }, [date, setBirthDate, closeModal]) - - return ( - - - - My Birthday - - - - - This information is not shared with other users. - - - - - - - {isError ? ( - - ) : undefined} - - - {isPending ? ( - - - - ) : ( - - - Save - - - )} - - - ) -} - -export function Component({}: {}) { - const {data: preferences} = usePreferencesQuery() - - return !preferences ? ( - - ) : ( - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: isWeb ? 0 : 40, - }, - titleSection: { - paddingTop: isWeb ? 0 : 4, - paddingBottom: isWeb ? 14 : 10, - }, - title: { - textAlign: 'center', - fontWeight: '600', - marginBottom: 5, - }, - error: { - borderRadius: 6, - marginTop: 10, - }, - dateInputButton: { - borderWidth: 1, - borderRadius: 6, - paddingVertical: 14, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnContainer: { - paddingTop: 20, - paddingHorizontal: 20, - }, -}) diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index 328d23dc..3c7edcf0 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -24,6 +24,8 @@ import { CONFIGURABLE_LABEL_GROUPS, UsePreferencesQueryResponse, } from '#/state/queries/preferences' +import {useDialogControl} from '#/components/Dialog' +import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' export const snapPoints = ['90%'] @@ -107,11 +109,11 @@ function AdultContentEnabledPref() { const {_} = useLingui() const {data: preferences} = usePreferencesQuery() const {mutate, variables} = usePreferencesSetAdultContentMutation() - const {openModal} = useModalControls() + const bithdayDialogControl = useDialogControl() const onSetAge = React.useCallback( - () => openModal({name: 'birth-date-settings'}), - [openModal], + () => bithdayDialogControl.open(), + [bithdayDialogControl], ) const onToggleAdultContent = React.useCallback(async () => { @@ -135,6 +137,10 @@ function AdultContentEnabledPref() { return ( + {isIOS ? ( preferences?.adultContentEnabled ? null : ( diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index e03879c1..e382e6fa 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -25,7 +25,6 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as ModerationDetailsModal from './ModerationDetails' -import * as BirthDateSettingsModal from './BirthDateSettings' import * as VerifyEmailModal from './VerifyEmail' import * as ChangeEmailModal from './ChangeEmail' import * as ChangePasswordModal from './ChangePassword' @@ -122,9 +121,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'moderation-details') { snapPoints = ModerationDetailsModal.snapPoints element = - } else if (activeModal?.name === 'birth-date-settings') { - snapPoints = BirthDateSettingsModal.snapPoints - element = } else if (activeModal?.name === 'verify-email') { snapPoints = VerifyEmailModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index d72b7e48..66ea2311 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -27,7 +27,6 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as ModerationDetailsModal from './ModerationDetails' -import * as BirthDateSettingsModal from './BirthDateSettings' import * as VerifyEmailModal from './VerifyEmail' import * as ChangeEmailModal from './ChangeEmail' import * as ChangePasswordModal from './ChangePassword' @@ -117,8 +116,6 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'moderation-details') { element = - } else if (modal.name === 'birth-date-settings') { - element = } else if (modal.name === 'verify-email') { element = } else if (modal.name === 'change-email') { diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 2e2de1d8..3d8d310e 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -40,7 +40,10 @@ import { } from '#/state/preferences' import {useSession, useSessionApi, SessionAccount} from '#/state/session' import {useProfileQuery} from '#/state/queries/profile' -import {useClearPreferencesMutation} from '#/state/queries/preferences' +import { + useClearPreferencesMutation, + usePreferencesQuery, +} from '#/state/queries/preferences' // TODO import {useInviteCodesQuery} from '#/state/queries/invites' import {clear as clearStorage} from '#/state/persisted/store' import {clearLegacyStorage} from '#/state/persisted/legacy' @@ -68,6 +71,7 @@ import {SelectableBtn} from 'view/com/util/forms/SelectableBtn' import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {ExportCarDialog} from './ExportCarDialog' +import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' function SettingsAccountCard({account}: {account: SessionAccount}) { const pal = usePalette('default') @@ -152,6 +156,7 @@ export function SettingsScreen({}: Props) { const {screen, track} = useAnalytics() const {openModal} = useModalControls() const {isSwitchingAccounts, accounts, currentAccount} = useSession() + const {data: preferences} = usePreferencesQuery() const {mutate: clearPreferences} = useClearPreferencesMutation() // TODO // const {data: invites} = useInviteCodesQuery() @@ -159,6 +164,7 @@ export function SettingsScreen({}: Props) { const {setShowLoggedOut} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() const exportCarControl = useDialogControl() + const birthdayControl = useDialogControl() // const primaryBg = useCustomPalette({ // light: {backgroundColor: colors.blue0}, @@ -269,6 +275,10 @@ export function SettingsScreen({}: Props) { Linking.openURL(STATUS_PAGE_URL) }, []) + const onPressBirthday = React.useCallback(() => { + birthdayControl.open() + }, [birthdayControl]) + const clearAllStorage = React.useCallback(async () => { await clearStorage() Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) @@ -281,6 +291,10 @@ export function SettingsScreen({}: Props) { return ( + Birthday:{' '} - openModal({name: 'birth-date-settings'})}> + Show