diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7b7f9a11..dda1c8c7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,11 +14,11 @@ name: "CodeQL" on: push: branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '35 4 * * 4' + # pull_request: + # # The branches below must be a subset of the branches above + # branches: [ main ] + # schedule: + # - cron: '35 4 * * 4' jobs: analyze: diff --git a/README.md b/README.md index 2420f572..c4c48d1b 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ ## Special thanks -@forenta for German translation +[@forenta](https://github.com/forenta) for German translation -@andrigamerita for Italian translation +[@andrigamerita](https://github.com/andrigamerita) for Italian translation -@hellojaccc for Korean translation +[@hellojaccc](https://github.com/hellojaccc) for Korean translation -@duy@mas.to for Vietnamese translation \ No newline at end of file +[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese + +[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation diff --git a/babel.config.js b/babel.config.js index 576c1c12..630526a3 100644 --- a/babel.config.js +++ b/babel.config.js @@ -29,7 +29,19 @@ module.exports = function (api) { } return { - presets: ['babel-preset-expo'], + presets: [ + 'babel-preset-expo', + [ + '@babel/preset-react', + { + importSource: '@welldone-software/why-did-you-render', + runtime: 'automatic', + development: + process.env.NODE_ENV === 'development' || + process.env.BABEL_ENV === 'development' + } + ] + ], plugins } } diff --git a/package.json b/package.json index 37f8acee..209e3464 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "native": "220508", "major": 4, "minor": 0, - "patch": 3, + "patch": 4, "expo": "45.0.0" }, "description": "tooot app for Mastodon", @@ -26,12 +26,12 @@ }, "dependencies": { "@expo/react-native-action-sheet": "3.13.0", - "@formatjs/intl-datetimeformat": "^5.0.2", - "@formatjs/intl-getcanonicallocales": "^1.9.2", - "@formatjs/intl-locale": "^2.4.47", - "@formatjs/intl-numberformat": "^7.4.3", - "@formatjs/intl-pluralrules": "^4.3.3", - "@formatjs/intl-relativetimeformat": "^10.0.1", + "@formatjs/intl-datetimeformat": "^6.0.1", + "@formatjs/intl-getcanonicallocales": "^2.0.1", + "@formatjs/intl-locale": "^3.0.1", + "@formatjs/intl-numberformat": "^8.0.1", + "@formatjs/intl-pluralrules": "^5.0.1", + "@formatjs/intl-relativetimeformat": "^11.0.1", "@neverdull-agency/expo-unlimited-secure-store": "1.0.10", "@react-native-async-storage/async-storage": "1.17.4", "@react-native-community/blur": "3.6.0", @@ -42,7 +42,7 @@ "@react-navigation/native": "6.0.10", "@react-navigation/native-stack": "6.6.2", "@react-navigation/stack": "6.2.1", - "@reduxjs/toolkit": "1.8.1", + "@reduxjs/toolkit": "1.8.2", "@sentry/react-native": "3.4.2", "@sharcoux/slider": "6.0.3", "axios": "0.27.2", @@ -68,13 +68,13 @@ "expo-updates": "0.13.1", "expo-video-thumbnails": "6.3.0", "expo-web-browser": "10.2.0", - "i18next": "21.8.2", + "i18next": "21.8.8", "li": "1.3.0", "lodash": "4.17.21", "react": "17.0.2", "react-dom": "17.0.2", - "react-i18next": "11.16.9", - "react-intl": "^5.25.1", + "react-i18next": "11.17.0", + "react-intl": "^6.0.3", "react-native": "0.68.2", "react-native-animated-spinkit": "1.5.2", "react-native-base64": "^0.2.1", @@ -93,8 +93,8 @@ "react-native-svg": "12.3.0", "react-native-swipe-list-view": "3.2.9", "react-native-tab-view": "3.1.1", - "react-query": "3.39.0", - "react-redux": "8.0.1", + "react-query": "3.39.1", + "react-redux": "8.0.2", "redux-persist": "6.0.0", "rn-placeholder": "3.0.3", "sentry-expo": "4.1.1", @@ -102,14 +102,15 @@ "valid-url": "1.0.9" }, "devDependencies": { - "@babel/core": "7.17.10", - "@babel/plugin-proposal-optional-chaining": "7.16.7", - "@babel/preset-typescript": "7.16.7", + "@babel/core": "7.18.2", + "@babel/plugin-proposal-optional-chaining": "7.17.12", + "@babel/preset-react": "^7.17.12", + "@babel/preset-typescript": "7.17.12", "@expo/config": "6.0.24", "@types/lodash": "4.14.182", "@types/react": "17.0.43", "@types/react-dom": "17.0.14", - "@types/react-native": "0.67.6", + "@types/react-native": "0.67.7", "@types/react-native-base64": "^0.2.0", "@types/react-native-share-menu": "^5.0.2", "@types/react-timeago": "4.1.3", @@ -122,7 +123,7 @@ "patch-package": "6.4.7", "postinstall-postinstall": "2.1.0", "react-native-clean-project": "4.0.1", - "typescript": "4.6.4" + "typescript": "4.7.3" }, "resolutions": { "@types/react": "17.0.43", @@ -150,4 +151,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 2e1663f7..fd7ce0d7 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -341,6 +341,7 @@ declare namespace Mastodon { | 'favourite' | 'poll' | 'status' + | 'update' created_at: string account: Account @@ -375,10 +376,12 @@ declare namespace Mastodon { endpoint: string alerts: { follow: boolean + follow_request: boolean favourite: boolean reblog: boolean mention: boolean poll: boolean + status: boolean } server_key: string } diff --git a/src/App.tsx b/src/App.tsx index bdf6569a..496f2c71 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,4 @@ import { ActionSheetProvider } from '@expo/react-native-action-sheet' -import '@formatjs/intl-getcanonicallocales/polyfill' -import '@formatjs/intl-locale/polyfill' -import '@formatjs/intl-pluralrules/polyfill' -import '@formatjs/intl-pluralrules/locale-data/de' -import '@formatjs/intl-pluralrules/locale-data/en' -import '@formatjs/intl-pluralrules/locale-data/ko' -import '@formatjs/intl-pluralrules/locale-data/vi' -import '@formatjs/intl-pluralrules/locale-data/zh' -import '@formatjs/intl-numberformat/polyfill' -import '@formatjs/intl-numberformat/locale-data/de' -import '@formatjs/intl-numberformat/locale-data/en' -import '@formatjs/intl-numberformat/locale-data/ko' -import '@formatjs/intl-numberformat/locale-data/vi' -import '@formatjs/intl-numberformat/locale-data/zh' -import '@formatjs/intl-datetimeformat/polyfill' -import '@formatjs/intl-datetimeformat/locale-data/de' -import '@formatjs/intl-datetimeformat/locale-data/en' -import '@formatjs/intl-datetimeformat/locale-data/ko' -import '@formatjs/intl-datetimeformat/locale-data/vi' -import '@formatjs/intl-datetimeformat/locale-data/zh' -import '@formatjs/intl-datetimeformat/add-all-tz' -import '@formatjs/intl-relativetimeformat/polyfill' -import '@formatjs/intl-relativetimeformat/locale-data/de' -import '@formatjs/intl-relativetimeformat/locale-data/en' -import '@formatjs/intl-relativetimeformat/locale-data/ko' -import '@formatjs/intl-relativetimeformat/locale-data/vi' -import '@formatjs/intl-relativetimeformat/locale-data/zh' import queryClient from '@helpers/queryClient' import i18n from '@root/i18n/i18n' import Screens from '@root/Screens' @@ -44,10 +17,9 @@ import { } from '@utils/slices/settingsSlice' import ThemeManager from '@utils/styles/ThemeManager' import 'expo-asset' -import * as Notifications from 'expo-notifications' import * as SplashScreen from 'expo-splash-screen' import React, { useCallback, useEffect, useState } from 'react' -import { AppState, LogBox, Platform } from 'react-native' +import { LogBox, Platform } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' import 'react-native-image-keyboard' import { enableFreeze } from 'react-native-screens' @@ -71,18 +43,6 @@ const App: React.FC = () => { log('log', 'App', 'rendering App') const [localCorrupt, setLocalCorrupt] = useState() - const appStateEffect = useCallback(() => { - Notifications.setBadgeCountAsync(0) - Notifications.dismissAllNotificationsAsync() - }, []) - useEffect(() => { - const appStateListener = AppState.addEventListener('change', appStateEffect) - - return () => { - appStateListener.remove() - } - }, []) - useEffect(() => { const delaySplash = async () => { log('log', 'App', 'delay splash') diff --git a/src/api/tooot.ts b/src/api/tooot.ts index db5c86af..b4264a76 100644 --- a/src/api/tooot.ts +++ b/src/api/tooot.ts @@ -19,7 +19,7 @@ export type Params = { export const TOOOT_API_DOMAIN = mapEnvironment({ release: 'api.tooot.app', - candidate: 'api.tooot.app', + candidate: 'api-candidate.tooot.app', development: 'api-development.tooot.app' }) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 579134b7..80049c5d 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -29,6 +29,7 @@ export interface Props { strokeWidth?: number size?: 'S' | 'M' | 'L' + fontBold?: boolean spacing?: 'XS' | 'S' | 'M' | 'L' round?: boolean overlay?: boolean @@ -48,6 +49,7 @@ const Button: React.FC = ({ disabled = false, strokeWidth, size = 'M', + fontBold = false, spacing = 'S', round = false, overlay = false, @@ -122,6 +124,7 @@ const Button: React.FC = ({ StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1), opacity: loading ? 0 : 1 }} + fontWeight={fontBold ? 'Bold' : 'Normal'} children={content} testID='text' /> diff --git a/src/components/Instance/Auth.tsx b/src/components/Instance/Auth.tsx index 2418bb49..16af0428 100644 --- a/src/components/Instance/Auth.tsx +++ b/src/components/Instance/Auth.tsx @@ -1,8 +1,9 @@ import { useNavigation } from '@react-navigation/native' import { useAppDispatch } from '@root/store' +import { InstanceLatest } from '@utils/migrations/instances/migration' import { TabMeStackNavigationProp } from '@utils/navigation/navigators' import addInstance from '@utils/slices/instances/add' -import { checkInstanceFeature, Instance } from '@utils/slices/instancesSlice' +import { checkInstanceFeature } from '@utils/slices/instancesSlice' import * as AuthSession from 'expo-auth-session' import React, { useEffect } from 'react' import { useQueryClient } from 'react-query' @@ -12,7 +13,7 @@ export interface Props { instanceDomain: string // Domain can be different than uri instance: Mastodon.Instance - appData: Instance['appData'] + appData: InstanceLatest['appData'] goBack?: boolean } diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index f53d562c..98c48d14 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -22,7 +22,7 @@ export interface Props { switchDisabled?: boolean switchOnValueChange?: () => void - iconBack?: 'ChevronRight' | 'ExternalLink' + iconBack?: 'ChevronRight' | 'ExternalLink' | 'Check' iconBackColor?: ColorDefinitions loading?: boolean diff --git a/src/components/Parse/Emojis.tsx b/src/components/Parse/Emojis.tsx index b09f6776..6e8df38b 100644 --- a/src/components/Parse/Emojis.tsx +++ b/src/components/Parse/Emojis.tsx @@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants' import { adaptiveScale } from '@utils/styles/scaling' import { useTheme } from '@utils/styles/ThemeManager' import React, { useMemo } from 'react' -import { StyleSheet } from 'react-native' +import { Platform, StyleSheet } from 'react-native' import FastImage from 'react-native-fast-image' import { useSelector } from 'react-redux' import validUrl from 'valid-url' @@ -51,7 +51,13 @@ const ParseEmojis = React.memo( image: { width: adaptedFontsize, height: adaptedFontsize, - transform: [{ translateY: -2 }] + ...(Platform.OS === 'ios' + ? { + transform: [{ translateY: -2 }] + } + : { + transform: [{ translateY: 1 }] + }) } }) }, [theme, adaptiveFontsize]) diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index d5a2dac7..c4c0ff62 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -13,7 +13,7 @@ import { adaptiveScale } from '@utils/styles/scaling' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Pressable, View } from 'react-native' +import { Platform, Pressable, View } from 'react-native' import HTMLView from 'react-native-htmlview' import { useSelector } from 'react-redux' @@ -139,7 +139,13 @@ const renderNode = ({ name='ExternalLink' size={adaptedFontsize} style={{ - transform: [{ translateY: -2 }] + ...(Platform.OS === 'ios' + ? { + transform: [{ translateY: -2 }] + } + : { + transform: [{ translateY: 1 }] + }) }} /> ) : null} diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index ec8d6b33..b18aec52 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -1,10 +1,6 @@ import ComponentSeparator from '@components/Separator' import { useScrollToTop } from '@react-navigation/native' -import { - QueryKeyTimeline, - TimelineData, - useTimelineQuery -} from '@utils/queryHooks/timeline' +import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { getInstanceActive } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' @@ -16,10 +12,20 @@ import { RefreshControl, StyleSheet } from 'react-native' -import { InfiniteData, useQueryClient } from 'react-query' +import Animated, { + useAnimatedScrollHandler, + useSharedValue +} from 'react-native-reanimated' +import { useQueryClient } from 'react-query' import { useSelector } from 'react-redux' import TimelineEmpty from './Timeline/Empty' import TimelineFooter from './Timeline/Footer' +import TimelineRefresh, { + SEPARATION_Y_1, + SEPARATION_Y_2 +} from './Timeline/Refresh' + +const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) export interface Props { flRef?: RefObject> @@ -40,15 +46,12 @@ const Timeline: React.FC = ({ }) => { const { colors } = useTheme() - const queryClient = useQueryClient() const { data, refetch, isFetching, isLoading, - fetchPreviousPage, fetchNextPage, - isFetchingPreviousPage, isFetchingNextPage } = useTimelineQuery({ ...queryKey[1], @@ -57,12 +60,6 @@ const Timeline: React.FC = ({ ios: ['dataUpdatedAt', 'isFetching'], android: ['dataUpdatedAt', 'isFetching', 'isLoading'] }), - getPreviousPageParam: firstPage => - firstPage?.links?.prev && { - min_id: firstPage.links.prev, - // https://github.com/facebook/react-native/issues/25239 - limit: '10' - }, getNextPageParam: lastPage => lastPage?.links?.next && { max_id: lastPage.links.next @@ -92,6 +89,27 @@ const Timeline: React.FC = ({ const flRef = useRef(null) + const scrollY = useSharedValue(0) + const fetchingType = useSharedValue<0 | 1 | 2>(0) + + const onScroll = useAnimatedScrollHandler( + { + onScroll: ({ contentOffset: { y } }) => { + scrollY.value = y + }, + onEndDrag: ({ contentOffset: { y } }) => { + if (!disableRefresh && !isFetching) { + if (y <= SEPARATION_Y_2) { + fetchingType.value = 2 + } else if (y <= SEPARATION_Y_1) { + fetchingType.value = 1 + } + } + } + }, + [isFetching] + ) + const androidRefreshControl = Platform.select({ android: { refreshControl: ( @@ -115,46 +133,40 @@ const Timeline: React.FC = ({ }) return ( - - } - ListEmptyComponent={} - ItemSeparatorComponent={ItemSeparatorComponent} - {...(isFetchingPreviousPage && { - maintainVisibleContentPosition: { minIndexForVisible: 0 } - })} - refreshing={isFetchingPreviousPage} - onRefresh={() => { - if (!disableRefresh && !isFetchingPreviousPage) { - queryClient.setQueryData | undefined>( - queryKey, - data => { - if (data?.pages[0] && data.pages[0].body.length === 0) { - return { - pages: data.pages.slice(1), - pageParams: data.pageParams.slice(1) - } - } else { - return data - } - } - ) - fetchPreviousPage() + <> + + } - }} - {...androidRefreshControl} - {...customProps} - /> + ListEmptyComponent={} + ItemSeparatorComponent={ItemSeparatorComponent} + maintainVisibleContentPosition={{ + minIndexForVisible: 0 + }} + {...androidRefreshControl} + {...customProps} + /> + ) } diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index cba31325..9d863144 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -14,7 +14,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import { isEqual, uniqBy } from 'lodash' +import { uniqBy } from 'lodash' import React, { useCallback } from 'react' import { Pressable, View } from 'react-native' import { useSelector } from 'react-redux' @@ -34,145 +34,136 @@ export interface Props { } // When the poll is long -const TimelineDefault = React.memo( - ({ - item, - queryKey, - rootQueryKey, - origin, - highlighted = false, - disableDetails = false, - disableOnPress = false - }: Props) => { - const { colors } = useTheme() - const instanceAccount = useSelector(getInstanceAccount, () => true) - const navigation = - useNavigation>() +const TimelineDefault: React.FC = ({ + item, + queryKey, + rootQueryKey, + origin, + highlighted = false, + disableDetails = false, + disableOnPress = false +}) => { + const { colors } = useTheme() + const instanceAccount = useSelector(getInstanceAccount, () => true) + const navigation = + useNavigation>() - const actualStatus = item.reblog ? item.reblog : item + const actualStatus = item.reblog ? item.reblog : item - const ownAccount = actualStatus.account?.id === instanceAccount?.id + const ownAccount = actualStatus.account?.id === instanceAccount?.id - if ( + if ( + !highlighted && + queryKey && + shouldFilter({ status: actualStatus, queryKey }) + ) { + return + } + + const onPress = useCallback(() => { + analytics('timeline_default_press', { + page: queryKey ? queryKey[1].page : origin + }) + !disableOnPress && !highlighted && - queryKey && - shouldFilter({ status: actualStatus, queryKey }) - ) { - return - } - - const onPress = useCallback(() => { - analytics('timeline_default_press', { - page: queryKey ? queryKey[1].page : origin + navigation.push('Tab-Shared-Toot', { + toot: actualStatus, + rootQueryKey: queryKey }) - !disableOnPress && - !highlighted && - navigation.push('Tab-Shared-Toot', { - toot: actualStatus, - rootQueryKey: queryKey - }) - }, []) + }, []) - return ( - + {item.reblog ? ( + + ) : item._pinned ? ( + + ) : null} + + + + + + + - {item.reblog ? ( - - ) : item._pinned ? ( - - ) : null} - - - - 0 ? ( + - - - - {typeof actualStatus.content === 'string' && - actualStatus.content.length > 0 ? ( - - ) : null} - {queryKey && actualStatus.poll ? ( - - ) : null} - {!disableDetails && - Array.isArray(actualStatus.media_attachments) && - actualStatus.media_attachments.length ? ( - - ) : null} - {!disableDetails && actualStatus.card ? ( - - ) : null} - {!disableDetails ? ( - - ) : null} - - - - - {queryKey && !disableDetails ? ( - d?.id !== instanceAccount?.id), - d => d?.id - ).map(d => d?.acct)} + statusId={actualStatus.id} + poll={actualStatus.poll} reblog={item.reblog ? true : false} + sameAccount={ownAccount} /> ) : null} - - ) - }, - (prev, next) => isEqual(prev.item, next.item) -) + {!disableDetails && + Array.isArray(actualStatus.media_attachments) && + actualStatus.media_attachments.length ? ( + + ) : null} + {!disableDetails && actualStatus.card ? ( + + ) : null} + {!disableDetails ? ( + + ) : null} + + + + + {queryKey && !disableDetails ? ( + d?.id !== instanceAccount?.id), + d => d?.id + ).map(d => d?.acct)} + reblog={item.reblog ? true : false} + /> + ) : null} + + ) +} export default TimelineDefault diff --git a/src/components/Timeline/Refresh.tsx b/src/components/Timeline/Refresh.tsx new file mode 100644 index 00000000..f5d5ad2a --- /dev/null +++ b/src/components/Timeline/Refresh.tsx @@ -0,0 +1,323 @@ +import haptics from '@components/haptics' +import Icon from '@components/Icon' +import { + QueryKeyTimeline, + TimelineData, + useTimelineQuery +} from '@utils/queryHooks/timeline' +import { StyleConstants } from '@utils/styles/constants' +import { useTheme } from '@utils/styles/ThemeManager' +import React, { RefObject, useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList, Platform, StyleSheet, Text, View } from 'react-native' +import { Circle } from 'react-native-animated-spinkit' +import Animated, { + Extrapolate, + interpolate, + runOnJS, + useAnimatedReaction, + useAnimatedStyle, + useSharedValue, + withTiming +} from 'react-native-reanimated' +import { InfiniteData, useQueryClient } from 'react-query' + +export interface Props { + flRef: RefObject> + queryKey: QueryKeyTimeline + scrollY: Animated.SharedValue + fetchingType: Animated.SharedValue<0 | 1 | 2> + disableRefresh?: boolean +} + +const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 +export const SEPARATION_Y_1 = -( + CONTAINER_HEIGHT / 2 + + StyleConstants.Font.Size.S / 2 +) +export const SEPARATION_Y_2 = -( + CONTAINER_HEIGHT * 1.5 + + StyleConstants.Font.Size.S / 2 +) + +const TimelineRefresh: React.FC = ({ + flRef, + queryKey, + scrollY, + fetchingType, + disableRefresh = false +}) => { + if (Platform.OS !== 'ios') { + return null + } + if (disableRefresh) { + return null + } + + const fetchingLatestIndex = useRef(0) + const refetchActive = useRef(false) + + const { + refetch, + isFetching, + isLoading, + fetchPreviousPage, + hasPreviousPage, + isFetchingNextPage + } = useTimelineQuery({ + ...queryKey[1], + options: { + getPreviousPageParam: firstPage => + firstPage?.links?.prev && { + min_id: firstPage.links.prev, + // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 + limit: '3' + }, + select: data => { + if (refetchActive.current) { + data.pageParams = [data.pageParams[0]] + data.pages = [data.pages[0]] + refetchActive.current = false + } + return data + }, + onSuccess: () => { + if (fetchingLatestIndex.current > 0) { + if (fetchingLatestIndex.current > 5) { + clearFirstPage() + fetchingLatestIndex.current = 0 + } else { + if (hasPreviousPage) { + fetchPreviousPage() + fetchingLatestIndex.current++ + } else { + clearFirstPage() + fetchingLatestIndex.current = 0 + } + } + } + } + } + }) + + const { t } = useTranslation('componentTimeline') + const { colors } = useTheme() + + const queryClient = useQueryClient() + const clearFirstPage = () => { + queryClient.setQueryData | undefined>( + queryKey, + data => { + if (data?.pages[0] && data.pages[0].body.length === 0) { + return { + pages: data.pages.slice(1), + pageParams: data.pageParams.slice(1) + } + } else { + return data + } + } + ) + } + const prepareRefetch = () => { + refetchActive.current = true + queryClient.setQueryData | undefined>( + queryKey, + data => { + if (data) { + data.pageParams = [undefined] + const newFirstPage: TimelineData = { body: [] } + for (let page of data.pages) { + // @ts-ignore + newFirstPage.body.push(...page.body) + if (newFirstPage.body.length > 10) break + } + data.pages = [newFirstPage] + } + + return data + } + ) + } + const callRefetch = async () => { + await refetch() + setTimeout(() => flRef.current?.scrollToOffset({ offset: 1 }), 50) + } + + const [textRight, setTextRight] = useState(0) + const arrowY = useAnimatedStyle(() => ({ + transform: [ + { + translateY: interpolate( + scrollY.value, + [0, SEPARATION_Y_1], + [ + -CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2, + CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2 + ], + Extrapolate.CLAMP + ) + } + ] + })) + const arrowTop = useAnimatedStyle(() => ({ + marginTop: + scrollY.value < SEPARATION_Y_2 + ? withTiming(CONTAINER_HEIGHT) + : withTiming(0) + })) + + const arrowStage = useSharedValue(0) + const onLayout = useCallback( + ({ nativeEvent }) => { + if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { + setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) + } + }, + [textRight] + ) + useAnimatedReaction( + () => { + if (isFetching) { + return false + } + switch (arrowStage.value) { + case 0: + if (scrollY.value < SEPARATION_Y_1) { + arrowStage.value = 1 + return true + } + return false + case 1: + if (scrollY.value < SEPARATION_Y_2) { + arrowStage.value = 2 + return true + } + if (scrollY.value > SEPARATION_Y_1) { + arrowStage.value = 0 + return false + } + return false + case 2: + if (scrollY.value > SEPARATION_Y_2) { + arrowStage.value = 1 + return false + } + return false + } + }, + data => { + if (data) { + runOnJS(haptics)('Light') + } + }, + [isFetching] + ) + const wrapperStartLatest = () => { + fetchingLatestIndex.current = 1 + } + + useAnimatedReaction( + () => { + return fetchingType.value + }, + data => { + fetchingType.value = 0 + switch (data) { + case 1: + runOnJS(wrapperStartLatest)() + runOnJS(clearFirstPage)() + runOnJS(fetchPreviousPage)() + break + case 2: + runOnJS(prepareRefetch)() + runOnJS(callRefetch)() + break + } + }, + [] + ) + + const headerPadding = useAnimatedStyle( + () => ({ + paddingTop: + fetchingLatestIndex.current !== 0 || + (isFetching && !isLoading && !isFetchingNextPage) + ? withTiming(StyleConstants.Spacing.M * 2.5) + : withTiming(0) + }), + [fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading] + ) + + return ( + + + {isFetching ? ( + + + + ) : ( + <> + + + + } + /> + + + + + + )} + + + ) +} + +const styles = StyleSheet.create({ + base: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: CONTAINER_HEIGHT * 2, + alignItems: 'center' + }, + container1: { + flex: 1, + flexDirection: 'row', + height: CONTAINER_HEIGHT + }, + container2: { height: CONTAINER_HEIGHT, justifyContent: 'center' }, + explanation: { + fontSize: StyleConstants.Font.Size.S, + lineHeight: CONTAINER_HEIGHT + } +}) + +export default TimelineRefresh diff --git a/src/components/Timeline/Shared/Actioned.tsx b/src/components/Timeline/Shared/Actioned.tsx index 9b600d2f..0ff986e3 100644 --- a/src/components/Timeline/Shared/Actioned.tsx +++ b/src/components/Timeline/Shared/Actioned.tsx @@ -34,7 +34,7 @@ const TimelineActioned = React.memo( navigation.push('Tab-Shared-Account', { account }) }, []) - const children = useMemo(() => { + const children = () => { switch (action) { case 'pinned': return ( @@ -48,7 +48,6 @@ const TimelineActioned = React.memo( {content(t('shared.actioned.pinned'))} ) - break case 'favourite': return ( <> @@ -63,7 +62,6 @@ const TimelineActioned = React.memo( ) - break case 'follow': return ( <> @@ -78,7 +76,6 @@ const TimelineActioned = React.memo( ) - break case 'follow_request': return ( <> @@ -93,7 +90,6 @@ const TimelineActioned = React.memo( ) - break case 'poll': return ( <> @@ -106,7 +102,6 @@ const TimelineActioned = React.memo( {content(t('shared.actioned.poll'))} ) - break case 'reblog': return ( <> @@ -125,7 +120,6 @@ const TimelineActioned = React.memo( ) - break case 'status': return ( <> @@ -140,9 +134,22 @@ const TimelineActioned = React.memo( ) - break + case 'update': + return ( + <> + + {content(t('shared.actioned.update'))} + + ) + default: + return <> } - }, []) + } return ( + > + {children()} + ) }, () => true diff --git a/src/components/Timeline/Shared/Attachment.tsx b/src/components/Timeline/Shared/Attachment.tsx index a4a4b670..0d2f5c40 100644 --- a/src/components/Timeline/Shared/Attachment.tsx +++ b/src/components/Timeline/Shared/Attachment.tsx @@ -8,11 +8,13 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video' import { useNavigation } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { RootStackParamList } from '@utils/navigation/navigators' +import { getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import layoutAnimation from '@utils/styles/layoutAnimation' import React, { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Pressable, View } from 'react-native' +import { useSelector } from 'react-redux' export interface Props { status: Pick @@ -22,7 +24,23 @@ const TimelineAttachment = React.memo( ({ status }: Props) => { const { t } = useTranslation('componentTimeline') - const [sensitiveShown, setSensitiveShown] = useState(status.sensitive) + const account = useSelector( + getInstanceAccount, + (prev, next) => + prev.preferences['reading:expand:media'] === + next.preferences['reading:expand:media'] + ) + const defaultSensitive = () => { + switch (account.preferences['reading:expand:media']) { + case 'show_all': + return false + case 'hide_all': + return true + default: + return status.sensitive + } + } + const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive()) const imageUrls = useRef< RootStackParamList['Screen-ImagesViewer']['imageUrls'] @@ -151,7 +169,7 @@ const TimelineAttachment = React.memo( })} - {status.sensitive && + {defaultSensitive() && (sensitiveShown ? ( = ({ sensitiveShown, text }) => { + if (!text) { + return null + } + + const navigation = useNavigation>() + + return !sensitiveShown ? ( +