Merge remote-tracking branch 'upstream/main' into Improve-notification-localization

This commit is contained in:
Minseo Lee
2024-06-11 09:06:35 +09:00
28 changed files with 248 additions and 118 deletions
+3 -11
View File
@@ -12,7 +12,8 @@ import {
isExternalUrl, isExternalUrl,
linkRequiresWarning, linkRequiresWarning,
} from '#/lib/strings/url-helpers' } 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 {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser' import {useOpenLink} from '#/state/preferences/in-app-browser'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
@@ -116,16 +117,7 @@ export function useLink({
if (isExternal) { if (isExternal) {
openLink(href) openLink(href)
} else { } else {
/** const shouldOpenInNewTab = shouldClickOpenNewTab(e)
* 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
if (isBskyDownloadUrl(href)) { if (isBskyDownloadUrl(href)) {
shareUrl(BSKY_DOWNLOAD_URL) shareUrl(BSKY_DOWNLOAD_URL)
@@ -1,23 +1,15 @@
import {interpolate, useAnimatedStyle} from 'react-native-reanimated' import {interpolate, useAnimatedStyle} from 'react-native-reanimated'
import {useMinimalShellMode as useMinimalShellModeState} from '#/state/shell/minimal-mode'
import {useMinimalShellMode} from '#/state/shell/minimal-mode'
import {useShellLayout} from '#/state/shell/shell-layout' import {useShellLayout} from '#/state/shell/shell-layout'
export function useMinimalShellMode() { // Keep these separated so that we only pay for useAnimatedStyle that gets used.
const mode = useMinimalShellModeState()
const {footerHeight, headerHeight} = useShellLayout()
const footerMinimalShellTransform = useAnimatedStyle(() => { export function useMinimalShellHeaderTransform() {
return { const mode = useMinimalShellMode()
pointerEvents: mode.value === 0 ? 'auto' : 'none', const {headerHeight} = useShellLayout()
opacity: Math.pow(1 - mode.value, 2),
transform: [ const headerTransform = useAnimatedStyle(() => {
{
translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]),
},
],
}
})
const headerMinimalShellTransform = useAnimatedStyle(() => {
return { return {
pointerEvents: mode.value === 0 ? 'auto' : 'none', pointerEvents: mode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - mode.value, 2), opacity: Math.pow(1 - mode.value, 2),
@@ -28,7 +20,32 @@ export function useMinimalShellMode() {
], ],
} }
}) })
const fabMinimalShellTransform = useAnimatedStyle(() => {
return headerTransform
}
export function useMinimalShellFooterTransform() {
const mode = useMinimalShellMode()
const {footerHeight} = useShellLayout()
const footerTransform = useAnimatedStyle(() => {
return {
pointerEvents: mode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - mode.value, 2),
transform: [
{
translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]),
},
],
}
})
return footerTransform
}
export function useMinimalShellFabTransform() {
const mode = useMinimalShellMode()
const fabTransform = useAnimatedStyle(() => {
return { return {
transform: [ transform: [
{ {
@@ -37,9 +54,5 @@ export function useMinimalShellMode() {
], ],
} }
}) })
return { return fabTransform
footerMinimalShellTransform,
headerMinimalShellTransform,
fabMinimalShellTransform,
}
} }
+8 -2
View File
@@ -72,14 +72,17 @@ export function useNotificationsRegistration() {
export function useRequestNotificationsPermission() { export function useRequestNotificationsPermission() {
const gate = useGate() const gate = useGate()
const {currentAccount} = useSession()
return async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => { return async (
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
) => {
const permissions = await Notifications.getPermissionsAsync() const permissions = await Notifications.getPermissionsAsync()
if ( if (
!isNative || !isNative ||
permissions?.status === 'granted' || permissions?.status === 'granted' ||
permissions?.status === 'denied' (permissions?.status === 'denied' && !permissions.canAskAgain)
) { ) {
return return
} }
@@ -95,6 +98,9 @@ export function useRequestNotificationsPermission() {
) { ) {
return return
} }
if (context === 'Home' && !currentAccount) {
return
}
const res = await Notifications.requestPermissionsAsync() const res = await Notifications.requestPermissionsAsync()
logEvent('notifications:request', { logEvent('notifications:request', {
+1 -1
View File
@@ -17,7 +17,7 @@ export type LogEvents = {
} }
'notifications:openApp': {} 'notifications:openApp': {}
'notifications:request': { 'notifications:request': {
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'
status: 'granted' | 'denied' | 'undetermined' status: 'granted' | 'denied' | 'undetermined'
} }
'state:background:sampled': { 'state:background:sampled': {
-1
View File
@@ -3,4 +3,3 @@ export type Gate =
| 'request_notifications_permission_after_onboarding_v2' | 'request_notifications_permission_after_onboarding_v2'
| 'show_avi_follow_button' | 'show_avi_follow_button'
| 'show_follow_back_label_v2' | 'show_follow_back_label_v2'
| 'show_notification_badge_mobile_web'
+18 -1
View File
@@ -116,7 +116,7 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
const langs = appLanguage.split(',').filter(Boolean) const langs = appLanguage.split(',').filter(Boolean)
for (const lang of langs) { for (const lang of langs) {
switch (lang) { switch (fixLegacyLanguageCode(lang)) {
case 'en': case 'en':
return AppLanguage.en return AppLanguage.en
case 'ca': case 'ca':
@@ -157,3 +157,20 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
} }
return AppLanguage.en 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
}
+2 -1
View File
@@ -2,6 +2,7 @@ import {Platform} from 'react-native'
import {isReducedMotion} from 'react-native-reanimated' import {isReducedMotion} from 'react-native-reanimated'
import {getLocales} from 'expo-localization' import {getLocales} from 'expo-localization'
import {fixLegacyLanguageCode} from '#/locale/helpers'
import {dedupArray} from 'lib/functions' import {dedupArray} from 'lib/functions'
export const isIOS = Platform.OS === 'ios' export const isIOS = Platform.OS === 'ios'
@@ -17,7 +18,7 @@ export const isMobileWeb =
export const deviceLocales = dedupArray( export const deviceLocales = dedupArray(
getLocales?.() getLocales?.()
.map?.(locale => locale.languageCode) .map?.(locale => fixLegacyLanguageCode(locale.languageCode))
.filter(code => typeof code === 'string'), .filter(code => typeof code === 'string'),
) as string[] ) as string[]
+14 -1
View File
@@ -1,4 +1,5 @@
import {Linking} from 'react-native' import {GestureResponderEvent, Linking} from 'react-native'
import {isNative, isWeb} from './detection' import {isNative, isWeb} from './detection'
export async function getInitialURL(): Promise<string | undefined> { export async function getInitialURL(): Promise<string | undefined> {
@@ -23,3 +24,15 @@ export function clearHash() {
window.location.hash = '' 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
}
+1 -1
View File
@@ -56,7 +56,7 @@ export const ProfileFeedSection = React.forwardRef<
})) }))
const renderPostsEmpty = React.useCallback(() => { const renderPostsEmpty = React.useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> return <EmptyState icon="growth" message={_(msg`No posts yet.`)} />
}, [_]) }, [_])
React.useEffect(() => { React.useEffect(() => {
+6
View File
@@ -13,7 +13,13 @@ export async function write(value: Schema) {
export async function read(): Promise<Schema | undefined> { export async function read(): Promise<Schema | undefined> {
const rawData = await AsyncStorage.getItem(BSKY_STORAGE) const rawData = await AsyncStorage.getItem(BSKY_STORAGE)
const objData = rawData ? JSON.parse(rawData) : undefined const objData = rawData ? JSON.parse(rawData) : undefined
// new user
if (!objData) return undefined
// existing user, validate
const parsed = schema.safeParse(objData) const parsed = schema.safeParse(objData)
if (parsed.success) { if (parsed.success) {
return objData return objData
} else { } else {
@@ -16,6 +16,7 @@ import {
useInfiniteQuery, useInfiniteQuery,
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import throttle from 'lodash.throttle'
import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useCurrentConvoId} from '#/state/messages/current-convo-id'
import {useMessagesEventBus} from '#/state/messages/events' import {useMessagesEventBus} from '#/state/messages/events'
@@ -89,6 +90,15 @@ export function ListConvosProviderInner({
const {currentConvoId} = useCurrentConvoId() const {currentConvoId} = useCurrentConvoId()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const debouncedRefetch = useMemo(
() =>
throttle(refetch, 500, {
leading: true,
trailing: true,
}),
[refetch],
)
useEffect(() => { useEffect(() => {
const unsub = messagesBus.on( const unsub = messagesBus.on(
events => { events => {
@@ -96,7 +106,7 @@ export function ListConvosProviderInner({
events.logs.forEach(log => { events.logs.forEach(log => {
if (ChatBskyConvoDefs.isLogBeginConvo(log)) { if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
refetch() debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) { } else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) =>
optimisticDelete(log.convoId, old), optimisticDelete(log.convoId, old),
@@ -170,7 +180,7 @@ export function ListConvosProviderInner({
}), }),
} }
} else { } else {
refetch() debouncedRefetch()
} }
}) })
} }
@@ -183,7 +193,14 @@ export function ListConvosProviderInner({
) )
return () => unsub() return () => unsub()
}, [messagesBus, currentConvoId, refetch, queryClient, currentAccount?.did]) }, [
messagesBus,
currentConvoId,
refetch,
queryClient,
currentAccount?.did,
debouncedRefetch,
])
const ctx = useMemo(() => { const ctx = useMemo(() => {
return data?.pages.flatMap(page => page.convos) ?? [] return data?.pages.flatMap(page => page.convos) ?? []
+10 -3
View File
@@ -305,7 +305,13 @@ export const ComposePost = observer(function ComposePost({
localThumb: undefined, localThumb: undefined,
} as apilib.ExternalEmbedDraft) } 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) setIsProcessing(false)
return return
} finally { } finally {
@@ -785,11 +791,12 @@ const styles = StyleSheet.create({
}, },
errorLine: { errorLine: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.red1, backgroundColor: colors.red1,
borderRadius: 6, borderRadius: 6,
marginHorizontal: 16, marginHorizontal: 16,
paddingHorizontal: 8, paddingHorizontal: 12,
paddingVertical: 6, paddingVertical: 10,
marginBottom: 8, marginBottom: 8,
}, },
reminderLine: { reminderLine: {
+31 -10
View File
@@ -1,5 +1,12 @@
import React from 'react' 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 {AtUri} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Plural, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
@@ -26,6 +33,7 @@ import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth import hairlineWidth = StyleSheet.hairlineWidth
import {shouldClickOpenNewTab} from '#/platform/urls'
export function FeedSourceCard({ export function FeedSourceCard({
feedUri, feedUri,
@@ -203,17 +211,30 @@ export function FeedSourceCardLoaded({
style, style,
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth}, {borderTopWidth: hideTopBorder ? 0 : hairlineWidth},
]} ]}
onPress={() => { onPress={e => {
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
if (feed.type === 'feed') { if (feed.type === 'feed') {
navigation.push('ProfileFeed', { if (shouldOpenInNewTab) {
name: feed.creatorDid, Linking.openURL(
rkey: new AtUri(feed.uri).rkey, `/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') { } else if (feed.type === 'list') {
navigation.push('ProfileList', { if (shouldOpenInNewTab) {
name: feed.creatorDid, Linking.openURL(
rkey: new AtUri(feed.uri).rkey, `/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}> key={feed.uri}>
+2 -2
View File
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
import {CogIcon} from '#/lib/icons' import {CogIcon} from '#/lib/icons'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout' import {useShellLayout} from '#/state/shell/shell-layout'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
@@ -39,7 +39,7 @@ function HomeHeaderLayoutDesktopAndTablet({
tabBarAnchor: JSX.Element | null | undefined tabBarAnchor: JSX.Element | null | undefined
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {headerMinimalShellTransform} = useMinimalShellMode() const headerMinimalShellTransform = useMinimalShellHeaderTransform()
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {hasSession} = useSession() const {hasSession} = useSession()
const {_} = useLingui() const {_} = useLingui()
+2 -2
View File
@@ -10,7 +10,7 @@ import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useShellLayout} from '#/state/shell/shell-layout' import {useShellLayout} from '#/state/shell/shell-layout'
import {HITSLOP_10} from 'lib/constants' import {HITSLOP_10} from 'lib/constants'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
@@ -30,7 +30,7 @@ export function HomeHeaderLayoutMobile({
const {_} = useLingui() const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {headerMinimalShellTransform} = useMinimalShellMode() const headerMinimalShellTransform = useMinimalShellHeaderTransform()
const {hasSession} = useSession() const {hasSession} = useSession()
const onPressAvi = React.useCallback(() => { const onPressAvi = React.useCallback(() => {
+2 -3
View File
@@ -49,8 +49,7 @@ export function FeedErrorMessage({
if ( if (
typeof knownError !== 'undefined' && typeof knownError !== 'undefined' &&
knownError !== KnownError.Unknown && knownError !== KnownError.Unknown &&
(savedFeedConfig?.type === 'feed' || feedDesc.startsWith('feedgen')
knownError === KnownError.FeedNSFPublic)
) { ) {
return ( return (
<FeedgenErrorMessage <FeedgenErrorMessage
@@ -272,7 +271,7 @@ function detectKnownError(
) { ) {
return KnownError.FeedgenMisconfigured return KnownError.FeedgenMisconfigured
} }
if (error.includes('feed provided an invalid response')) { if (error.includes('invalid response')) {
return KnownError.FeedgenBadResponse return KnownError.FeedgenBadResponse
} }
return KnownError.FeedgenUnknown return KnownError.FeedgenUnknown
+37 -18
View File
@@ -5,10 +5,13 @@ import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {Text} from './text/Text'
import {UserGroupIcon} from 'lib/icons' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {UserGroupIcon} from 'lib/icons'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
import {Text} from './text/Text'
export function EmptyState({ export function EmptyState({
testID, testID,
@@ -17,32 +20,41 @@ export function EmptyState({
style, style,
}: { }: {
testID?: string testID?: string
icon: IconProp | 'user-group' icon: IconProp | 'user-group' | 'growth'
message: string message: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {isTabletOrDesktop} = useWebMediaQueries()
const iconSize = isTabletOrDesktop ? 80 : 64
return ( return (
<View <View
testID={testID} testID={testID}
style={[styles.container, isWeb && pal.border, style]}> style={[
<View style={styles.iconContainer}> styles.container,
isWeb && pal.border,
isTabletOrDesktop && {paddingRight: 20},
style,
]}>
<View
style={[
styles.iconContainer,
isTabletOrDesktop && styles.iconContainerBig,
pal.viewLight,
]}>
{icon === 'user-group' ? ( {icon === 'user-group' ? (
<UserGroupIcon size="64" style={styles.icon} /> <UserGroupIcon size={iconSize} />
) : icon === 'growth' ? (
<Growth width={iconSize} fill={pal.colors.emptyStateIcon} />
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon
icon={icon} icon={icon}
size={64} size={iconSize}
style={[ style={[{color: pal.colors.emptyStateIcon} as FontAwesomeIconStyle]}
styles.icon,
{color: pal.colors.emptyStateIcon} as FontAwesomeIconStyle,
]}
/> />
)} )}
</View> </View>
<Text <Text type="xl" style={[{color: pal.colors.textLight}, styles.text]}>
type="xl-medium"
style={[{color: pal.colors.textVeryLight}, styles.text]}>
{message} {message}
</Text> </Text>
</View> </View>
@@ -51,16 +63,23 @@ export function EmptyState({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingVertical: 24,
paddingHorizontal: 36,
borderTopWidth: isWeb ? 1 : undefined, borderTopWidth: isWeb ? 1 : undefined,
}, },
iconContainer: { iconContainer: {
flexDirection: 'row', flexDirection: 'row',
}, alignItems: 'center',
icon: { justifyContent: 'center',
height: 100,
width: 100,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
borderRadius: 80,
marginTop: 30,
},
iconContainerBig: {
width: 140,
height: 140,
marginTop: 50,
}, },
text: { text: {
textAlign: 'center', textAlign: 'center',
+2 -2
View File
@@ -8,7 +8,7 @@ import {useNavigation} from '@react-navigation/native'
import {useSetDrawerOpen} from '#/state/shell' import {useSetDrawerOpen} from '#/state/shell'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
@@ -197,7 +197,7 @@ function Container({
showBorder?: boolean showBorder?: boolean
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {headerMinimalShellTransform} = useMinimalShellMode() const headerMinimalShellTransform = useMinimalShellHeaderTransform()
if (!hideOnScroll) { if (!hideOnScroll) {
return ( return (
+2 -2
View File
@@ -4,7 +4,7 @@ import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
import {gradients} from '#/lib/styles' import {gradients} from '#/lib/styles'
@@ -20,7 +20,7 @@ export interface FABProps
export function FABInner({testID, icon, ...props}: FABProps) { export function FABInner({testID, icon, ...props}: FABProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const {isMobile, isTablet} = useWebMediaQueries() const {isMobile, isTablet} = useWebMediaQueries()
const {fabMinimalShellTransform} = useMinimalShellMode() const fabMinimalShellTransform = useMinimalShellFabTransform()
const { const {
state: pressed, state: pressed,
onIn: onPressIn, onIn: onPressIn,
@@ -6,7 +6,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useMediaQuery} from 'react-responsive' import {useMediaQuery} from 'react-responsive'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
@@ -30,7 +30,7 @@ export function LoadLatestBtn({
const pal = usePalette('default') const pal = usePalette('default')
const {hasSession} = useSession() const {hasSession} = useSession()
const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries() const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries()
const {fabMinimalShellTransform} = useMinimalShellMode() const fabMinimalShellTransform = useMinimalShellFabTransform()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
// move button inline if it starts overlapping the left nav // move button inline if it starts overlapping the left nav
@@ -3,6 +3,7 @@ import {View} from 'react-native'
import {msg, plural} from '@lingui/macro' import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -64,7 +65,8 @@ let RepostButton = ({
} (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`} } (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`}
shape="round" shape="round"
variant="ghost" variant="ghost"
color="secondary"> color="secondary"
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<Repost style={color} width={big ? 22 : 18} /> <Repost style={color} width={big ? 22 : 18} />
{typeof repostCount !== 'undefined' && repostCount > 0 ? ( {typeof repostCount !== 'undefined' && repostCount > 0 ? (
<Text <Text
+6
View File
@@ -20,6 +20,7 @@ import {
} from '#/state/shell' } from '#/state/shell'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates' import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {FeedPage} from 'view/com/feeds/FeedPage' import {FeedPage} from 'view/com/feeds/FeedPage'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
@@ -67,10 +68,15 @@ function HomeScreenReady({
const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed) const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed)
const selectedIndex = Math.max(0, maybeFoundIndex) const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex] const selectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission()
useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
useOTAUpdates() useOTAUpdates()
React.useEffect(() => {
requestNotificationsPermission('Home')
}, [requestNotificationsPermission])
const pagerRef = React.useRef<PagerRef>(null) const pagerRef = React.useRef<PagerRef>(null)
const lastPagerReportedIndexRef = React.useRef(selectedIndex) const lastPagerReportedIndexRef = React.useRef(selectedIndex)
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
+19 -3
View File
@@ -30,6 +30,7 @@ import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List' import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {FAB} from '../com/util/fab/FAB' import {FAB} from '../com/util/fab/FAB'
import {MainScrollProvider} from '../com/util/MainScrollProvider' import {MainScrollProvider} from '../com/util/MainScrollProvider'
@@ -43,6 +44,7 @@ export function NotificationsScreen({}: Props) {
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null) const scrollElRef = React.useRef<ListMethods>(null)
const {screen} = useAnalytics() const {screen} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
@@ -68,9 +70,13 @@ export function NotificationsScreen({}: Props) {
truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
} else { } else {
// check with the server // check with the server
unreadApi.checkUnread({invalidate: true}) setIsLoadingLatest(true)
unreadApi
.checkUnread({invalidate: true})
.catch(() => undefined)
.then(() => setIsLoadingLatest(false))
} }
}, [scrollToTop, queryClient, unreadApi, hasNew]) }, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest])
const onFocusCheckLatest = useNonReactiveCallback(() => { const onFocusCheckLatest = useNonReactiveCallback(() => {
// on focus, check for latest, but only invalidate if the user // on focus, check for latest, but only invalidate if the user
@@ -139,11 +145,20 @@ export function NotificationsScreen({}: Props) {
} }
onPress={emitSoftReset} onPress={emitSoftReset}
/> />
{isLoadingLatest ? <Loader size="md" /> : <></>}
</View> </View>
) )
} }
return <></> return <></>
}, [isDesktop, pal, hasNew]) }, [isDesktop, pal, hasNew, isLoadingLatest])
const renderHeaderSpinner = React.useCallback(() => {
return (
<View style={{width: 30, height: 20, alignItems: 'flex-end'}}>
{isLoadingLatest ? <Loader width={20} /> : <></>}
</View>
)
}, [isLoadingLatest])
return ( return (
<CenteredView <CenteredView
@@ -154,6 +169,7 @@ export function NotificationsScreen({}: Props) {
title={_(msg`Notifications`)} title={_(msg`Notifications`)}
canGoBack={false} canGoBack={false}
showBorder={true} showBorder={true}
renderButton={renderHeaderSpinner}
/> />
<MainScrollProvider> <MainScrollProvider>
<Feed <Feed
+2 -2
View File
@@ -14,7 +14,7 @@ import {
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useMinimalShellFabTransform} from 'lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
@@ -26,7 +26,7 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export function PostThreadScreen({route}: Props) { export function PostThreadScreen({route}: Props) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {hasSession} = useSession() const {hasSession} = useSession()
const {fabMinimalShellTransform} = useMinimalShellMode() const fabMinimalShellTransform = useMinimalShellFabTransform()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
+1 -1
View File
@@ -468,7 +468,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused]) }, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => { const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_]) }, [_])
return ( return (
+1 -1
View File
@@ -726,7 +726,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused]) }, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => { const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_]) }, [_])
return ( return (
+2 -2
View File
@@ -10,7 +10,7 @@ import {StackActions} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useDedupe} from '#/lib/hooks/useDedupe' import {useDedupe} from '#/lib/hooks/useDedupe'
import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform'
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
@@ -66,7 +66,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
useNavigationTabState() useNavigationTabState()
const numUnreadNotifications = useUnreadNotifications() const numUnreadNotifications = useUnreadNotifications()
const numUnreadMessages = useUnreadMessageCount() const numUnreadMessages = useUnreadMessageCount()
const {footerMinimalShellTransform} = useMinimalShellMode() const footerMinimalShellTransform = useMinimalShellFooterTransform()
const {data: profile} = useProfileQuery({did: currentAccount?.did}) const {data: profile} = useProfileQuery({did: currentAccount?.did})
const {requestSwitchToAccount} = useLoggedOutViewControls() const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
+16 -20
View File
@@ -6,7 +6,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigationState} from '@react-navigation/native' import {useNavigationState} from '@react-navigation/native'
import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
@@ -16,7 +16,6 @@ import {s} from '#/lib/styles'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {useGate} from 'lib/statsig/statsig'
import {useUnreadMessageCount} from 'state/queries/messages/list-converations' import {useUnreadMessageCount} from 'state/queries/messages/list-converations'
import {useUnreadNotifications} from 'state/queries/notifications/unread' import {useUnreadNotifications} from 'state/queries/notifications/unread'
import {Button} from '#/view/com/util/forms/Button' import {Button} from '#/view/com/util/forms/Button'
@@ -49,8 +48,7 @@ export function BottomBarWeb() {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const pal = usePalette('default') const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const gate = useGate() const footerMinimalShellTransform = useMinimalShellFooterTransform()
const {footerMinimalShellTransform} = useMinimalShellMode()
const {requestSwitchToAccount} = useLoggedOutViewControls() const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
const iconWidth = 26 const iconWidth = 26
@@ -115,14 +113,13 @@ export function BottomBarWeb() {
width={iconWidth - 1} width={iconWidth - 1}
style={[styles.ctrlIcon, pal.text, styles.messagesIcon]} style={[styles.ctrlIcon, pal.text, styles.messagesIcon]}
/> />
{unreadMessageCount.count > 0 && {unreadMessageCount.count > 0 && (
gate('show_notification_badge_mobile_web') && ( <View style={styles.notificationCount}>
<View style={styles.notificationCount}> <Text style={styles.notificationCountLabel}>
<Text style={styles.notificationCountLabel}> {unreadMessageCount.numUnread}
{unreadMessageCount.numUnread} </Text>
</Text> </View>
</View> )}
)}
</> </>
) )
}} }}
@@ -136,14 +133,13 @@ export function BottomBarWeb() {
width={iconWidth} width={iconWidth}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]} style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/> />
{notificationCountStr !== '' && {notificationCountStr !== '' && (
gate('show_notification_badge_mobile_web') && ( <View style={styles.notificationCount}>
<View style={styles.notificationCount}> <Text style={styles.notificationCountLabel}>
<Text style={styles.notificationCountLabel}> {notificationCountStr}
{notificationCountStr} </Text>
</Text> </View>
</View> )}
)}
</> </>
) )
}} }}