diff --git a/src/@types/react-navigation.d.ts b/src/@types/react-navigation.d.ts
index 33834f60..acf59127 100644
--- a/src/@types/react-navigation.d.ts
+++ b/src/@types/react-navigation.d.ts
@@ -58,9 +58,10 @@ declare namespace Nav {
imageUrls: {
id: Mastodon.Attachment['id']
url: Mastodon.AttachmentImage['url']
+ remote_url?: Mastodon.AttachmentImage['remote_url']
+ blurhash: Mastodon.AttachmentImage['blurhash']
width?: number
height?: number
- remote_url?: Mastodon.AttachmentImage['remote_url']
}[]
id: Mastodon.Attachment['id']
}
diff --git a/src/App.tsx b/src/App.tsx
index 2503520c..0ef71933 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,7 @@ import log from '@root/startup/log'
import netInfo from '@root/startup/netInfo'
import sentry from '@root/startup/sentry'
import { persistor, store } from '@root/store'
+import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
import ThemeManager from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
@@ -90,9 +91,11 @@ const App: React.FC = () => {
i18n.changeLanguage(getSettingsLanguage(store.getState()))
return (
-
-
-
+
+
+
+
+
)
} else {
diff --git a/src/components/GracefullyImage.tsx b/src/components/GracefullyImage.tsx
index 04ad5270..ed2deccd 100644
--- a/src/components/GracefullyImage.tsx
+++ b/src/components/GracefullyImage.tsx
@@ -102,11 +102,7 @@ const GracefullyImage = React.memo(
return (
{
+ const { reduceMotionEnabled } = useAccessibility()
+
const adaptiveFontsize = useSelector(getSettingsFontsize)
const adaptedFontsize = adaptiveScale(
StyleConstants.Font.Size[size],
@@ -69,9 +73,13 @@ const ParseEmojis = React.memo(
{/* When emoji starts a paragraph, lineHeight will break */}
{i === 0 ? : null}
-
diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx
index e19e874f..d789a08c 100644
--- a/src/components/Parse/HTML.tsx
+++ b/src/components/Parse/HTML.tsx
@@ -235,7 +235,7 @@ const ParseHTML = React.memo(
const [expanded, setExpanded] = useState(false)
const onTextLayout = useCallback(({ nativeEvent }) => {
- if (nativeEvent.lines.length >= numberOfLines) {
+ if (nativeEvent.lines.length >= numberOfLines + 5) {
setExpandAllow(true)
}
}, [])
@@ -245,7 +245,9 @@ const ParseHTML = React.memo(
{expandAllow ? (
= ({
})
}, [videoLoaded, videoPosition])
+ useEffect(() => {
+ if (gifv) {
+ videoPlayer.current?.setIsLoopingAsync(true)
+ }
+ }, [])
+
return (
= ({
height: '100%',
opacity: sensitiveShown ? 0 : 1
}}
- resizeMode='cover'
usePoster
- posterSource={{ uri: video.preview_url }}
- posterStyle={{ resizeMode: 'cover' }}
+ {...(gifv
+ ? { shouldPlay: true, source: { uri: video.url } }
+ : {
+ resizeMode: 'cover',
+ posterSource: { uri: video.preview_url },
+ posterStyle: { resizeMode: 'cover' }
+ })}
useNativeControls={false}
onFullscreenUpdate={event => {
if (event.fullscreenUpdate === 3) {
@@ -94,7 +104,7 @@ const AttachmentVideo: React.FC = ({
}}
/>
) : null
- ) : (
+ ) : !gifv ? (
)
diff --git a/src/components/Timeline/Shared/Avatar.tsx b/src/components/Timeline/Shared/Avatar.tsx
index 54f6b722..0372640d 100644
--- a/src/components/Timeline/Shared/Avatar.tsx
+++ b/src/components/Timeline/Shared/Avatar.tsx
@@ -33,9 +33,10 @@ const TimelineAvatar = React.memo(
height: StyleConstants.Avatar.M
}}
style={{
- borderRadius: 4,
+ borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden',
- marginRight: StyleConstants.Spacing.S
+ marginRight: StyleConstants.Spacing.S,
+ backgroundColor: 'red'
}}
/>
)
diff --git a/src/components/Timeline/Shared/Poll.tsx b/src/components/Timeline/Shared/Poll.tsx
index 52ae1d64..2f6ec37a 100644
--- a/src/components/Timeline/Shared/Poll.tsx
+++ b/src/components/Timeline/Shared/Poll.tsx
@@ -229,7 +229,7 @@ const TimelinePoll: React.FC = ({
style={styles.optionContainer}
onPress={() => {
analytics('timeline_shared_vote_option_press')
- haptics('Light')
+ !allOptions[index] && haptics('Light')
if (poll.multiple) {
setAllOptions(allOptions.map((o, i) => (i === index ? !o : o)))
} else {
diff --git a/src/screens/Announcements.tsx b/src/screens/Announcements.tsx
index 555b539f..b25b952d 100644
--- a/src/screens/Announcements.tsx
+++ b/src/screens/Announcements.tsx
@@ -6,6 +6,7 @@ import { ParseHTML } from '@components/Parse'
import RelativeTime from '@components/RelativeTime'
import { BlurView } from '@react-native-community/blur'
import { StackScreenProps } from '@react-navigation/stack'
+import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import {
useAnnouncementMutation,
useAnnouncementQuery
@@ -14,15 +15,9 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
-import {
- Dimensions,
- Image,
- Pressable,
- StyleSheet,
- Text,
- View
-} from 'react-native'
+import { Dimensions, Pressable, StyleSheet, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
+import FastImage from 'react-native-fast-image'
import { FlatList, ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context'
@@ -37,6 +32,7 @@ const ScreenAnnouncements: React.FC = ({
},
navigation
}) => {
+ const { reduceMotionEnabled } = useAccessibility()
const { mode, theme } = useTheme()
const [index, setIndex] = useState(0)
const { t } = useTranslation('sharedAnnouncements')
@@ -102,7 +98,9 @@ const ScreenAnnouncements: React.FC = ({
style={[
styles.reaction,
{
- borderColor: reaction.me ? theme.disabled : theme.primaryDefault,
+ borderColor: reaction.me
+ ? theme.disabled
+ : theme.primaryDefault,
backgroundColor: reaction.me
? theme.disabled
: theme.backgroundDefault
@@ -121,8 +119,12 @@ const ScreenAnnouncements: React.FC = ({
}}
>
{reaction.url ? (
-
) : (
@@ -130,7 +132,10 @@ const ScreenAnnouncements: React.FC = ({
)}
{reaction.count ? (
{reaction.count}
@@ -246,7 +251,8 @@ const ScreenAnnouncements: React.FC = ({
styles.indicator,
{
borderColor: theme.primaryDefault,
- backgroundColor: i === index ? theme.primaryDefault : undefined,
+ backgroundColor:
+ i === index ? theme.primaryDefault : undefined,
marginLeft:
i === query.data.length ? 0 : StyleConstants.Spacing.S
}
diff --git a/src/screens/Compose/Root.tsx b/src/screens/Compose/Root.tsx
index 8ed0369a..fea1bff6 100644
--- a/src/screens/Compose/Root.tsx
+++ b/src/screens/Compose/Root.tsx
@@ -4,14 +4,8 @@ import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { forEach, groupBy, sortBy } from 'lodash'
-import React, {
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useRef
-} from 'react'
-import { FlatList, Image, StyleSheet, View } from 'react-native'
+import React, { useCallback, useContext, useEffect, useMemo } from 'react'
+import { FlatList, StyleSheet, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import ComposeActions from './Root/Actions'
import ComposePosting from './Posting'
@@ -20,24 +14,32 @@ import ComposeRootHeader from './Root/Header'
import ComposeRootSuggestion from './Root/Suggestion'
import ComposeContext from './utils/createContext'
import ComposeDrafts from './Root/Drafts'
+import FastImage from 'react-native-fast-image'
+import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
const prefetchEmojis = (
- sortedEmojis: { title: string; data: Mastodon.Emoji[] }[]
+ sortedEmojis: { title: string; data: Mastodon.Emoji[] }[],
+ reduceMotionEnabled: boolean
) => {
+ const prefetches: { uri: string }[] = []
let requestedIndex = 0
sortedEmojis.forEach(sorted => {
sorted.data.forEach(emoji => {
if (requestedIndex > 40) {
return
}
- Image.prefetch(emoji.url)
+ prefetches.push({
+ uri: reduceMotionEnabled ? emoji.static_url : emoji.url
+ })
requestedIndex++
})
})
+ FastImage.preload(prefetches)
}
const ComposeRoot = React.memo(
() => {
+ const { reduceMotionEnabled } = useAccessibility()
const { theme } = useTheme()
const { composeState, composeDispatch } = useContext(ComposeContext)
@@ -74,9 +76,9 @@ const ComposeRoot = React.memo(
type: 'emoji',
payload: { ...composeState.emoji, emojis: sortedEmojis }
})
- prefetchEmojis(sortedEmojis)
+ prefetchEmojis(sortedEmojis, reduceMotionEnabled)
}
- }, [emojisData])
+ }, [emojisData, reduceMotionEnabled])
const listEmpty = useMemo(() => {
if (isFetching) {
diff --git a/src/screens/Compose/Root/Footer/Emojis.tsx b/src/screens/Compose/Root/Footer/Emojis.tsx
index 807370a8..0be2886b 100644
--- a/src/screens/Compose/Root/Footer/Emojis.tsx
+++ b/src/screens/Compose/Root/Footer/Emojis.tsx
@@ -3,18 +3,15 @@ import haptics from '@components/haptics'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useMemo } from 'react'
-import {
- Image,
- Pressable,
- SectionList,
- StyleSheet,
- Text,
- View
-} from 'react-native'
+import { Pressable, SectionList, StyleSheet, Text, View } from 'react-native'
import ComposeContext from '../../utils/createContext'
import updateText from '../../updateText'
+import FastImage from 'react-native-fast-image'
+import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
+ const { reduceMotionEnabled } = useAccessibility()
+
const { composeState, composeDispatch } = useContext(ComposeContext)
const onPress = useCallback(() => {
analytics('compose_emoji_add')
@@ -32,8 +29,8 @@ const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
}, [composeState])
const children = useMemo(
() => (
-
),
@@ -81,6 +78,7 @@ const ComposeEmojis: React.FC = () => {
keyExtractor={item => item.shortcode}
renderSectionHeader={listHeader}
renderItem={listItem}
+ windowSize={3}
/>
)
diff --git a/src/screens/ImageViewer/components/ImageItem.android.tsx b/src/screens/ImageViewer/components/ImageItem.android.tsx
index 20a611af..069dc494 100644
--- a/src/screens/ImageViewer/components/ImageItem.android.tsx
+++ b/src/screens/ImageViewer/components/ImageItem.android.tsx
@@ -93,6 +93,7 @@ const ImageItem = ({
original: imageSrc.url,
remote: imageSrc.remote_url
}}
+ blurhash={imageSrc.blurhash}
{...((!imageSrc.width || !imageSrc.height) && {
setImageDimensions
})}
diff --git a/src/screens/ImageViewer/components/ImageItem.ios.tsx b/src/screens/ImageViewer/components/ImageItem.ios.tsx
index b21f7aa2..92b3486c 100644
--- a/src/screens/ImageViewer/components/ImageItem.ios.tsx
+++ b/src/screens/ImageViewer/components/ImageItem.ios.tsx
@@ -148,6 +148,7 @@ const ImageItem = ({
original: imageSrc.url,
remote: imageSrc.remote_url
}}
+ blurhash={imageSrc.blurhash}
{...((!imageSrc.width || !imageSrc.height) && {
setImageDimensions
})}
diff --git a/src/screens/Tabs/Shared/Account/Header.tsx b/src/screens/Tabs/Shared/Account/Header.tsx
index 48f3ee40..d82cfb27 100644
--- a/src/screens/Tabs/Shared/Account/Header.tsx
+++ b/src/screens/Tabs/Shared/Account/Header.tsx
@@ -1,3 +1,4 @@
+import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { Dimensions, Image } from 'react-native'
@@ -9,14 +10,17 @@ export interface Props {
limitHeight?: boolean
}
-const AccountHeader: React.FC = ({ account, limitHeight = false }) => {
+const AccountHeader: React.FC = ({ account }) => {
const { accountState } = useContext(AccountContext)
+ const { reduceMotionEnabled } = useAccessibility()
const { theme } = useTheme()
const topInset = useSafeAreaInsets().top
return (
= ({ account, myInfo }) => {
const navigation = useNavigation<
StackNavigationProp
>()
+ const { reduceMotionEnabled } = useAccessibility()
return (
= ({ account, myInfo }) => {
)
diff --git a/src/utils/accessibility/AccessibilityManager.tsx b/src/utils/accessibility/AccessibilityManager.tsx
new file mode 100644
index 00000000..510cf7c9
--- /dev/null
+++ b/src/utils/accessibility/AccessibilityManager.tsx
@@ -0,0 +1,52 @@
+import React, { createContext, useContext, useEffect, useState } from 'react'
+import { AccessibilityInfo } from 'react-native'
+
+type ContextType = {
+ reduceMotionEnabled: boolean
+}
+
+const AccessibilityContext = createContext({
+ reduceMotionEnabled: false
+})
+
+export const useAccessibility = () => useContext(AccessibilityContext)
+
+const AccessibilityManager: React.FC = ({ children }) => {
+ const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false)
+
+ const handleReduceMotionChanged = (reduceMotionEnabled: boolean) =>
+ setReduceMotionEnabled(reduceMotionEnabled)
+
+ const loadReduceMotion = async () => {
+ const reduceMotion = await AccessibilityInfo.isReduceMotionEnabled()
+ setReduceMotionEnabled(reduceMotion)
+ }
+
+ useEffect(() => {
+ loadReduceMotion()
+
+ AccessibilityInfo.addEventListener(
+ 'reduceMotionChanged',
+ handleReduceMotionChanged
+ )
+
+ return () => {
+ AccessibilityInfo.removeEventListener(
+ 'reduceMotionChanged',
+ handleReduceMotionChanged
+ )
+ }
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default AccessibilityManager
diff --git a/src/utils/styles/ThemeManager.tsx b/src/utils/styles/ThemeManager.tsx
index eaead5d3..4a739d99 100644
--- a/src/utils/styles/ThemeManager.tsx
+++ b/src/utils/styles/ThemeManager.tsx
@@ -11,7 +11,7 @@ type ContextType = {
setTheme: (theme: 'light' | 'dark') => void
}
-export const ManageThemeContext = createContext({
+const ManageThemeContext = createContext({
mode: 'light',
theme: getTheme('light'),
setTheme: () => {}