diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 103e95c2..492ae4bc 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: - Automatic setting detected language when tooting +- Remember public timeline type selection - Added notification for admins - Fix whole word filter matching - Fix tablet cannot delete toot drafts \ No newline at end of file diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index b7098afa..4e3b5ced 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,5 +1,6 @@ toooting愉快!此版本包括以下改进和修复: - 自动识别发嘟语言 +- 记住上次公共时间轴选项 - 新增管理员推送通知 - 修复过滤整词功能 - 修复平板不能删除草稿 \ No newline at end of file diff --git a/package.json b/package.json index 42c4ade9..53bef058 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@reduxjs/toolkit": "^1.9.1", "@sentry/react-native": "4.10.1", "@sharcoux/slider": "^6.1.1", + "@tanstack/react-query": "^4.19.1", "axios": "^0.27.2", "expo": "^47.0.8", "expo-auth-session": "^3.7.3", @@ -60,7 +61,6 @@ "expo-video-thumbnails": "^7.0.0", "expo-web-browser": "~12.0.0", "i18next": "^22.0.6", - "li": "^1.3.0", "linkify-it": "^4.0.1", "lodash": "^4.17.21", "react": "^18.2.0", @@ -89,7 +89,6 @@ "react-native-svg": "^13.6.0", "react-native-swipe-list-view": "^3.2.9", "react-native-tab-view": "^3.3.2", - "react-query": "^3.39.2", "react-redux": "^8.0.5", "redux-persist": "^6.0.0", "rn-placeholder": "^3.0.3", diff --git a/src/@types/app.d.ts b/src/@types/app.d.ts index 0db454c2..d3d4f400 100644 --- a/src/@types/app.d.ts +++ b/src/@types/app.d.ts @@ -3,6 +3,7 @@ declare namespace App { | 'Following' | 'Local' | 'LocalPublic' + | 'Trending' | 'Notifications' | 'Hashtag' | 'List' diff --git a/src/@types/untyped.d.ts b/src/@types/untyped.d.ts index e3b7bc1a..25b5f1b1 100644 --- a/src/@types/untyped.d.ts +++ b/src/@types/untyped.d.ts @@ -1,6 +1,5 @@ declare module 'gl-react-blurhash' declare module 'htmlparser2-without-node-native' -declare module 'li' declare module 'react-native-feather' declare module 'react-native-htmlview' declare module 'react-native-toast-message' diff --git a/src/App.tsx b/src/App.tsx index 92dcc448..5d77d55a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,7 @@ import { LogBox, Platform } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' import { SafeAreaProvider } from 'react-native-safe-area-context' import { enableFreeze } from 'react-native-screens' -import { QueryClientProvider } from 'react-query' +import { QueryClientProvider } from '@tanstack/react-query' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' diff --git a/src/api/instance.ts b/src/api/instance.ts index a178a42c..ca094b4d 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -1,6 +1,5 @@ import { RootState } from '@root/store' import axios, { AxiosRequestConfig } from 'axios' -import li from 'li' import { ctx, handleError, userAgent } from './helpers' export type Params = { @@ -15,9 +14,10 @@ export type Params = { extras?: Omit } +type LinkFormat = { id: string; isOffset: boolean } export type InstanceResponse = { body: T - links: { prev?: string; next?: string } + links: { prev?: LinkFormat; next?: LinkFormat } } const apiInstance = async ({ @@ -74,17 +74,27 @@ const apiInstance = async ({ ...extras }) .then(response => { - let prev - let next + let links: { + prev?: { id: string; isOffset: boolean } + next?: { id: string; isOffset: boolean } + } = {} + if (response.headers?.link) { - const headersLinks = li.parse(response.headers?.link) - prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1] - next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1] + const linksParsed = response.headers.link.matchAll( + new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi') + ) + for (const link of linksParsed) { + switch (link[3]) { + case 'prev': + links.prev = { id: link[2], isOffset: link[1].includes('offset') } + break + case 'next': + links.next = { id: link[2], isOffset: link[1].includes('offset') } + break + } + } } - return Promise.resolve({ - body: response.data, - links: { prev, next } - }) + return Promise.resolve({ body: response.data, links }) }) .catch(handleError()) } diff --git a/src/components/Hashtag.tsx b/src/components/Hashtag.tsx index 2d05edc9..1402d7ac 100644 --- a/src/components/Hashtag.tsx +++ b/src/components/Hashtag.tsx @@ -3,8 +3,8 @@ import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useCallback, useState } from 'react' -import { Dimensions, Pressable } from 'react-native' +import React, { PropsWithChildren, useCallback, useState } from 'react' +import { Dimensions, Pressable, View } from 'react-native' import Sparkline from './Sparkline' import CustomText from './Text' @@ -13,7 +13,11 @@ export interface Props { onPress?: () => void } -const ComponentHashtag: React.FC = ({ hashtag, onPress: customOnPress }) => { +const ComponentHashtag: React.FC = ({ + hashtag, + onPress: customOnPress, + children +}) => { const { colors } = useTheme() const navigation = useNavigation>() @@ -31,15 +35,11 @@ const ComponentHashtag: React.FC = ({ hashtag, onPress: customOnPress }) style={{ flex: 1, flexDirection: 'row', + alignItems: 'center', justifyContent: 'space-between', padding }} onPress={customOnPress || onPress} - onLayout={({ - nativeEvent: { - layout: { height } - } - }) => setHeight(height - padding * 2 - 1)} > = ({ hashtag, onPress: customOnPress }) > #{hashtag.name} - parseInt(h.uses)).reverse()} - width={width} - height={height} - /> + setHeight(height)} + > + parseInt(h.uses)).reverse()} + width={width} + height={height} + margin={children ? StyleConstants.Spacing.S : undefined} + /> + {children} + ) } diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index 3953e5c9..5100745d 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { ColorDefinitions } from '@utils/styles/themes' import React, { useMemo } from 'react' -import { Text, View } from 'react-native' +import { View } from 'react-native' import { Flow } from 'react-native-animated-spinkit' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' diff --git a/src/components/Relationship/Incoming.tsx b/src/components/Relationship/Incoming.tsx index d6eb633c..837345f6 100644 --- a/src/components/Relationship/Incoming.tsx +++ b/src/components/Relationship/Incoming.tsx @@ -8,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet, View } from 'react-native' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' export interface Props { id: Mastodon.Account['id'] diff --git a/src/components/Relationship/Outgoing.tsx b/src/components/Relationship/Outgoing.tsx index b551d670..f41ea157 100644 --- a/src/components/Relationship/Outgoing.tsx +++ b/src/components/Relationship/Outgoing.tsx @@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { useTranslation } from 'react-i18next' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' export interface Props { id: Mastodon.Account['id'] diff --git a/src/components/Sparkline.tsx b/src/components/Sparkline.tsx index 21792069..707f28e7 100644 --- a/src/components/Sparkline.tsx +++ b/src/components/Sparkline.tsx @@ -1,7 +1,6 @@ import { useTheme } from '@utils/styles/ThemeManager' import { maxBy, minBy } from 'lodash' import React from 'react' -import { Platform } from 'react-native' import Svg, { G, Path } from 'react-native-svg' export interface Props { @@ -69,7 +68,7 @@ const Sparkline: React.FC = ({ data, width, height, margin = 0 }) => { const fillPoints = linePoints.concat(closePolyPoints) return ( - + - customProps: Partial> & - Pick, 'renderItem'> + customProps: Partial> & Pick, 'renderItem'> } const Timeline: React.FC = ({ @@ -39,30 +32,24 @@ const Timeline: React.FC = ({ }) => { const { colors } = useTheme() - const { - data, - refetch, - isFetching, - isLoading, - fetchNextPage, - isFetchingNextPage - } = useTimelineQuery({ - ...queryKey[1], - options: { - notifyOnChangeProps: Platform.select({ - ios: ['dataUpdatedAt', 'isFetching'], - android: ['dataUpdatedAt', 'isFetching', 'isLoading'] - }), - getNextPageParam: lastPage => - lastPage?.links?.next && { - max_id: lastPage.links.next - } - } - }) + const { data, refetch, isFetching, isLoading, fetchNextPage, isFetchingNextPage } = + useTimelineQuery({ + ...queryKey[1], + options: { + notifyOnChangeProps: Platform.select({ + ios: ['dataUpdatedAt', 'isFetching'], + android: ['dataUpdatedAt', 'isFetching', 'isLoading'] + }), + getNextPageParam: lastPage => + lastPage?.links?.next && { + ...(lastPage.links.next.isOffset + ? { offset: lastPage.links.next.id } + : { max_id: lastPage.links.next.id }) + } + } + }) - const flattenData = data?.pages - ? data.pages?.flatMap(page => [...page.body]) - : [] + const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : [] const onEndReached = useCallback( () => !disableInfinity && !isFetchingNextPage && fetchNextPage(), @@ -134,10 +121,7 @@ const Timeline: React.FC = ({ onEndReached={onEndReached} onEndReachedThreshold={0.75} ListFooterComponent={ - + } ListEmptyComponent={} ItemSeparatorComponent={({ leadingItem }) => @@ -145,9 +129,7 @@ const Timeline: React.FC = ({ ) : ( ) } diff --git a/src/components/Timeline/Conversation.tsx b/src/components/Timeline/Conversation.tsx index b3311a28..8e07d2c0 100644 --- a/src/components/Timeline/Conversation.tsx +++ b/src/components/Timeline/Conversation.tsx @@ -8,7 +8,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback } from 'react' import { Pressable, View } from 'react-native' -import { useMutation, useQueryClient } from 'react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import TimelineActions from './Shared/Actions' import TimelineContent from './Shared/Content' import StatusContext from './Shared/Context' diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index cf57785b..16c8ac90 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -125,6 +125,7 @@ const TimelineDefault: React.FC = ({ spoilerHidden, copiableContent, highlighted, + inThread: queryKey?.[1].page === 'Toot', disableDetails, disableOnPress }} diff --git a/src/components/Timeline/Footer.tsx b/src/components/Timeline/Footer.tsx index ab4e1934..4ac3f2af 100644 --- a/src/components/Timeline/Footer.tsx +++ b/src/components/Timeline/Footer.tsx @@ -21,7 +21,11 @@ const TimelineFooter = React.memo( enabled: !disableInfinity, notifyOnChangeProps: ['hasNextPage'], getNextPageParam: lastPage => - lastPage?.links?.next && { max_id: lastPage.links.next } + lastPage?.links?.next && { + ...(lastPage.links.next.isOffset + ? { offset: lastPage.links.next.id } + : { max_id: lastPage.links.next.id }) + } } }) @@ -43,11 +47,7 @@ const TimelineFooter = React.memo( + ]} /> diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx index 04204965..4d7b69a6 100644 --- a/src/components/Timeline/Notifications.tsx +++ b/src/components/Timeline/Notifications.tsx @@ -108,7 +108,10 @@ const TimelineNotifications: React.FC = ({ paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S }} > - + diff --git a/src/components/Timeline/Refresh.tsx b/src/components/Timeline/Refresh.tsx index 38cfdcdd..9ac45e95 100644 --- a/src/components/Timeline/Refresh.tsx +++ b/src/components/Timeline/Refresh.tsx @@ -1,10 +1,6 @@ import haptics from '@components/haptics' import Icon from '@components/Icon' -import { - QueryKeyTimeline, - TimelineData, - useTimelineQuery -} from '@utils/queryHooks/timeline' +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' @@ -20,7 +16,7 @@ import Animated, { useSharedValue, withTiming } from 'react-native-reanimated' -import { InfiniteData, useQueryClient } from 'react-query' +import { InfiniteData, useQueryClient } from '@tanstack/react-query' export interface Props { flRef: RefObject> @@ -31,14 +27,8 @@ export interface Props { } 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 -) +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, @@ -57,87 +47,77 @@ const TimelineRefresh: React.FC = ({ 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' + const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } = + useTimelineQuery({ + ...queryKey[1], + options: { + getPreviousPageParam: firstPage => + firstPage?.links?.prev && { + ...(firstPage.links.prev.isOffset + ? { offset: firstPage.links.prev.id } + : { max_id: firstPage.links.prev.id }), + // 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 }, - 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 { + 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 + 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] + 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 } - - return data + data.pages = [newFirstPage] } - ) + + return data + }) } const callRefetch = async () => { await refetch() @@ -161,10 +141,7 @@ const TimelineRefresh: React.FC = ({ ] })) const arrowTop = useAnimatedStyle(() => ({ - marginTop: - scrollY.value < SEPARATION_Y_2 - ? withTiming(CONTAINER_HEIGHT) - : withTiming(0) + marginTop: scrollY.value < SEPARATION_Y_2 ? withTiming(CONTAINER_HEIGHT) : withTiming(0) })) const arrowStage = useSharedValue(0) @@ -241,8 +218,7 @@ const TimelineRefresh: React.FC = ({ const headerPadding = useAnimatedStyle( () => ({ paddingTop: - fetchingLatestIndex.current !== 0 || - (isFetching && !isLoading && !isFetchingNextPage) + fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage) ? withTiming(StyleConstants.Spacing.M * 2.5) : withTiming(0) }), @@ -254,10 +230,7 @@ const TimelineRefresh: React.FC = ({ {isFetching ? ( - + ) : ( <> diff --git a/src/components/Timeline/Shared/Actions.tsx b/src/components/Timeline/Shared/Actions.tsx index 0991ee89..a803dd45 100644 --- a/src/components/Timeline/Shared/Actions.tsx +++ b/src/components/Timeline/Shared/Actions.tsx @@ -17,7 +17,7 @@ import { uniqBy } from 'lodash' import React, { useCallback, useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Pressable, StyleSheet, View } from 'react-native' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' import { useSelector } from 'react-redux' import StatusContext from './Context' diff --git a/src/components/Timeline/Shared/Content.tsx b/src/components/Timeline/Shared/Content.tsx index 5e2578a1..85985330 100644 --- a/src/components/Timeline/Shared/Content.tsx +++ b/src/components/Timeline/Shared/Content.tsx @@ -6,11 +6,12 @@ import { useSelector } from 'react-redux' import StatusContext from './Context' export interface Props { + notificationOwnToot?: boolean setSpoilerExpanded?: React.Dispatch> } -const TimelineContent: React.FC = ({ setSpoilerExpanded }) => { - const { status, highlighted, disableDetails } = useContext(StatusContext) +const TimelineContent: React.FC = ({ notificationOwnToot = false, setSpoilerExpanded }) => { + const { status, highlighted, inThread, disableDetails } = useContext(StatusContext) if (!status || typeof status.content !== 'string' || !status.content.length) return null const { t } = useTranslation('componentTimeline') @@ -38,7 +39,13 @@ const TimelineContent: React.FC = ({ setSpoilerExpanded }) => { emojis={status.emojis} mentions={status.mentions} tags={status.tags} - numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1} + numberOfLines={ + instanceAccount.preferences['reading:expand:spoilers'] || inThread + ? notificationOwnToot + ? 2 + : 999 + : 1 + } expandHint={t('shared.content.expandHint')} setSpoilerExpanded={setSpoilerExpanded} highlighted={highlighted} @@ -53,7 +60,7 @@ const TimelineContent: React.FC = ({ setSpoilerExpanded }) => { emojis={status.emojis} mentions={status.mentions} tags={status.tags} - numberOfLines={highlighted ? 999 : undefined} + numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined} disableDetails={disableDetails} /> )} diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx index 5dafee11..7a4db11e 100644 --- a/src/components/Timeline/Shared/Context.tsx +++ b/src/components/Timeline/Shared/Context.tsx @@ -16,6 +16,7 @@ type ContextType = { }> highlighted?: boolean + inThread?: boolean disableDetails?: boolean disableOnPress?: boolean } diff --git a/src/components/Timeline/Shared/HeaderConversation.tsx b/src/components/Timeline/Shared/HeaderConversation.tsx index baa255d7..51dbb870 100644 --- a/src/components/Timeline/Shared/HeaderConversation.tsx +++ b/src/components/Timeline/Shared/HeaderConversation.tsx @@ -8,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager' import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { Pressable, View } from 'react-native' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' import StatusContext from './Context' import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedMuted from './HeaderShared/Muted' diff --git a/src/components/Timeline/Shared/Poll.tsx b/src/components/Timeline/Shared/Poll.tsx index 6a21fcae..933d3da9 100644 --- a/src/components/Timeline/Shared/Poll.tsx +++ b/src/components/Timeline/Shared/Poll.tsx @@ -16,7 +16,7 @@ import { maxBy } from 'lodash' import React, { useCallback, useContext, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Pressable, View } from 'react-native' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' import StatusContext from './Context' const TimelinePoll: React.FC = () => { diff --git a/src/components/Timeline/Shared/Translate.tsx b/src/components/Timeline/Shared/Translate.tsx index cdbd66b9..d55369dd 100644 --- a/src/components/Timeline/Shared/Translate.tsx +++ b/src/components/Timeline/Shared/Translate.tsx @@ -56,7 +56,7 @@ const TimelineTranslate = () => { : settingsLanguage || Localization.locale || 'en' const [enabled, setEnabled] = useState(false) - const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({ + const { refetch, data, isFetching, isSuccess, isError } = useTranslateQuery({ source: detectedLanguage.language, target: targetLanguage, text, @@ -111,7 +111,7 @@ const TimelineTranslate = () => { {isError @@ -125,7 +125,7 @@ const TimelineTranslate = () => { }) : t('shared.translate.default')} - {isLoading ? ( + {isFetching ? ( + permissions + ? !!( + (typeof permissions === 'string' ? parseInt(permissions || '0') : permissions) & permission + ) + : false diff --git a/src/helpers/queryClient.ts b/src/helpers/queryClient.ts index 5c9848d8..90649981 100644 --- a/src/helpers/queryClient.ts +++ b/src/helpers/queryClient.ts @@ -1,4 +1,4 @@ -import { QueryClient } from 'react-query' +import { QueryClient } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } }) diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 8a4d2329..f2a5a1c6 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -5,6 +5,7 @@ "cancel": "Cancel", "discard": "Discard", "continue": "Continue", + "create": "Create", "delete": "Delete", "done": "Done" }, diff --git a/src/i18n/en/screens/actions.json b/src/i18n/en/screens/actions.json index 942accaf..970345fa 100644 --- a/src/i18n/en/screens/actions.json +++ b/src/i18n/en/screens/actions.json @@ -2,19 +2,6 @@ "content": { "altText": { "heading": "Alternative Text" - }, - "notificationsFilter": { - "heading": "Show notification types", - "content": { - "follow": "$t(screenTabs:me.push.follow.heading)", - "follow_request": "Follow request", - "favourite": "$t(screenTabs:me.push.favourite.heading)", - "reblog": "$t(screenTabs:me.push.reblog.heading)", - "mention": "$t(screenTabs:me.push.mention.heading)", - "poll": "$t(screenTabs:me.push.poll.heading)", - "status": "Toot from subscribed users", - "update": "Reblog has been edited" - } } } } \ No newline at end of file diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index c4da7ba8..fe32807e 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -6,8 +6,9 @@ "public": { "name": "", "segments": { - "left": "Federated", - "right": "Local" + "federated": "Federated", + "local": "Local", + "trending": "Trending" } }, "notifications": { @@ -24,9 +25,22 @@ } }, "notifications": { - "filter": { + "filters": { "accessibilityLabel": "Filter", - "accessibilityHint": "Filter shown notifications' types" + "accessibilityHint": "Filter shown notifications' types", + "title": "Show notifications", + "options": { + "follow": "$t(screenTabs:me.push.follow.heading)", + "follow_request": "Follow request", + "favourite": "$t(screenTabs:me.push.favourite.heading)", + "reblog": "$t(screenTabs:me.push.reblog.heading)", + "mention": "$t(screenTabs:me.push.mention.heading)", + "poll": "$t(screenTabs:me.push.poll.heading)", + "status": "Toot from subscribed users", + "update": "Reblog has been edited", + "admin.sign_up": "$t(screenTabs:me.push.admin.sign_up.heading)", + "admin.report": "$t(screenTabs:me.push.admin.report.heading)" + } } }, "me": { @@ -40,6 +54,9 @@ "favourites": { "name": "Favourites" }, + "followedTags": { + "name": "Followed hashtags" + }, "fontSize": { "name": "Toot Font Size" }, @@ -53,7 +70,7 @@ "name": "Users in list: {{list}}" }, "listAdd": { - "name": "Add a List" + "name": "Create a List" }, "listEdit": { "name": "Edit List Details" diff --git a/src/screens/Actions.tsx b/src/screens/Actions.tsx index 54626356..0a8b3c3a 100644 --- a/src/screens/Actions.tsx +++ b/src/screens/Actions.tsx @@ -15,7 +15,6 @@ import Animated, { } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import ActionsAltText from './Actions/AltText' -import ActionsNotificationsFilter from './Actions/NotificationsFilter' const ScreenActions = ({ route: { params }, @@ -53,8 +52,6 @@ const ScreenActions = ({ const actions = () => { switch (params.type) { - case 'notifications_filter': - return case 'alt_text': return } diff --git a/src/screens/Actions/NotificationsFilter.tsx b/src/screens/Actions/NotificationsFilter.tsx deleted file mode 100644 index 928bb5ae..00000000 --- a/src/screens/Actions/NotificationsFilter.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import Button from '@components/Button' -import MenuContainer from '@components/Menu/Container' -import MenuHeader from '@components/Menu/Header' -import MenuRow from '@components/Menu/Row' -import { useNavigation } from '@react-navigation/native' -import { - checkInstanceFeature, - getInstanceNotificationsFilter, - updateInstanceNotificationsFilter -} from '@utils/slices/instancesSlice' -import { StyleConstants } from '@utils/styles/constants' -import React, { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' -import { useQueryClient } from 'react-query' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' -import { useAppDispatch } from '@root/store' - -const ActionsNotificationsFilter: React.FC = () => { - const navigation = useNavigation() - const { t } = useTranslation('screenActions') - - const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }] - const queryClient = useQueryClient() - - const dispatch = useAppDispatch() - const instanceNotificationsFilter = useSelector( - getInstanceNotificationsFilter - ) - - if (!instanceNotificationsFilter) { - navigation.goBack() - return null - } - - const hasTypeStatus = useSelector( - checkInstanceFeature('notification_type_status') - ) - const hasTypeUpdate = useSelector( - checkInstanceFeature('notification_type_update') - ) - const options = useMemo(() => { - return ( - instanceNotificationsFilter && - ( - [ - 'follow', - 'follow_request', - 'favourite', - 'reblog', - 'mention', - 'poll', - 'status', - 'update' - ] as [ - 'follow', - 'follow_request', - 'favourite', - 'reblog', - 'mention', - 'poll', - 'status', - 'update' - ] - ) - .filter(type => { - switch (type) { - case 'status': - return hasTypeStatus - case 'update': - return hasTypeUpdate - default: - return true - } - }) - .map(type => ( - - dispatch( - updateInstanceNotificationsFilter({ - ...instanceNotificationsFilter, - [type]: !instanceNotificationsFilter[type] - }) - ) - } - /> - )) - ) - }, [instanceNotificationsFilter, hasTypeStatus, hasTypeUpdate]) - - return ( - <> - - - {options} - - )} diff --git a/src/screens/Compose/utils/parseState.ts b/src/screens/Compose/utils/parseState.ts index 688c0e7c..811c3371 100644 --- a/src/screens/Compose/utils/parseState.ts +++ b/src/screens/Compose/utils/parseState.ts @@ -65,6 +65,7 @@ const composeParseState = ( }), ...(params.incomingStatus.media_attachments && { attachments: { + ...(params.type === 'edit' && { disallowEditing: true }), sensitive: params.incomingStatus.sensitive, uploads: params.incomingStatus.media_attachments.map(media => ({ remote: media diff --git a/src/screens/Compose/utils/processText.tsx b/src/screens/Compose/utils/processText.tsx index 46be6143..7cd2c0d6 100644 --- a/src/screens/Compose/utils/processText.tsx +++ b/src/screens/Compose/utils/processText.tsx @@ -1,7 +1,6 @@ import LinkifyIt from 'linkify-it' import { debounce, differenceWith, isEqual } from 'lodash' import React, { Dispatch } from 'react' -import { FetchOptions } from 'react-query/types/core/query' import { useTheme } from '@utils/styles/ThemeManager' import { ComposeAction, ComposeState } from './types' import { instanceConfigurationStatusCharsURL } from '../Root' @@ -12,7 +11,6 @@ export interface Params { textInput: ComposeState['textInputFocus']['current'] composeDispatch: Dispatch content: string - refetch?: (options?: FetchOptions | undefined) => Promise disableDebounce?: boolean } diff --git a/src/screens/Compose/utils/types.d.ts b/src/screens/Compose/utils/types.d.ts index 3500bee6..f5457e46 100644 --- a/src/screens/Compose/utils/types.d.ts +++ b/src/screens/Compose/utils/types.d.ts @@ -51,6 +51,7 @@ export type ComposeState = { expire: '300' | '1800' | '3600' | '21600' | '86400' | '259200' | '604800' } attachments: { + disallowEditing?: boolean // https://github.com/mastodon/mastodon/pull/20878 sensitive: boolean uploads: ExtendedAttachment[] } @@ -59,8 +60,8 @@ export type ComposeState = { replyToStatus?: Mastodon.Status textInputFocus: { current: 'text' | 'spoiler' - refs: { text: RefObject, spoiler: RefObject } - isFocused: { text: MutableRefObject, spoiler: MutableRefObject } + refs: { text: RefObject; spoiler: RefObject } + isFocused: { text: MutableRefObject; spoiler: MutableRefObject } } } diff --git a/src/screens/Tabs.tsx b/src/screens/Tabs.tsx index d8f50cc9..f0b2ce9f 100644 --- a/src/screens/Tabs.tsx +++ b/src/screens/Tabs.tsx @@ -3,16 +3,10 @@ import haptics from '@components/haptics' import Icon from '@components/Icon' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import { useAppDispatch } from '@root/store' -import { - RootStackScreenProps, - ScreenTabsStackParamList -} from '@utils/navigation/navigators' -import { getPreviousTab } from '@utils/slices/contextsSlice' -import { - getInstanceAccount, - getInstanceActive -} from '@utils/slices/instancesSlice' +import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators' import { getVersionUpdate, retrieveVersionLatest } from '@utils/slices/appSlice' +import { getPreviousTab } from '@utils/slices/contextsSlice' +import { getInstanceAccount, getInstanceActive } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback, useEffect, useMemo } from 'react' import { Platform } from 'react-native' @@ -125,11 +119,7 @@ const ScreenTabs = React.memo( > - + () - -const TabLocal = React.memo( - ({ navigation }: ScreenTabsScreenProps<'Tab-Local'>) => { - const { t } = useTranslation('screenTabs') - - const { data: lists } = useListsQuery({}) - - const [queryKey, setQueryKey] = useState(['Timeline', { page: 'Following' }]) - - usePopToTop() - - return ( - - ( - - - 0} - content={ - queryKey[1].page === 'List' && queryKey[1].list?.length - ? lists?.find(list => list.id === queryKey[1].list)?.title - : t('tabs.local.name') - } - /> - - - - {lists?.length - ? [ - { - key: 'default', - item: { - onSelect: () => setQueryKey(['Timeline', { page: 'Following' }]), - disabled: queryKey[1].page === 'Following', - destructive: false, - hidden: false - }, - title: t('tabs.local.name'), - icon: '' - }, - ...lists?.map(list => ({ - key: list.id, - item: { - onSelect: () => - setQueryKey(['Timeline', { page: 'List', list: list.id }]), - disabled: queryKey[1].page === 'List' && queryKey[1].list === list.id, - destructive: false, - hidden: false - }, - title: list.title, - icon: '' - })) - ].map(menu => ( - - - - - )) - : undefined} - - - ), - headerRight: () => ( - navigation.navigate('Tab-Local', { screen: 'Tab-Shared-Search' })} - /> - ) - }} - children={() => ( - - }} - /> - )} - /> - {TabShared({ Stack })} - - ) - }, - () => true -) - -export default TabLocal diff --git a/src/screens/Tabs/Local/Root.tsx b/src/screens/Tabs/Local/Root.tsx new file mode 100644 index 00000000..f3da4ff4 --- /dev/null +++ b/src/screens/Tabs/Local/Root.tsx @@ -0,0 +1,96 @@ +import { HeaderCenter, HeaderRight } from '@components/Header' +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 usePopToTop from '@utils/navigation/usePopToTop' +import { useListsQuery } from '@utils/queryHooks/lists' +import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import * as DropdownMenu from 'zeego/dropdown-menu' + +const Root: React.FC> = ({ + navigation +}) => { + const { t } = useTranslation('screenTabs') + + const { data: lists } = useListsQuery() + + const [queryKey, setQueryKey] = useState(['Timeline', { page: 'Following' }]) + + useEffect(() => { + navigation.setOptions({ + headerTitle: () => ( + + + 0} + content={ + queryKey[1].page === 'List' && queryKey[1].list?.length + ? lists?.find(list => list.id === queryKey[1].list)?.title + : t('tabs.local.name') + } + /> + + + + {lists?.length + ? [ + { + key: 'default', + item: { + onSelect: () => setQueryKey(['Timeline', { page: 'Following' }]), + disabled: queryKey[1].page === 'Following', + destructive: false, + hidden: false + }, + title: t('tabs.local.name'), + icon: '' + }, + ...lists?.map(list => ({ + key: list.id, + item: { + onSelect: () => setQueryKey(['Timeline', { page: 'List', list: list.id }]), + disabled: queryKey[1].page === 'List' && queryKey[1].list === list.id, + destructive: false, + hidden: false + }, + title: list.title, + icon: '' + })) + ].map(menu => ( + + + + + )) + : undefined} + + + ), + headerRight: () => ( + navigation.navigate('Tab-Shared-Search')} + /> + ) + }) + }, []) + + usePopToTop() + + return ( + + }} + /> + ) +} + +export default Root diff --git a/src/screens/Tabs/Local/index.tsx b/src/screens/Tabs/Local/index.tsx new file mode 100644 index 00000000..ea78dcbe --- /dev/null +++ b/src/screens/Tabs/Local/index.tsx @@ -0,0 +1,18 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack' +import { TabLocalStackParamList } from '@utils/navigation/navigators' +import React from 'react' +import TabShared from '../Shared' +import Root from './Root' + +const Stack = createNativeStackNavigator() + +const TabLocal: React.FC = () => { + return ( + + + {TabShared({ Stack })} + + ) +} + +export default TabLocal diff --git a/src/screens/Tabs/Me.tsx b/src/screens/Tabs/Me.tsx deleted file mode 100644 index 650dbff9..00000000 --- a/src/screens/Tabs/Me.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { HeaderCenter, HeaderLeft } from '@components/Header' -import { createNativeStackNavigator } from '@react-navigation/native-stack' -import { TabMeStackParamList } from '@utils/navigation/navigators' -import React from 'react' -import { useTranslation } from 'react-i18next' -import { Platform } from 'react-native' -import TabMeBookmarks from './Me/Bookmarks' -import TabMeConversations from './Me/Cconversations' -import TabMeFavourites from './Me/Favourites' -import TabMeList from './Me/List' -import TabMeListAccounts from './Me/List/Accounts' -import TabMeListEdit from './Me/List/Edit' -import TabMeListList from './Me/List/List' -import TabMeProfile from './Me/Profile' -import TabMePush from './Me/Push' -import TabMeRoot from './Me/Root' -import TabMeSettings from './Me/Settings' -import TabMeSettingsFontsize from './Me/SettingsFontsize' -import TabMeSettingsLanguage from './Me/SettingsLanguage' -import TabMeSwitch from './Me/Switch' -import TabShared from './Shared' - -const Stack = createNativeStackNavigator() - -const TabMe = React.memo( - () => { - const { t } = useTranslation('screenTabs') - - return ( - - - ({ - title: t('me.stacks.bookmarks.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.conversations.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.favourites.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.list.name', { list: route.params.title }), - ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.listAccounts.name', { list: params.title }), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - - ({ - title: t('me.stacks.lists.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - - ({ - title: t('me.stacks.push.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.goBack()} /> - })} - /> - ({ - title: t('me.stacks.settings.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.fontSize.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.language.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - presentation: 'modal', - headerShown: true, - title: t('me.stacks.switch.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => - }), - headerLeft: () => ( - navigation.goBack()} /> - ) - })} - /> - - {TabShared({ Stack })} - - ) - }, - () => true -) - -export default TabMe diff --git a/src/screens/Tabs/Me/FollowedTags.tsx b/src/screens/Tabs/Me/FollowedTags.tsx new file mode 100644 index 00000000..32a2af98 --- /dev/null +++ b/src/screens/Tabs/Me/FollowedTags.tsx @@ -0,0 +1,71 @@ +import Button from '@components/Button' +import haptics from '@components/haptics' +import ComponentHashtag from '@components/Hashtag' +import { displayMessage } from '@components/Message' +import ComponentSeparator from '@components/Separator' +import { TabMeStackScreenProps } from '@utils/navigation/navigators' +import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags' +import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList } from 'react-native-gesture-handler' + +const TabMeFollowedTags: React.FC> = ({ + navigation +}) => { + const { t } = useTranslation('screenTabs') + + const { data, fetchNextPage, refetch } = useFollowedTagsQuery() + const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : [] + useEffect(() => { + if (flattenData.length === 0) { + navigation.goBack() + } + }, [flattenData.length]) + + const mutation = useTagsMutation({ + onSuccess: () => { + haptics('Light') + refetch() + }, + onError: (err: any, { to }) => { + displayMessage({ + type: 'error', + message: t('common:message.error.message', { + function: to ? t('shared.hashtag.follow') : t('shared.hashtag.unfollow') + }), + ...(err.status && + typeof err.status === 'number' && + err.data && + err.data.error && + typeof err.data.error === 'string' && { + description: err.data.error + }) + }) + } + }) + + return ( + ( + {}} + children={ +