diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.native.tsx | 2 | ||||
-rw-r--r-- | src/components/ReportDialog/SelectLabelerView.tsx | 8 | ||||
-rw-r--r-- | src/components/ReportDialog/SelectReportOptionView.tsx | 13 | ||||
-rw-r--r-- | src/components/moderation/LabelPreference.tsx | 13 | ||||
-rw-r--r-- | src/lib/app-info.ts | 10 | ||||
-rw-r--r-- | src/lib/hooks/useOTAUpdates.ts | 142 | ||||
-rw-r--r-- | src/screens/Moderation/index.tsx | 60 | ||||
-rw-r--r-- | src/screens/Profile/Sections/Labels.tsx | 27 | ||||
-rw-r--r-- | src/view/screens/Settings/index.tsx | 86 |
9 files changed, 250 insertions, 111 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index d6e726a59..2c880f217 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -19,6 +19,7 @@ import {init as initPersistedState} from '#/state/persisted' import * as persisted from '#/state/persisted' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {useIntentHandler} from 'lib/hooks/useIntentHandler' +import {useOTAUpdates} from 'lib/hooks/useOTAUpdates' import * as notifications from 'lib/notifications/notifications' import { asyncStoragePersister, @@ -60,6 +61,7 @@ function InnerApp() { const theme = useColorModeTheme() const {_} = useLingui() useIntentHandler() + useOTAUpdates() // init useEffect(() => { diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx index 383d1b95f..dd07cafa3 100644 --- a/src/components/ReportDialog/SelectLabelerView.tsx +++ b/src/components/ReportDialog/SelectLabelerView.tsx @@ -1,18 +1,16 @@ import React from 'react' import {View} from 'react-native' +import {AppBskyLabelerDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {AppBskyLabelerDefs} from '@atproto/api' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' import {getLabelingServiceTitle} from '#/lib/moderation' - -import {atoms as a, useTheme, useBreakpoints} from '#/alf' -import {Text} from '#/components/Typography' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, useButtonContext} from '#/components/Button' import {Divider} from '#/components/Divider' import * as LabelingServiceCard from '#/components/LabelingServiceCard' - +import {Text} from '#/components/Typography' import {ReportDialogProps} from './types' export function SelectLabelerView({ diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx index 54844cfda..c67698348 100644 --- a/src/components/ReportDialog/SelectReportOptionView.tsx +++ b/src/components/ReportDialog/SelectReportOptionView.tsx @@ -1,16 +1,15 @@ import React from 'react' import {View} from 'react-native' +import {AppBskyLabelerDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {AppBskyLabelerDefs} from '@atproto/api' -import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions' -import {DMCA_LINK} from '#/components/ReportDialog/const' +import {ReportOption, useReportOptions} from '#/lib/moderation/useReportOptions' import {Link} from '#/components/Link' +import {DMCA_LINK} from '#/components/ReportDialog/const' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' -import {atoms as a, useTheme, useBreakpoints} from '#/alf' -import {Text} from '#/components/Typography' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import { Button, ButtonIcon, @@ -19,11 +18,11 @@ import { } from '#/components/Button' import {Divider} from '#/components/Divider' import { - ChevronRight_Stroke2_Corner0_Rounded as ChevronRight, ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft, + ChevronRight_Stroke2_Corner0_Rounded as ChevronRight, } from '#/components/icons/Chevron' import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight' - +import {Text} from '#/components/Typography' import {ReportDialogProps} from './types' export function SelectReportOptionView({ diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx index 7d4bd9c32..028bd1a39 100644 --- a/src/components/moderation/LabelPreference.tsx +++ b/src/components/moderation/LabelPreference.tsx @@ -1,22 +1,21 @@ import React from 'react' import {View} from 'react-native' import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api' -import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' +import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription' +import {getLabelStrings} from '#/lib/moderation/useLabelInfo' import { usePreferencesQuery, usePreferencesSetContentLabelMutation, } from '#/state/queries/preferences' -import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription' -import {getLabelStrings} from '#/lib/moderation/useLabelInfo' - -import {useTheme, atoms as a, useBreakpoints} from '#/alf' -import {Text} from '#/components/Typography' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import * as ToggleButton from '#/components/forms/ToggleButton' import {InlineLink} from '#/components/Link' +import {Text} from '#/components/Typography' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo' -import * as ToggleButton from '#/components/forms/ToggleButton' export function Outer({children}: React.PropsWithChildren<{}>) { return ( diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts index 3f026d3fe..3071e031b 100644 --- a/src/lib/app-info.ts +++ b/src/lib/app-info.ts @@ -1,5 +1,9 @@ import VersionNumber from 'react-native-version-number' -import * as Updates from 'expo-updates' -export const updateChannel = Updates.channel -export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})` +export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development' +export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight' + +const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production' +export const appVersion = `${VersionNumber.appVersion} (${ + VersionNumber.buildVersion +}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})` diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts new file mode 100644 index 000000000..181f0b2c6 --- /dev/null +++ b/src/lib/hooks/useOTAUpdates.ts @@ -0,0 +1,142 @@ +import React from 'react' +import {Alert, AppState, AppStateStatus} from 'react-native' +import app from 'react-native-version-number' +import { + checkForUpdateAsync, + fetchUpdateAsync, + isEnabled, + reloadAsync, + setExtraParamAsync, + useUpdates, +} from 'expo-updates' + +import {logger} from '#/logger' +import {IS_TESTFLIGHT} from 'lib/app-info' +import {isIOS} from 'platform/detection' + +const MINIMUM_MINIMIZE_TIME = 15 * 60e3 + +async function setExtraParams() { + await setExtraParamAsync( + isIOS ? 'ios-build-number' : 'android-build-number', + // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. + // This just ensures it gets passed as a string + `${app.buildVersion}`, + ) + await setExtraParamAsync( + 'channel', + IS_TESTFLIGHT ? 'testflight' : 'production', + ) +} + +export function useOTAUpdates() { + const appState = React.useRef<AppStateStatus>('active') + const lastMinimize = React.useRef(0) + const ranInitialCheck = React.useRef(false) + const timeout = React.useRef<NodeJS.Timeout>() + const {isUpdatePending} = useUpdates() + + const setCheckTimeout = React.useCallback(() => { + timeout.current = setTimeout(async () => { + try { + await setExtraParams() + + logger.debug('Checking for update...') + const res = await checkForUpdateAsync() + + if (res.isAvailable) { + logger.debug('Attempting to fetch update...') + await fetchUpdateAsync() + } else { + logger.debug('No update available.') + } + } catch (e) { + logger.warn('OTA Update Error', {error: `${e}`}) + } + }, 10e3) + }, []) + + const onIsTestFlight = React.useCallback(() => { + setTimeout(async () => { + try { + await setExtraParams() + + const res = await checkForUpdateAsync() + if (res.isAvailable) { + await fetchUpdateAsync() + + Alert.alert( + 'Update Available', + 'A new version of the app is available. Relaunch now?', + [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Relaunch', + style: 'default', + onPress: async () => { + await reloadAsync() + }, + }, + ], + ) + } + } catch (e: any) { + // No need to handle + } + }, 3e3) + }, []) + + React.useEffect(() => { + // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This + // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update + // immediately. + if (IS_TESTFLIGHT) { + onIsTestFlight() + return + } else if (!isEnabled || __DEV__ || ranInitialCheck.current) { + // Development client shouldn't check for updates at all, so we skip that here. + return + } + + setCheckTimeout() + ranInitialCheck.current = true + }, [onIsTestFlight, setCheckTimeout]) + + // After the app has been minimized for 30 minutes, we want to either A. install an update if one has become available + // or B check for an update again. + React.useEffect(() => { + if (!isEnabled) return + + const subscription = AppState.addEventListener( + 'change', + async nextAppState => { + if ( + appState.current.match(/inactive|background/) && + nextAppState === 'active' + ) { + // If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since + // chances are that there isn't anything important going on in the current session. + if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) { + if (isUpdatePending) { + await reloadAsync() + } else { + setCheckTimeout() + } + } + } else { + lastMinimize.current = Date.now() + } + + appState.current = nextAppState + }, + ) + + return () => { + clearTimeout(timeout.current) + subscription.remove() + } + }, [isUpdatePending, setCheckTimeout]) +} diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 7d991cc71..9d51a6197 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -1,51 +1,49 @@ import React from 'react' import {View} from 'react-native' -import {useFocusEffect} from '@react-navigation/native' +import {useSafeAreaFrame} from 'react-native-safe-area-context' import {ComAtprotoLabelDefs} from '@atproto/api' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' import {LABELS} from '@atproto/api' -import {useSafeAreaFrame} from 'react-native-safe-area-context' - -import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types' -import {CenteredView} from '#/view/com/util/Views' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {useAnalytics} from 'lib/analytics/analytics' -import {useSetMinimalShellMode} from '#/state/shell' -import {useSession} from '#/state/session' -import { - useProfileQuery, - useProfileUpdateMutation, -} from '#/state/queries/profile' -import {ScrollView} from '#/view/com/util/Views' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' +import {getLabelingServiceTitle} from '#/lib/moderation' +import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import {logger} from '#/logger' import { - UsePreferencesQueryResponse, useMyLabelersQuery, usePreferencesQuery, + UsePreferencesQueryResponse, usePreferencesSetAdultContentMutation, } from '#/state/queries/preferences' - -import {getLabelingServiceTitle} from '#/lib/moderation' -import {logger} from '#/logger' -import {useTheme, atoms as a, useBreakpoints, ViewStyleProp} from '#/alf' +import { + useProfileQuery, + useProfileUpdateMutation, +} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {useSetMinimalShellMode} from '#/state/shell' +import {useAnalytics} from 'lib/analytics/analytics' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {CenteredView} from '#/view/com/util/Views' +import {ScrollView} from '#/view/com/util/Views' +import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {Divider} from '#/components/Divider' +import * as Toggle from '#/components/forms/Toggle' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' +import {Props as SVGIconProps} from '#/components/icons/common' +import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' -import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' -import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' -import {Text} from '#/components/Typography' -import * as Toggle from '#/components/forms/Toggle' +import * as LabelingService from '#/components/LabelingServiceCard' import {InlineLink, Link} from '#/components/Link' -import {Button, ButtonText} from '#/components/Button' import {Loader} from '#/components/Loader' -import * as LabelingService from '#/components/LabelingServiceCard' import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' -import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' -import {Props as SVGIconProps} from '#/components/icons/common' -import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' -import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' function ErrorState({error}: {error: string}) { const t = useTheme() diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index 2b2b99594..5ba8f00a5 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -1,30 +1,29 @@ import React from 'react' import {View} from 'react-native' +import {useSafeAreaFrame} from 'react-native-safe-area-context' import { AppBskyLabelerDefs, - ModerationOpts, - interpretLabelValueDefinitions, InterpretedLabelValueDefinition, + interpretLabelValueDefinitions, + ModerationOpts, } from '@atproto/api' -import {Trans, msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useSafeAreaFrame} from 'react-native-safe-area-context' -import {useScrollHandlers} from '#/lib/ScrollContext' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' -import {ListRef} from '#/view/com/util/List' -import {SectionRef} from './types' +import {useScrollHandlers} from '#/lib/ScrollContext' import {isNative} from '#/platform/detection' - -import {useTheme, atoms as a} from '#/alf' -import {Text} from '#/components/Typography' -import {Loader} from '#/components/Loader' -import {Divider} from '#/components/Divider' +import {ListRef} from '#/view/com/util/List' import {CenteredView, ScrollView} from '#/view/com/util/Views' -import {ErrorState} from '../ErrorState' -import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' +import {atoms as a, useTheme} from '#/alf' +import {Divider} from '#/components/Divider' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {Loader} from '#/components/Loader' +import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' +import {Text} from '#/components/Typography' +import {ErrorState} from '../ErrorState' +import {SectionRef} from './types' interface LabelsSectionProps { isLabelerLoading: boolean diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 3967678b4..790ce5ee9 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -3,72 +3,72 @@ import { ActivityIndicator, Linking, Platform, - StyleSheet, Pressable, + StyleSheet, TextStyle, TouchableOpacity, View, ViewStyle, } from 'react-native' -import {useFocusEffect, useNavigation} from '@react-navigation/native' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import * as AppInfo from 'lib/app-info' -import {usePalette} from 'lib/hooks/usePalette' -import {useCustomPalette} from 'lib/hooks/useCustomPalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher' -import {useAnalytics} from 'lib/analytics/analytics' -import {NavigationProp} from 'lib/routes/types' -import {HandIcon, HashtagIcon} from 'lib/icons' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import Clipboard from '@react-native-clipboard/clipboard' -import {makeProfileLink} from 'lib/routes/links' -import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' +import {useFocusEffect, useNavigation} from '@react-navigation/native' +import {useQueryClient} from '@tanstack/react-query' + +import {isNative} from '#/platform/detection' import {useModalControls} from '#/state/modals' -import { - useSetMinimalShellMode, - useThemePrefs, - useSetThemePrefs, - useOnboardingDispatch, -} from '#/state/shell' +import {clearLegacyStorage} from '#/state/persisted/legacy' +// TODO import {useInviteCodesQuery} from '#/state/queries/invites' +import {clear as clearStorage} from '#/state/persisted/store' import { useRequireAltTextEnabled, useSetRequireAltTextEnabled, } from '#/state/preferences' -import {useSession, useSessionApi, SessionAccount} from '#/state/session' -import {useProfileQuery} from '#/state/queries/profile' -import {useClearPreferencesMutation} 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' -import {STATUS_PAGE_URL} from 'lib/constants' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useQueryClient} from '@tanstack/react-query' -import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import {useCloseAllActiveElements} from '#/state/util' import { useInAppBrowser, useSetInAppBrowser, } from '#/state/preferences/in-app-browser' -import {isNative} from '#/platform/detection' -import {useDialogControl} from '#/components/Dialog' - -import {s, colors} from 'lib/styles' -import {ScrollView} from 'view/com/util/Views' +import {useClearPreferencesMutation} from '#/state/queries/preferences' +import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' +import {useProfileQuery} from '#/state/queries/profile' +import {SessionAccount, useSession, useSessionApi} from '#/state/session' +import { + useOnboardingDispatch, + useSetMinimalShellMode, + useSetThemePrefs, + useThemePrefs, +} from '#/state/shell' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' +import {useAnalytics} from 'lib/analytics/analytics' +import * as AppInfo from 'lib/app-info' +import {STATUS_PAGE_URL} from 'lib/constants' +import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher' +import {useCustomPalette} from 'lib/hooks/useCustomPalette' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {HandIcon, HashtagIcon} from 'lib/icons' +import {makeProfileLink} from 'lib/routes/links' +import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' +import {NavigationProp} from 'lib/routes/types' +import {colors, s} from 'lib/styles' +import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' +import {SelectableBtn} from 'view/com/util/forms/SelectableBtn' +import {ToggleButton} from 'view/com/util/forms/ToggleButton' import {Link, TextLink} from 'view/com/util/Link' +import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' import {UserAvatar} from 'view/com/util/UserAvatar' -import {ToggleButton} from 'view/com/util/forms/ToggleButton' -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 {ScrollView} from 'view/com/util/Views' +import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import {ExportCarDialog} from './ExportCarDialog' function SettingsAccountCard({account}: {account: SessionAccount}) { const pal = usePalette('default') @@ -890,9 +890,7 @@ export function SettingsScreen({}: Props) { accessibilityRole="button" onPress={onPressBuildInfo}> <Text type="sm" style={[styles.buildInfo, pal.textLight]}> - <Trans> - Build version {AppInfo.appVersion} {AppInfo.updateChannel} - </Trans> + <Trans>Version {AppInfo.appVersion}</Trans> </Text> </TouchableOpacity> <Text type="sm" style={[pal.textLight]}> |