diff --git a/src/components/Link.tsx b/src/components/Link.tsx index a2e952a6ef..d8ac829b67 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -12,7 +12,8 @@ import { isExternalUrl, linkRequiresWarning, } from '#/lib/strings/url-helpers' -import {isNative, isWeb} from '#/platform/detection' +import {isNative} from '#/platform/detection' +import {shouldClickOpenNewTab} from '#/platform/urls' import {useModalControls} from '#/state/modals' import {useOpenLink} from '#/state/preferences/in-app-browser' import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' @@ -116,16 +117,7 @@ export function useLink({ if (isExternal) { openLink(href) } else { - /** - * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch - * of @ts-ignore below. - */ - const event = e as any - const isMiddleClick = isWeb && event.button === 1 - const isMetaKey = - isWeb && - (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) - const shouldOpenInNewTab = isMetaKey || isMiddleClick + const shouldOpenInNewTab = shouldClickOpenNewTab(e) if (isBskyDownloadUrl(href)) { shareUrl(BSKY_DOWNLOAD_URL) diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 55a7948ef3..58aa4087c0 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -72,14 +72,17 @@ export function useNotificationsRegistration() { export function useRequestNotificationsPermission() { const gate = useGate() + const {currentAccount} = useSession() - return async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => { + return async ( + context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home', + ) => { const permissions = await Notifications.getPermissionsAsync() if ( !isNative || permissions?.status === 'granted' || - permissions?.status === 'denied' + (permissions?.status === 'denied' && !permissions.canAskAgain) ) { return } @@ -95,6 +98,9 @@ export function useRequestNotificationsPermission() { ) { return } + if (context === 'Home' && !currentAccount) { + return + } const res = await Notifications.requestPermissionsAsync() logEvent('notifications:request', { diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index f3edd74382..2d76eb12c4 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -17,7 +17,7 @@ export type LogEvents = { } 'notifications:openApp': {} 'notifications:request': { - context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' + context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home' status: 'granted' | 'denied' | 'undetermined' } 'state:background:sampled': { diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index 24ab678934..cbaaf445a8 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -116,7 +116,7 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { const langs = appLanguage.split(',').filter(Boolean) for (const lang of langs) { - switch (lang) { + switch (fixLegacyLanguageCode(lang)) { case 'en': return AppLanguage.en case 'ca': @@ -157,3 +157,20 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { } return AppLanguage.en } + +export function fixLegacyLanguageCode(code: string | null): string | null { + // handle some legacy code conversions, see https://xml.coverpages.org/iso639a.html + if (code === 'in') { + // indonesian + return 'id' + } + if (code === 'iw') { + // hebrew + return 'he' + } + if (code === 'ji') { + // yiddish + return 'yi' + } + return code +} diff --git a/src/platform/detection.ts b/src/platform/detection.ts index fffde1be2d..0c0360a82a 100644 --- a/src/platform/detection.ts +++ b/src/platform/detection.ts @@ -2,6 +2,7 @@ import {Platform} from 'react-native' import {isReducedMotion} from 'react-native-reanimated' import {getLocales} from 'expo-localization' +import {fixLegacyLanguageCode} from '#/locale/helpers' import {dedupArray} from 'lib/functions' export const isIOS = Platform.OS === 'ios' @@ -17,7 +18,7 @@ export const isMobileWeb = export const deviceLocales = dedupArray( getLocales?.() - .map?.(locale => locale.languageCode) + .map?.(locale => fixLegacyLanguageCode(locale.languageCode)) .filter(code => typeof code === 'string'), ) as string[] diff --git a/src/platform/urls.tsx b/src/platform/urls.tsx index fd844d9331..fd9d297aa2 100644 --- a/src/platform/urls.tsx +++ b/src/platform/urls.tsx @@ -1,4 +1,5 @@ -import {Linking} from 'react-native' +import {GestureResponderEvent, Linking} from 'react-native' + import {isNative, isWeb} from './detection' export async function getInitialURL(): Promise { @@ -23,3 +24,15 @@ export function clearHash() { window.location.hash = '' } } + +export function shouldClickOpenNewTab(e: GestureResponderEvent) { + /** + * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch + * of @ts-ignore below. + */ + const event = e as any + const isMiddleClick = isWeb && event.button === 1 + const isMetaKey = + isWeb && (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) + return isMetaKey || isMiddleClick +} diff --git a/src/state/persisted/store.ts b/src/state/persisted/store.ts index 421bdccdf7..ca023a6363 100644 --- a/src/state/persisted/store.ts +++ b/src/state/persisted/store.ts @@ -13,7 +13,13 @@ export async function write(value: Schema) { export async function read(): Promise { const rawData = await AsyncStorage.getItem(BSKY_STORAGE) const objData = rawData ? JSON.parse(rawData) : undefined + + // new user + if (!objData) return undefined + + // existing user, validate const parsed = schema.safeParse(objData) + if (parsed.success) { return objData } else { diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index fac08a7113..5bcac2e67d 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -305,7 +305,13 @@ export const ComposePost = observer(function ComposePost({ localThumb: undefined, } as apilib.ExternalEmbedDraft) } - setError(cleanError(e.message)) + let err = cleanError(e.message) + if (err.includes('not locate record')) { + err = _( + msg`We're sorry! The post you are replying to has been deleted.`, + ) + } + setError(err) setIsProcessing(false) return } finally { @@ -785,11 +791,12 @@ const styles = StyleSheet.create({ }, errorLine: { flexDirection: 'row', + alignItems: 'center', backgroundColor: colors.red1, borderRadius: 6, marginHorizontal: 16, - paddingHorizontal: 8, - paddingVertical: 6, + paddingHorizontal: 12, + paddingVertical: 10, marginBottom: 8, }, reminderLine: { diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 575f0a1306..d216849c5f 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -1,5 +1,12 @@ import React from 'react' -import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import { + Linking, + Pressable, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native' import {AtUri} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Plural, Trans} from '@lingui/macro' @@ -26,6 +33,7 @@ import {RichText} from '#/components/RichText' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' import hairlineWidth = StyleSheet.hairlineWidth +import {shouldClickOpenNewTab} from '#/platform/urls' export function FeedSourceCard({ feedUri, @@ -203,17 +211,30 @@ export function FeedSourceCardLoaded({ style, {borderTopWidth: hideTopBorder ? 0 : hairlineWidth}, ]} - onPress={() => { + onPress={e => { + const shouldOpenInNewTab = shouldClickOpenNewTab(e) if (feed.type === 'feed') { - navigation.push('ProfileFeed', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) + if (shouldOpenInNewTab) { + Linking.openURL( + `/profile/${feed.creatorDid}/feed/${new AtUri(feed.uri).rkey}`, + ) + } else { + navigation.push('ProfileFeed', { + name: feed.creatorDid, + rkey: new AtUri(feed.uri).rkey, + }) + } } else if (feed.type === 'list') { - navigation.push('ProfileList', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) + if (shouldOpenInNewTab) { + Linking.openURL( + `/profile/${feed.creatorDid}/lists/${new AtUri(feed.uri).rkey}`, + ) + } else { + navigation.push('ProfileList', { + name: feed.creatorDid, + rkey: new AtUri(feed.uri).rkey, + }) + } } }} key={feed.uri}> diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx index a152bc9095..2b77739d32 100644 --- a/src/view/com/posts/FeedErrorMessage.tsx +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -49,8 +49,7 @@ export function FeedErrorMessage({ if ( typeof knownError !== 'undefined' && knownError !== KnownError.Unknown && - (savedFeedConfig?.type === 'feed' || - knownError === KnownError.FeedNSFPublic) + feedDesc.startsWith('feedgen') ) { return ( { + requestNotificationsPermission('Home') + }, [requestNotificationsPermission]) + const pagerRef = React.useRef(null) const lastPagerReportedIndexRef = React.useRef(selectedIndex) React.useLayoutEffect(() => {