Compare commits

...

7 Commits

Author SHA1 Message Date
Frudrax Cheng 41243e8740 Fix ast (#6774) 2024-12-07 00:59:12 +00:00
Eric Bailey 12f9dacb06 Immediately parse pre-filled links in composer state (#6974)
* Immediately parse pre-filled links in composer state

* Add hack to fix PasteInput height bug

* Parse out ext links separately from post links
2024-12-06 22:44:23 +00:00
dan b888fe1fcd Remove icon (#6994)
Co-authored-by: Eric Bailey <git@esb.lol>
2024-12-06 20:38:17 +00:00
dan 74c692a5a1 Prefetch adjacent Home pages (#6904) 2024-12-04 21:39:28 +00:00
dan 4e8b9c1da6 "Contain" images with missing dimensions instead of cropping them (#6828)
* Show unknown aspect as "contain" for autosize

* Fix a flash of wrong position when opening in lightbox

* Fix last frame flash on Android
2024-12-04 21:39:16 +00:00
dan e1bf717e9d Fix stuck lightbox (#6816)
* Fix lightbox getting stuck by fixing rAF order

If you spam opening lightbox too fast, the effect that calls rAF will clean up and set up again midflight. Unfortunately, due to rAF order being unreliable, it may fire in reverse order, causing "open, open, close" instead of "open, close, open", so it would get stuck closed. This fixes the rAF order.

* Don't allow opening another lightbox while it's open
2024-12-04 21:39:12 +00:00
Samuel Newman 8fa926e113 ensure black root view on iOS (#6813) 2024-12-04 21:39:06 +00:00
14 changed files with 169 additions and 44 deletions
+7 -2
View File
@@ -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
+1 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+9 -1
View File
@@ -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()}
}
})
},
)
+68 -2
View File
@@ -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
}
+4 -2
View File
@@ -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={
+57 -13
View File
@@ -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
}
})
}
}
+2 -2
View File
@@ -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 => {
+7 -7
View File
@@ -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,
}
}
}}
/>
+4 -1
View File
@@ -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}
/>