diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 87d4f712..5bc2a4ea 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,5 +1,6 @@ Enjoy toooting! This version includes following improvements and fixes: - Auto fetch remote content in conversations! +- Remember last read position in timeline! - Allowing adding more context of reports - Option to disable autoplay gif - Hide boosts from users diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index 41ef5bc6..bb70b250 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,5 +1,6 @@ toooting愉快!此版本包括以下改进和修复: - 主动获取对话的远程内容 +- 自动加载上次我的关注的阅读位置 - 可添加举报细节 - 新增暂停自动播放gif动画选项 - 隐藏用户的转嘟 diff --git a/src/components/Timeline/Refresh.tsx b/src/components/Timeline/Refresh.tsx index 5a94e0fc..6d9d31b7 100644 --- a/src/components/Timeline/Refresh.tsx +++ b/src/components/Timeline/Refresh.tsx @@ -7,18 +7,19 @@ import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' +import { setAccountStorage } from '@utils/storage/actions' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { RefObject, useEffect, useRef, useState } from 'react' +import React, { RefObject, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, Platform, Text, View } from 'react-native' -import { Circle } from 'react-native-animated-spinkit' import Animated, { Extrapolate, interpolate, runOnJS, useAnimatedReaction, useAnimatedStyle, + useDerivedValue, useSharedValue, withTiming } from 'react-native-reanimated' @@ -26,9 +27,11 @@ import Animated, { export interface Props { flRef: RefObject> queryKey: QueryKeyTimeline + fetchingActive: React.MutableRefObject scrollY: Animated.SharedValue fetchingType: Animated.SharedValue<0 | 1 | 2> disableRefresh?: boolean + readMarker?: 'read_marker_following' } const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 @@ -38,9 +41,11 @@ export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Siz const TimelineRefresh: React.FC = ({ flRef, queryKey, + fetchingActive, scrollY, fetchingType, - disableRefresh = false + disableRefresh = false, + readMarker }) => { if (Platform.OS !== 'ios') { return null @@ -55,7 +60,15 @@ const TimelineRefresh: React.FC = ({ const prevStatusId = useRef() const queryClient = useQueryClient() - const { refetch, isFetching } = useTimelineQuery({ ...queryKey[1] }) + const { refetch, isRefetching } = useTimelineQuery({ ...queryKey[1] }) + + useDerivedValue(() => { + if (prevActive.current || isRefetching) { + fetchingActive.current = true + } else { + fetchingActive.current = false + } + }, [prevActive.current, isRefetching]) const { t } = useTranslation('componentTimeline') const { colors } = useTheme() @@ -83,7 +96,7 @@ const TimelineRefresh: React.FC = ({ const arrowStage = useSharedValue(0) useAnimatedReaction( () => { - if (isFetching) { + if (fetchingActive.current) { return false } switch (arrowStage.value) { @@ -116,7 +129,7 @@ const TimelineRefresh: React.FC = ({ runOnJS(haptics)('Light') } }, - [isFetching] + [fetchingActive.current] ) const fetchAndScrolled = useSharedValue(false) @@ -201,11 +214,13 @@ const TimelineRefresh: React.FC = ({ } prevActive.current = false }) - .catch(err => console.warn(err)) } const runFetchLatest = async () => { queryClient.invalidateQueries(queryKey) + if (readMarker) { + setAccountStorage([{ key: readMarker, value: undefined }]) + } await refetch() setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50) } @@ -239,61 +254,53 @@ const TimelineRefresh: React.FC = ({ alignItems: 'center' }} > - {prevActive.current || isFetching ? ( - - - - ) : ( - <> - - { - if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { - setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) - } - }} - children={t('refresh.fetchPreviousPage')} + + { + if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { + setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) + } + }} + children={t('refresh.fetchPreviousPage')} + /> + - - } - /> - - - { - if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { - setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) - } - }} - children={t('refresh.refetch')} - /> - - - )} + } + /> + + + { + if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { + setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) + } + }} + children={t('refresh.refetch')} + /> + ) } diff --git a/src/components/Timeline/index.tsx b/src/components/Timeline/index.tsx index 7e4f1c92..2f1ea0e7 100644 --- a/src/components/Timeline/index.tsx +++ b/src/components/Timeline/index.tsx @@ -1,9 +1,14 @@ import ComponentSeparator from '@components/Separator' +import TimelineDefault from '@components/Timeline/Default' import { useScrollToTop } from '@react-navigation/native' import { UseInfiniteQueryOptions } from '@tanstack/react-query' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { flattenPages } from '@utils/queryHooks/utils' -import { useGlobalStorageListener } from '@utils/storage/actions' +import { + getAccountStorage, + setAccountStorage, + useGlobalStorageListener +} from '@utils/storage/actions' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { RefObject, useRef } from 'react' @@ -13,7 +18,7 @@ import TimelineEmpty from './Empty' import TimelineFooter from './Footer' import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Refresh' -const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) +const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) export interface Props { flRef?: RefObject> @@ -24,7 +29,8 @@ export interface Props { > disableRefresh?: boolean disableInfinity?: boolean - customProps: Partial> & Pick, 'renderItem'> + readMarker?: 'read_marker_following' + customProps?: Partial> } const Timeline: React.FC = ({ @@ -33,6 +39,7 @@ const Timeline: React.FC = ({ queryOptions, disableRefresh = false, disableInfinity = false, + readMarker = undefined, customProps }) => { const { colors } = useTheme() @@ -56,6 +63,7 @@ const Timeline: React.FC = ({ }) const flRef = useRef(null) + const fetchingActive = useRef(false) const scrollY = useSharedValue(0) const fetchingType = useSharedValue<0 | 1 | 2>(0) @@ -78,6 +86,32 @@ const Timeline: React.FC = ({ [isFetching] ) + const viewabilityConfigCallbackPairs = useRef< + Pick, 'viewabilityConfigCallbackPairs'>['viewabilityConfigCallbackPairs'] + >( + readMarker + ? [ + { + viewabilityConfig: { + minimumViewTime: 300, + itemVisiblePercentThreshold: 80, + waitForInteraction: true + }, + onViewableItemsChanged: ({ viewableItems }) => { + const marker = readMarker ? getAccountStorage.string(readMarker) : undefined + + const firstItemId = viewableItems.filter(item => item.isViewable)[0]?.item.id + if (!fetchingActive.current && firstItemId && firstItemId > (marker || '0')) { + setAccountStorage([{ key: readMarker, value: firstItemId }]) + } else { + // setAccountStorage([{ key: readMarker, value: '109519141378761752' }]) + } + } + } + ] + : undefined + ) + const androidRefreshControl = Platform.select({ android: { refreshControl: ( @@ -102,9 +136,11 @@ const Timeline: React.FC = ({ = ({ onScroll={onScroll} windowSize={7} data={flattenPages(data)} + {...(customProps?.renderItem + ? { renderItem: customProps.renderItem } + : { renderItem: ({ item }) => })} initialNumToRender={6} maxToRenderPerBatch={3} onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()} @@ -129,6 +168,7 @@ const Timeline: React.FC = ({ /> ) } + viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current} {...(!isLoading && { maintainVisibleContentPosition: { minIndexForVisible: 0 diff --git a/src/screens/Tabs/Local/Root.tsx b/src/screens/Tabs/Local/Root.tsx index 57d65863..465ec22d 100644 --- a/src/screens/Tabs/Local/Root.tsx +++ b/src/screens/Tabs/Local/Root.tsx @@ -2,7 +2,6 @@ import { HeaderRight } from '@components/Header' import Icon from '@components/Icon' import CustomText from '@components/Text' import Timeline from '@components/Timeline' -import TimelineDefault from '@components/Timeline/Default' import { NativeStackScreenProps } from '@react-navigation/native-stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' import { useListsQuery } from '@utils/queryHooks/lists' @@ -178,14 +177,7 @@ const Root: React.FC - }} - /> - ) + return } export default Root diff --git a/src/screens/Tabs/Me/Bookmarks.tsx b/src/screens/Tabs/Me/Bookmarks.tsx index 808dc3e1..83ffae0d 100644 --- a/src/screens/Tabs/Me/Bookmarks.tsx +++ b/src/screens/Tabs/Me/Bookmarks.tsx @@ -1,5 +1,4 @@ import Timeline from '@components/Timeline' -import TimelineDefault from '@components/Timeline/Default' import { NativeStackScreenProps } from '@react-navigation/native-stack' import { TabMeStackParamList } from '@utils/navigation/navigators' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' @@ -13,14 +12,7 @@ const TabMeBookmarks: React.FC - }} - /> - ) + return } export default TabMeBookmarks diff --git a/src/screens/Tabs/Me/Favourites.tsx b/src/screens/Tabs/Me/Favourites.tsx index fe9db5ae..a183751e 100644 --- a/src/screens/Tabs/Me/Favourites.tsx +++ b/src/screens/Tabs/Me/Favourites.tsx @@ -1,5 +1,4 @@ import Timeline from '@components/Timeline' -import TimelineDefault from '@components/Timeline/Default' import { NativeStackScreenProps } from '@react-navigation/native-stack' import { TabMeStackParamList } from '@utils/navigation/navigators' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' @@ -13,14 +12,7 @@ const TabMeFavourites: React.FC< navigation.setParams({ queryKey: queryKey }) }, []) - return ( - - }} - /> - ) + return } export default TabMeFavourites diff --git a/src/screens/Tabs/Me/List/index.tsx b/src/screens/Tabs/Me/List/index.tsx index 5fe3cd7b..71b59b4b 100644 --- a/src/screens/Tabs/Me/List/index.tsx +++ b/src/screens/Tabs/Me/List/index.tsx @@ -1,7 +1,6 @@ import Icon from '@components/Icon' import { displayMessage } from '@components/Message' import Timeline from '@components/Timeline' -import TimelineDefault from '@components/Timeline/Default' import { NativeStackScreenProps } from '@react-navigation/native-stack' import { useQueryClient } from '@tanstack/react-query' import { TabMeStackParamList } from '@utils/navigation/navigators' @@ -74,14 +73,7 @@ const TabMeList: React.FC - }} - /> - ) + return } export default TabMeList diff --git a/src/screens/Tabs/Public/Root.tsx b/src/screens/Tabs/Public/Root.tsx index bdfc410a..ef5054ec 100644 --- a/src/screens/Tabs/Public/Root.tsx +++ b/src/screens/Tabs/Public/Root.tsx @@ -1,6 +1,5 @@ import { HeaderRight } from '@components/Header' import Timeline from '@components/Timeline' -import TimelineDefault from '@components/Timeline/Default' import SegmentedControl from '@react-native-community/segmented-control' import { useNavigation } from '@react-navigation/native' import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack' @@ -21,15 +20,7 @@ const Route = ({ route: { key: page } }: { route: any }) => { useEffect(() => { navigation.setParams({ queryKey }) }, []) - return ( - - }} - /> - ) + return } const renderScene = SceneMap({ diff --git a/src/screens/Tabs/Shared/Attachments.tsx b/src/screens/Tabs/Shared/Attachments.tsx index 11f420fe..d6f5cdd5 100644 --- a/src/screens/Tabs/Shared/Attachments.tsx +++ b/src/screens/Tabs/Shared/Attachments.tsx @@ -2,7 +2,6 @@ import { HeaderLeft } from '@components/Header' import { ParseEmojis } from '@components/Parse' import CustomText from '@components/Text' import Timeline from '@components/Timeline' -import TimelineDefault from '@components/Timeline/Default' import { TabSharedStackScreenProps } from '@utils/navigation/navigators' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { useTheme } from '@utils/styles/ThemeManager' @@ -49,14 +48,7 @@ const TabSharedAttachments: React.FC - }} - /> - ) + return } export default TabSharedAttachments diff --git a/src/screens/Tabs/Shared/Hashtag.tsx b/src/screens/Tabs/Shared/Hashtag.tsx index 542fe67b..149459f0 100644 --- a/src/screens/Tabs/Shared/Hashtag.tsx +++ b/src/screens/Tabs/Shared/Hashtag.tsx @@ -2,7 +2,6 @@ import haptics from '@components/haptics' import { HeaderLeft, HeaderRight } from '@components/Header' import { displayMessage } from '@components/Message' import Timeline from '@components/Timeline' -import TimelineDefault from '@components/Timeline/Default' import { useQueryClient } from '@tanstack/react-query' import { featureCheck } from '@utils/helpers/featureCheck' import { TabSharedStackScreenProps } from '@utils/navigation/navigators' @@ -82,14 +81,7 @@ const TabSharedHashtag: React.FC }) }, [canFollowTags, data?.following, isFetching]) - return ( - - }} - /> - ) + return } export default TabSharedHashtag diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts index b85ed23c..5d5608a9 100644 --- a/src/utils/queryHooks/timeline.ts +++ b/src/utils/queryHooks/timeline.ts @@ -11,7 +11,8 @@ import apiInstance from '@utils/api/instance' import { featureCheck } from '@utils/helpers/featureCheck' import { useNavState } from '@utils/navigation/navigators' import { queryClient } from '@utils/queryHooks' -import { getAccountStorage } from '@utils/storage/actions' +import { StorageAccount } from '@utils/storage/account' +import { getAccountStorage, setAccountStorage } from '@utils/storage/actions' import { AxiosError } from 'axios' import { uniqBy } from 'lodash' import { searchLocalStatus } from './search' @@ -57,7 +58,25 @@ export const queryFunctionTimeline = async ({ pageParam }: QueryFunctionContext) => { const page = queryKey[1] - let params: { [key: string]: string } = { limit: 40, ...pageParam } + + let marker: string | undefined + if (page.page === 'Following' && !pageParam?.offset && !pageParam?.min_id && !pageParam?.max_id) { + const storedMarker = getAccountStorage.string('read_marker_following') + if (storedMarker) { + await apiInstance({ + method: 'get', + url: 'timelines/home', + params: { limit: 1, min_id: storedMarker } + }).then(res => { + if (res.body.length) { + marker = storedMarker + } + }) + } + } + let params: { [key: string]: string } = marker + ? { limit: 40, max_id: marker } + : { limit: 40, ...pageParam } switch (page.page) { case 'Following': @@ -431,7 +450,6 @@ const useTimelineMutation = ({ updateStatusProperty(params, navigationState) break case 'editItem': - console.log('YES!!!') editItem(params) break case 'deleteItem': diff --git a/src/utils/storage/account/v0.ts b/src/utils/storage/account/v0.ts index d1fb9ea3..d1ec06de 100644 --- a/src/utils/storage/account/v0.ts +++ b/src/utils/storage/account/v0.ts @@ -24,6 +24,7 @@ export type AccountV0 = { 'auth.account.domain': string // used for username 'auth.account.avatar_static': string version: string + read_marker_following?: string // number // boolean // object