Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41243e8740 | |||
| 12f9dacb06 | |||
| b888fe1fcd | |||
| 74c692a5a1 | |||
| 4e8b9c1da6 | |||
| e1bf717e9d | |||
| 8fa926e113 |
+7
-2
@@ -10,9 +10,11 @@ import {
|
||||
SafeAreaProvider,
|
||||
} from 'react-native-safe-area-context'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {
|
||||
initialize,
|
||||
@@ -23,6 +25,7 @@ import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
@@ -65,13 +68,15 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {AppProfiler} from '#/AppProfiler'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {AppProfiler} from './AppProfiler'
|
||||
import {KeyboardControllerProvider} from './lib/hooks/useEnableKeyboardController'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
if (isIOS) {
|
||||
SystemUI.setBackgroundColorAsync('black')
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import React from 'react'
|
||||
import {formatDistance, Locale} from 'date-fns'
|
||||
import {
|
||||
ast,
|
||||
ca,
|
||||
de,
|
||||
enGB,
|
||||
@@ -45,7 +44,7 @@ import {useLanguagePrefs} from '#/state/preferences'
|
||||
*/
|
||||
const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
en: undefined,
|
||||
ast,
|
||||
ast: undefined,
|
||||
an: undefined,
|
||||
ca,
|
||||
de,
|
||||
|
||||
@@ -43,7 +43,7 @@ interface AppLanguageConfig {
|
||||
export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.en, name: 'English'},
|
||||
{code2: AppLanguage.an, name: 'Aragonés – Aragonese'},
|
||||
{code3: AppLanguage.ast, name: 'Asturianu - Asturian'},
|
||||
{code2: AppLanguage.ast, name: 'Asturianu - Asturian'},
|
||||
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
||||
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
||||
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
||||
|
||||
@@ -14,7 +14,6 @@ import {Lab_Stroke2_Corner0_Rounded as Lab} from '#/components/icons/Lab'
|
||||
import {Leaf_Stroke2_Corner0_Rounded as Leaf} from '#/components/icons/Leaf'
|
||||
import {MusicNote_Stroke2_Corner0_Rounded as MusicNote} from '#/components/icons/MusicNote'
|
||||
import {PiggyBank_Stroke2_Corner0_Rounded as PiggyBank} from '#/components/icons/PiggyBank'
|
||||
import {Pizza_Stroke2_Corner0_Rounded as Pizza} from '#/components/icons/Pizza'
|
||||
import {Poop_Stroke2_Corner0_Rounded as Poop} from '#/components/icons/Poop'
|
||||
import {Rose_Stroke2_Corner0_Rounded as Rose} from '#/components/icons/Rose'
|
||||
import {Shaka_Stroke2_Corner0_Rounded as Shaka} from '#/components/icons/Shaka'
|
||||
@@ -38,7 +37,6 @@ export const emojiNames = [
|
||||
'gameController',
|
||||
'leaf',
|
||||
'musicNote',
|
||||
'pizza',
|
||||
'rose',
|
||||
'shaka',
|
||||
'ufo',
|
||||
@@ -99,10 +97,6 @@ export const emojiItems: Record<EmojiName, Emoji> = {
|
||||
name: 'musicNote',
|
||||
component: MusicNote,
|
||||
},
|
||||
pizza: {
|
||||
name: 'pizza',
|
||||
component: Pizza,
|
||||
},
|
||||
rose: {
|
||||
name: 'rose',
|
||||
component: Rose,
|
||||
|
||||
@@ -31,7 +31,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
const openLightbox = useNonReactiveCallback(
|
||||
(lightbox: Omit<Lightbox, 'id'>) => {
|
||||
setActiveLightbox({...lightbox, id: nanoid()})
|
||||
setActiveLightbox(prevLightbox => {
|
||||
if (prevLightbox) {
|
||||
// Ignore duplicate open requests. If it's already open,
|
||||
// the user has to explicitly close the previous one first.
|
||||
return prevLightbox
|
||||
} else {
|
||||
return {...lightbox, id: nanoid()}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {AppBskyFeedPostgate, RichText} from '@atproto/api'
|
||||
import {AppBskyFeedPostgate, AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {SelfLabel} from '#/lib/moderation'
|
||||
@@ -16,6 +16,10 @@ import {Gif} from '#/state/queries/tenor'
|
||||
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate'
|
||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
import {
|
||||
LinkFacetMatch,
|
||||
suggestLinkCardUri,
|
||||
} from '#/view/com/composer/text-input/text-input-util'
|
||||
import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
|
||||
|
||||
type ImagesMedia = {
|
||||
@@ -508,6 +512,68 @@ export function createComposerState({
|
||||
)
|
||||
: '',
|
||||
})
|
||||
|
||||
let link: Link | undefined
|
||||
|
||||
/**
|
||||
* `initText` atm is only used for compose intents, meaning share links from
|
||||
* external sources. If `initText` is defined, we want to extract links/posts
|
||||
* from `initText` and suggest them as embeds.
|
||||
*
|
||||
* This checks for posts separately from other types of links so that posts
|
||||
* can become quotes. The util `suggestLinkCardUri` is then applied to ensure
|
||||
* we suggest at most 1 of each.
|
||||
*/
|
||||
if (initText) {
|
||||
initRichText.detectFacetsWithoutResolution()
|
||||
const detectedExtUris = new Map<string, LinkFacetMatch>()
|
||||
const detectedPostUris = new Map<string, LinkFacetMatch>()
|
||||
if (initRichText.facets) {
|
||||
for (const facet of initRichText.facets) {
|
||||
for (const feature of facet.features) {
|
||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||
if (isBskyPostUrl(feature.uri)) {
|
||||
detectedPostUris.set(feature.uri, {facet, rt: initRichText})
|
||||
} else {
|
||||
detectedExtUris.set(feature.uri, {facet, rt: initRichText})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const pastSuggestedUris = new Set<string>()
|
||||
const suggestedExtUri = suggestLinkCardUri(
|
||||
true,
|
||||
detectedExtUris,
|
||||
new Map(),
|
||||
pastSuggestedUris,
|
||||
)
|
||||
if (suggestedExtUri) {
|
||||
link = {
|
||||
type: 'link',
|
||||
uri: suggestedExtUri,
|
||||
}
|
||||
}
|
||||
const suggestedPostUri = suggestLinkCardUri(
|
||||
true,
|
||||
detectedPostUris,
|
||||
new Map(),
|
||||
pastSuggestedUris,
|
||||
)
|
||||
if (suggestedPostUri) {
|
||||
/*
|
||||
* `initQuote` is only populated via in-app user action, but we're being
|
||||
* future-defensive here.
|
||||
*/
|
||||
if (!quote) {
|
||||
quote = {
|
||||
type: 'link',
|
||||
uri: suggestedPostUri,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activePostIndex: 0,
|
||||
mutableNeedsFocusActive: false,
|
||||
@@ -521,7 +587,7 @@ export function createComposerState({
|
||||
embed: {
|
||||
quote,
|
||||
media,
|
||||
link: undefined,
|
||||
link,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -257,6 +257,10 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
minHeight: 60,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
{
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
]}
|
||||
{...props}>
|
||||
{textDecorated}
|
||||
|
||||
@@ -6,7 +6,7 @@ export type LinkFacetMatch = {
|
||||
}
|
||||
|
||||
export function suggestLinkCardUri(
|
||||
mayBePaste: boolean,
|
||||
suggestLinkImmediately: boolean,
|
||||
nextDetectedUris: Map<string, LinkFacetMatch>,
|
||||
prevDetectedUris: Map<string, LinkFacetMatch>,
|
||||
pastSuggestedUris: Set<string>,
|
||||
@@ -20,8 +20,8 @@ export function suggestLinkCardUri(
|
||||
// Don't suggest already added or already dismissed link cards.
|
||||
continue
|
||||
}
|
||||
if (mayBePaste) {
|
||||
// Immediately add the pasted link without waiting to type more.
|
||||
if (suggestLinkImmediately) {
|
||||
// Immediately add the pasted or intent-prefilled link without waiting to type more.
|
||||
suggestedUris.add(uri)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const POLL_FREQ = 60e3 // 60sec
|
||||
export function FeedPage({
|
||||
testID,
|
||||
isPageFocused,
|
||||
isPageAdjacent,
|
||||
feed,
|
||||
feedParams,
|
||||
renderEmptyState,
|
||||
@@ -42,6 +43,7 @@ export function FeedPage({
|
||||
feed: FeedDescriptor
|
||||
feedParams?: FeedParams
|
||||
isPageFocused: boolean
|
||||
isPageAdjacent: boolean
|
||||
renderEmptyState: () => JSX.Element
|
||||
renderEndOfFeed?: () => JSX.Element
|
||||
savedFeedConfig?: AppBskyActorDefs.SavedFeed
|
||||
@@ -111,11 +113,11 @@ export function FeedPage({
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<Feed
|
||||
testID={testID ? `${testID}-feed` : undefined}
|
||||
enabled={isPageFocused}
|
||||
enabled={isPageFocused || isPageAdjacent}
|
||||
feed={feed}
|
||||
feedParams={feedParams}
|
||||
pollInterval={POLL_FREQ}
|
||||
disablePoll={hasNew}
|
||||
disablePoll={hasNew || !isPageFocused}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
onHasNew={setHasNew}
|
||||
|
||||
@@ -392,9 +392,9 @@ const ImageItem = ({
|
||||
<Animated.View style={imageCropStyle}>
|
||||
<Animated.View style={imageStyle}>
|
||||
<Image
|
||||
contentFit="cover"
|
||||
contentFit="contain"
|
||||
source={{uri: imageSrc.uri}}
|
||||
placeholderContentFit="cover"
|
||||
placeholderContentFit="contain"
|
||||
placeholder={{uri: imageSrc.thumbUri}}
|
||||
accessibilityLabel={imageSrc.alt}
|
||||
onLoad={
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// Original code copied and simplified from the link below as the codebase is currently not maintained:
|
||||
// https://github.com/jobtoday/react-native-image-viewing
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {
|
||||
LayoutAnimation,
|
||||
PixelRatio,
|
||||
@@ -79,6 +79,15 @@ const FAST_SPRING: WithSpringConfig = {
|
||||
restDisplacementThreshold: 0.01,
|
||||
}
|
||||
|
||||
function canAnimate(lightbox: Lightbox): boolean {
|
||||
return (
|
||||
!PlatformInfo.getIsReducedMotionEnabled() &&
|
||||
lightbox.images.every(
|
||||
img => img.thumbRect && (img.dimensions || img.thumbDimensions),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default function ImageViewRoot({
|
||||
lightbox: nextLightbox,
|
||||
onRequestClose,
|
||||
@@ -104,23 +113,19 @@ export default function ImageViewRoot({
|
||||
return
|
||||
}
|
||||
|
||||
const canAnimate =
|
||||
!PlatformInfo.getIsReducedMotionEnabled() &&
|
||||
nextLightbox.images.every(
|
||||
img => img.thumbRect && (img.dimensions || img.thumbDimensions),
|
||||
)
|
||||
const isAnimated = canAnimate(nextLightbox)
|
||||
|
||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||
requestAnimationFrame(() => {
|
||||
rAF_FIXED(() => {
|
||||
openProgress.set(() =>
|
||||
canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1,
|
||||
isAnimated ? withClampedSpring(1, SLOW_SPRING) : 1,
|
||||
)
|
||||
})
|
||||
return () => {
|
||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||
requestAnimationFrame(() => {
|
||||
rAF_FIXED(() => {
|
||||
openProgress.set(() =>
|
||||
canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0,
|
||||
isAnimated ? withClampedSpring(0, SLOW_SPRING) : 0,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -185,6 +190,7 @@ function ImageView({
|
||||
openProgress: SharedValue<number>
|
||||
}) {
|
||||
const {images, index: initialImageIndex} = lightbox
|
||||
const isAnimated = useMemo(() => canAnimate(lightbox), [lightbox])
|
||||
const [isScaled, setIsScaled] = useState(false)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [imageIndex, setImageIndex] = useState(initialImageIndex)
|
||||
@@ -194,10 +200,19 @@ function ImageView({
|
||||
const isFlyingAway = useSharedValue(false)
|
||||
|
||||
const containerStyle = useAnimatedStyle(() => {
|
||||
if (openProgress.get() < 1 || isFlyingAway.get()) {
|
||||
return {pointerEvents: 'none'}
|
||||
if (openProgress.get() < 1) {
|
||||
return {
|
||||
pointerEvents: 'none',
|
||||
opacity: isAnimated ? 1 : 0,
|
||||
}
|
||||
}
|
||||
return {pointerEvents: 'auto'}
|
||||
if (isFlyingAway.get()) {
|
||||
return {
|
||||
pointerEvents: 'none',
|
||||
opacity: 1,
|
||||
}
|
||||
}
|
||||
return {pointerEvents: 'auto', opacity: 1}
|
||||
})
|
||||
|
||||
const backdropStyle = useAnimatedStyle(() => {
|
||||
@@ -752,3 +767,32 @@ function withClampedSpring(value: any, config: WithSpringConfig) {
|
||||
'worklet'
|
||||
return withSpring(value, {...config, overshootClamping: true})
|
||||
}
|
||||
|
||||
// We have to do this because we can't trust RN's rAF to fire in order.
|
||||
// https://github.com/facebook/react-native/issues/48005
|
||||
let isFrameScheduled = false
|
||||
let pendingFrameCallbacks: Array<() => void> = []
|
||||
function rAF_FIXED(callback: () => void) {
|
||||
pendingFrameCallbacks.push(callback)
|
||||
if (!isFrameScheduled) {
|
||||
isFrameScheduled = true
|
||||
requestAnimationFrame(() => {
|
||||
const callbacks = pendingFrameCallbacks.slice()
|
||||
isFrameScheduled = false
|
||||
pendingFrameCallbacks = []
|
||||
let hasError = false
|
||||
let error
|
||||
for (let i = 0; i < callbacks.length; i++) {
|
||||
try {
|
||||
callbacks[i]()
|
||||
} catch (e) {
|
||||
hasError = true
|
||||
error = e
|
||||
}
|
||||
}
|
||||
if (hasError) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ let Feed = ({
|
||||
checkForNewRef.current = checkForNew
|
||||
}, [checkForNew])
|
||||
React.useEffect(() => {
|
||||
if (enabled) {
|
||||
if (enabled && !disablePoll) {
|
||||
const timeSinceFirstLoad = Date.now() - lastFetchRef.current
|
||||
// DISABLED need to check if this is causing random feed refreshes -prf
|
||||
/*if (timeSinceFirstLoad > REFRESH_AFTER) {
|
||||
@@ -231,7 +231,7 @@ let Feed = ({
|
||||
checkForNewRef.current()
|
||||
}
|
||||
}
|
||||
}, [enabled, feed, queryClient, scrollElRef])
|
||||
}, [enabled, disablePoll, feed, queryClient, scrollElRef])
|
||||
React.useEffect(() => {
|
||||
let cleanup1: () => void | undefined, cleanup2: () => void | undefined
|
||||
const subscription = AppState.addEventListener('change', nextAppState => {
|
||||
|
||||
@@ -85,10 +85,6 @@ export function AutoSizedImage({
|
||||
if (Number.isNaN(aspectRatio)) {
|
||||
aspectRatio = undefined
|
||||
}
|
||||
} else {
|
||||
// If we don't know it synchronously, treat it like a square.
|
||||
// We won't use fetched dimensions to avoid a layout shift.
|
||||
aspectRatio = 1
|
||||
}
|
||||
|
||||
let constrained: number | undefined
|
||||
@@ -103,11 +99,13 @@ export function AutoSizedImage({
|
||||
|
||||
const cropDisabled = crop === 'none'
|
||||
const isCropped = rawIsCropped && !cropDisabled
|
||||
const isContain = aspectRatio === undefined
|
||||
const hasAlt = !!image.alt
|
||||
|
||||
const contents = (
|
||||
<View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
||||
<Image
|
||||
contentFit={isContain ? 'contain' : 'cover'}
|
||||
style={[a.w_full, a.h_full]}
|
||||
source={image.thumb}
|
||||
accessible={true} // Must set for `accessibilityLabel` to work
|
||||
@@ -115,9 +113,11 @@ export function AutoSizedImage({
|
||||
accessibilityLabel={image.alt}
|
||||
accessibilityHint=""
|
||||
onLoad={e => {
|
||||
fetchedDimsRef.current = {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
if (!isContain) {
|
||||
fetchedDimsRef.current = {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -233,7 +233,7 @@ function HomeScreenReady({
|
||||
onPageScrollStateChanged={onPageScrollStateChanged}
|
||||
renderTabBar={renderTabBar}>
|
||||
{pinnedFeedInfos.length ? (
|
||||
pinnedFeedInfos.map(feedInfo => {
|
||||
pinnedFeedInfos.map((feedInfo, index) => {
|
||||
const feed = feedInfo.feedDescriptor
|
||||
if (feed === 'following') {
|
||||
return (
|
||||
@@ -241,6 +241,7 @@ function HomeScreenReady({
|
||||
key={feed}
|
||||
testID="followingFeedPage"
|
||||
isPageFocused={selectedFeed === feed}
|
||||
isPageAdjacent={Math.abs(selectedIndex - index) === 1}
|
||||
feed={feed}
|
||||
feedParams={homeFeedParams}
|
||||
renderEmptyState={renderFollowingEmptyState}
|
||||
@@ -254,6 +255,7 @@ function HomeScreenReady({
|
||||
key={feed}
|
||||
testID="customFeedPage"
|
||||
isPageFocused={selectedFeed === feed}
|
||||
isPageAdjacent={Math.abs(selectedIndex - index) === 1}
|
||||
feed={feed}
|
||||
renderEmptyState={renderCustomFeedEmptyState}
|
||||
savedFeedConfig={savedFeedConfig}
|
||||
@@ -273,6 +275,7 @@ function HomeScreenReady({
|
||||
<FeedPage
|
||||
testID="customFeedPage"
|
||||
isPageFocused
|
||||
isPageAdjacent={false}
|
||||
feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
|
||||
renderEmptyState={renderCustomFeedEmptyState}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user