diff --git a/src/@types/app.d.ts b/src/@types/app.d.ts index a2cd7817..49e772cd 100644 --- a/src/@types/app.d.ts +++ b/src/@types/app.d.ts @@ -57,7 +57,6 @@ declare namespace QueryKey { type Timeline = [ Pages, { - page: Pages hashtag?: Mastodon.Tag['name'] list?: Mastodon.List['id'] toot?: Mastodon.Status diff --git a/src/components/ParseContent.tsx b/src/components/ParseContent.tsx index abceea38..50442a35 100644 --- a/src/components/ParseContent.tsx +++ b/src/components/ParseContent.tsx @@ -84,8 +84,7 @@ const renderNode = ({ }} onPress={() => { navigation.navigate('Screen-Shared-Webview', { - uri: href, - domain: domain[1] + uri: href }) }} > diff --git a/src/components/Timelines.tsx b/src/components/Timelines.tsx index 025dfdbb..1f136b58 100644 --- a/src/components/Timelines.tsx +++ b/src/components/Timelines.tsx @@ -39,7 +39,12 @@ const Timelines: React.FC = ({ name, content }) => { }) }, []) - const [routes] = useState(content.map(p => ({ key: p.page }))) + const routes = content + .filter(p => + localRegistered ? p : p.page === 'RemotePublic' ? p : undefined + ) + .map(p => ({ key: p.page })) + const renderScene = ({ route }: { @@ -47,7 +52,11 @@ const Timelines: React.FC = ({ name, content }) => { key: App.Pages } }) => { - return + return ( + (localRegistered || route.key === 'RemotePublic') && ( + + ) + ) } return ( diff --git a/src/components/Timelines/Timeline.tsx b/src/components/Timelines/Timeline.tsx index b6bf48a3..5d0125e2 100644 --- a/src/components/Timelines/Timeline.tsx +++ b/src/components/Timelines/Timeline.tsx @@ -40,7 +40,6 @@ const Timeline: React.FC = ({ const queryKey: QueryKey.Timeline = [ page, { - page, ...(hashtag && { hashtag }), ...(list && { list }), ...(toot && { toot }), @@ -91,27 +90,32 @@ const Timeline: React.FC = ({ }, [status]) const flKeyExtrator = useCallback(({ id }) => id, []) - const flRenderItem = useCallback(({ item, index }) => { - switch (page) { - case 'Conversations': - return - case 'Notifications': - return - default: - return ( - - ) - } - }, []) + const flRenderItem = useCallback( + ({ item, index }) => { + switch (page) { + case 'Conversations': + return + case 'Notifications': + return ( + + ) + default: + return ( + + ) + } + }, + [flattenPinnedLength[0]] + ) const flItemSeparatorComponent = useCallback( ({ leadingItem }) => ( = ({ : StyleConstants.Avatar.M + StyleConstants.Spacing.S }} > - + ) diff --git a/src/components/Timelines/Timeline/Default.tsx b/src/components/Timelines/Timeline/Default.tsx index 499796f5..336d78d4 100644 --- a/src/components/Timelines/Timeline/Default.tsx +++ b/src/components/Timelines/Timeline/Default.tsx @@ -12,6 +12,8 @@ import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderD import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll' import { StyleConstants } from '@utils/styles/constants' +import { useSelector } from 'react-redux' +import { getLocalAccountId } from '@root/utils/slices/instancesSlice' export interface Props { item: Mastodon.Status @@ -29,6 +31,7 @@ const TimelineDefault: React.FC = ({ pinnedLength, highlighted = false }) => { + const localAccountId = useSelector(getLocalAccountId) const isRemotePublic = queryKey[0] === 'RemotePublic' const navigation = useNavigation() @@ -68,6 +71,7 @@ const TimelineDefault: React.FC = ({ queryKey={queryKey} poll={actualStatus.poll} reblog={item.reblog ? true : false} + sameAccount={actualStatus.account.id === localAccountId} /> )} {actualStatus.media_attachments.length > 0 && ( @@ -76,7 +80,7 @@ const TimelineDefault: React.FC = ({ {actualStatus.card && } ), - [actualStatus.poll?.voted] + [actualStatus.poll] ) return ( @@ -95,6 +99,7 @@ const TimelineDefault: React.FC = ({ @@ -112,6 +117,7 @@ const TimelineDefault: React.FC = ({ queryKey={queryKey} status={actualStatus} reblog={item.reblog ? true : false} + sameAccountRoot={item.account.id === localAccountId} /> )} diff --git a/src/components/Timelines/Timeline/Notifications.tsx b/src/components/Timelines/Timeline/Notifications.tsx index cb678a2b..ed50a12a 100644 --- a/src/components/Timelines/Timeline/Notifications.tsx +++ b/src/components/Timelines/Timeline/Notifications.tsx @@ -12,6 +12,8 @@ import TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/He import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll' import { StyleConstants } from '@utils/styles/constants' +import { useSelector } from 'react-redux' +import { getLocalAccountId } from '@root/utils/slices/instancesSlice' export interface Props { notification: Mastodon.Notification @@ -24,6 +26,7 @@ const TimelineNotifications: React.FC = ({ queryKey, highlighted = false }) => { + const localAccountId = useSelector(getLocalAccountId) const navigation = useNavigation() const actualAccount = notification.status ? notification.status.account @@ -61,7 +64,12 @@ const TimelineNotifications: React.FC = ({ /> )} {notification.status.poll && ( - + )} {notification.status.media_attachments.length > 0 && ( = ({ : StyleConstants.Avatar.M + StyleConstants.Spacing.S }} > - + )} diff --git a/src/components/Timelines/Timeline/Shared/Actions.tsx b/src/components/Timelines/Timeline/Shared/Actions.tsx index cb98a58c..6fed1270 100644 --- a/src/components/Timelines/Timeline/Shared/Actions.tsx +++ b/src/components/Timelines/Timeline/Shared/Actions.tsx @@ -11,6 +11,8 @@ import { useNavigation } from '@react-navigation/native' import getCurrentTab from '@utils/getCurrentTab' import { findIndex } from 'lodash' import { TimelineData } from '../../Timeline' +import { useSelector } from 'react-redux' +import { getLocalAccountId } from '@root/utils/slices/instancesSlice' const fireMutation = async ({ id, @@ -49,9 +51,15 @@ export interface Props { queryKey: QueryKey.Timeline status: Mastodon.Status reblog: boolean + sameAccountRoot: boolean } -const TimelineActions: React.FC = ({ queryKey, status, reblog }) => { +const TimelineActions: React.FC = ({ + queryKey, + status, + reblog, + sameAccountRoot +}) => { const navigation = useNavigation() const { theme } = useTheme() const iconColor = theme.secondary @@ -65,9 +73,28 @@ const TimelineActions: React.FC = ({ queryKey, status, reblog }) => { const oldData = queryClient.getQueryData(queryKey) switch (type) { + // Update each specific page case 'favourite': case 'reblog': case 'bookmark': + if (type === 'favourite' && queryKey[0] === 'Favourites') { + queryClient.invalidateQueries(['Favourites']) + break + } + if ( + type === 'reblog' && + queryKey[0] === 'Following' && + prevState === true && + sameAccountRoot + ) { + queryClient.invalidateQueries(['Following']) + break + } + if (type === 'bookmark' && queryKey[0] === 'Bookmarks') { + queryClient.invalidateQueries(['Bookmarks']) + break + } + queryClient.setQueryData(queryKey, old => { let tootIndex = -1 const pageIndex = findIndex(old?.pages, page => { diff --git a/src/components/Timelines/Timeline/Shared/HeaderConversation.tsx b/src/components/Timelines/Timeline/Shared/HeaderConversation.tsx index 4db8a29c..443f3782 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderConversation.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderConversation.tsx @@ -23,7 +23,7 @@ const fireMutation = async ({ id }: { id: string }) => { instance: 'local', url: `conversations/${id}` }) - console.log(res) + if (!res.body.error) { toast({ type: 'success', content: '删除私信成功' }) return Promise.resolve() diff --git a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx index 6108b69e..6ce9e5e1 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx @@ -5,7 +5,7 @@ import { Feather } from '@expo/vector-icons' import Emojis from '@components/Timelines/Timeline/Shared/Emojis' import relativeTime from '@utils/relativeTime' -import { getLocalAccountId, getLocalUrl } from '@utils/slices/instancesSlice' +import { getLocalUrl } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import BottomSheet from '@components/BottomSheet' import { useSelector } from 'react-redux' @@ -17,9 +17,14 @@ import HeaderDefaultActionsDomain from '@components/Timelines/Timeline/Shared/He export interface Props { queryKey?: QueryKey.Timeline status: Mastodon.Status + sameAccount: boolean } -const TimelineHeaderDefault: React.FC = ({ queryKey, status }) => { +const TimelineHeaderDefault: React.FC = ({ + queryKey, + status, + sameAccount +}) => { const domain = status.uri ? status.uri.split(new RegExp(/\/\/(.*?)\//))[1] : '' @@ -29,7 +34,6 @@ const TimelineHeaderDefault: React.FC = ({ queryKey, status }) => { const { theme } = useTheme() const navigation = useNavigation() - const localAccountId = useSelector(getLocalAccountId) const localDomain = useSelector(getLocalUrl) const [since, setSince] = useState(relativeTime(status.created_at)) const [modalVisible, setBottomSheetVisible] = useState(false) @@ -46,9 +50,10 @@ const TimelineHeaderDefault: React.FC = ({ queryKey, status }) => { const onPressAction = useCallback(() => setBottomSheetVisible(true), []) const onPressApplication = useCallback(() => { - navigation.navigate('Screen-Shared-Webview', { - uri: status.application!.website - }) + status.application!.website && + navigation.navigate('Screen-Shared-Webview', { + uri: status.application!.website + }) }, []) const pressableAction = useMemo( @@ -128,7 +133,7 @@ const TimelineHeaderDefault: React.FC = ({ queryKey, status }) => { visible={modalVisible} handleDismiss={() => setBottomSheetVisible(false)} > - {status.account.id !== localAccountId && ( + {!sameAccount && ( = ({ queryKey, status }) => { /> )} - {status.account.id === localAccountId && ( + {sameAccount && ( = ({ }) break case 'delete': - console.log('deleting toot') queryClient.setQueryData( queryKey, old => diff --git a/src/components/Timelines/Timeline/Shared/Poll.tsx b/src/components/Timelines/Timeline/Shared/Poll.tsx index 2a16924c..022b0f41 100644 --- a/src/components/Timelines/Timeline/Shared/Poll.tsx +++ b/src/components/Timelines/Timeline/Shared/Poll.tsx @@ -18,26 +18,26 @@ const fireMutation = async ({ options }: { id: string - options: { [key: number]: boolean } + options?: { [key: number]: boolean } }) => { const formData = new FormData() - Object.keys(options).forEach(option => { - // @ts-ignore - if (options[option]) { - formData.append('choices[]', option) - } - }) + options && + Object.keys(options).forEach(option => { + // @ts-ignore + if (options[option]) { + formData.append('choices[]', option) + } + }) const res = await client({ - method: 'post', + method: options ? 'post' : 'get', instance: 'local', - url: `polls/${id}/votes`, - body: formData + url: options ? `polls/${id}/votes` : `polls/${id}`, + ...(options && { body: formData }) }) if (res.body.id === id) { - toast({ type: 'success', content: '投票成功成功' }) - return Promise.resolve() + return Promise.resolve(res.body as Mastodon.Poll) } else { toast({ type: 'error', @@ -52,40 +52,20 @@ export interface Props { queryKey: QueryKey.Timeline poll: NonNullable reblog: boolean + sameAccount: boolean } -const TimelinePoll: React.FC = ({ queryKey, poll, reblog }) => { +const TimelinePoll: React.FC = ({ + queryKey, + poll, + reblog, + sameAccount +}) => { const { theme } = useTheme() - const queryClient = useQueryClient() - const { mutate } = useMutation(fireMutation, { - onMutate: ({ id, options }) => { + const mutation = useMutation(fireMutation, { + onSuccess: (data, { id }) => { queryClient.cancelQueries(queryKey) - const oldData = queryClient.getQueryData(queryKey) - - const updatePoll = (poll: Mastodon.Poll): Mastodon.Poll => { - const myVotes = Object.keys(options).filter( - // @ts-ignore - option => options[option] - ) - const myVotesInt = myVotes.map(option => parseInt(option)) - - return { - ...poll, - votes_count: poll.votes_count - ? poll.votes_count + myVotes.length - : myVotes.length, - voters_count: poll.voters_count ? poll.voters_count + 1 : 1, - voted: true, - own_votes: myVotesInt, - options: poll.options.map((o, i) => { - if (myVotesInt.includes(i)) { - o.votes_count = o.votes_count + 1 - } - return o - }) - } - } queryClient.setQueryData(queryKey, old => { let tootIndex = -1 @@ -104,28 +84,49 @@ const TimelinePoll: React.FC = ({ queryKey, poll, reblog }) => { if (pageIndex >= 0 && tootIndex >= 0) { if (reblog) { - old!.pages[pageIndex].toots[tootIndex].reblog!.poll = updatePoll( - old!.pages[pageIndex].toots[tootIndex].reblog!.poll! - ) + old!.pages[pageIndex].toots[tootIndex].reblog!.poll = data } else { - old!.pages[pageIndex].toots[tootIndex].poll = updatePoll( - old!.pages[pageIndex].toots[tootIndex].poll! - ) + old!.pages[pageIndex].toots[tootIndex].poll = data } } return old }) - - return oldData - }, - onError: (err, _, oldData) => { - toast({ type: 'error', content: '请重试' }) - queryClient.setQueryData(queryKey, oldData) } }) + const pollButton = () => { + if (!poll.expired) { + if (!sameAccount && !poll.voted) { + return ( + + { + if (poll.multiple) { + mutation.mutate({ id: poll.id, options: multipleOptions }) + } else { + mutation.mutate({ id: poll.id, options: singleOptions }) + } + }} + {...(mutation.isLoading ? { icon: 'loader' } : { text: '投票' })} + disabled={mutation.isLoading} + /> + + ) + } else { + return ( + + mutation.mutate({ id: poll.id })} + {...(mutation.isLoading ? { icon: 'loader' } : { text: '刷新' })} + disabled={mutation.isLoading} + /> + + ) + } + } + } + const pollExpiration = useMemo(() => { - // how many voted if (poll.expired) { return ( @@ -178,7 +179,10 @@ const TimelinePoll: React.FC = ({ queryKey, poll, reblog }) => { )} - {Math.round((option.votes_count / poll.votes_count) * 100)}% + {poll.votes_count + ? Math.round((option.votes_count / poll.voters_count) * 100) + : 0} + % @@ -187,7 +191,7 @@ const TimelinePoll: React.FC = ({ queryKey, poll, reblog }) => { styles.background, { width: `${Math.round( - (option.votes_count / poll.votes_count) * 100 + (option.votes_count / poll.voters_count) * 100 )}%`, backgroundColor: theme.border } @@ -234,20 +238,7 @@ const TimelinePoll: React.FC = ({ queryKey, poll, reblog }) => { ) )} - {!poll.expired && !poll.own_votes?.length && ( - - { - if (poll.multiple) { - mutate({ id: poll.id, options: multipleOptions }) - } else { - mutate({ id: poll.id, options: singleOptions }) - } - }} - text='投票' - /> - - )} + {pollButton()} 已投{poll.voters_count || 0}人{' • '} @@ -299,7 +290,7 @@ const styles = StyleSheet.create({ top: 0, left: 0, height: '100%', - minWidth: 1, + minWidth: 2, borderTopRightRadius: 6, borderBottomRightRadius: 6 }, @@ -320,7 +311,4 @@ const styles = StyleSheet.create({ } }) -export default React.memo( - TimelinePoll, - (prev, next) => prev.poll.voted === next.poll.voted -) +export default TimelinePoll diff --git a/src/screens/Shared/Compose.tsx b/src/screens/Shared/Compose.tsx index b8dffa52..7305bf48 100644 --- a/src/screens/Shared/Compose.tsx +++ b/src/screens/Shared/Compose.tsx @@ -29,6 +29,7 @@ import { HeaderLeft, HeaderRight } from '@components/Header' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import formatText from '@screens/Shared/Compose/formatText' +import { useQueryClient } from 'react-query' const Stack = createNativeStackNavigator() @@ -316,6 +317,7 @@ export interface Props { const Compose: React.FC = ({ route: { params }, navigation }) => { const { theme } = useTheme() + const queryClient = useQueryClient() const [hasKeyboard, setHasKeyboard] = useState(false) useEffect(() => { @@ -449,7 +451,8 @@ const Compose: React.FC = ({ route: { params }, navigation }) => { composeState.poll.expire + composeState.attachments.sensitive + composeState.attachments.uploads.map(upload => upload.id) + - composeState.visibility + composeState.visibility + + (params?.type === 'edit' ? Math.random() : '') ).toString() }, body: formData @@ -462,7 +465,7 @@ const Compose: React.FC = ({ route: { params }, navigation }) => { { text: '好的', onPress: () => { - // clear homepage cache + queryClient.invalidateQueries(['Following']) navigation.goBack() } } diff --git a/src/screens/Shared/Search.tsx b/src/screens/Shared/Search.tsx index 81687480..ccc6d7b0 100644 --- a/src/screens/Shared/Search.tsx +++ b/src/screens/Shared/Search.tsx @@ -8,6 +8,7 @@ import { useTheme } from '@root/utils/styles/ThemeManager' import { debounce } from 'lodash' import React, { useCallback, useEffect, useMemo, useState } from 'react' import { + ActivityIndicator, Image, Pressable, SectionList, @@ -70,42 +71,47 @@ const ScreenSharedSearch: React.FC = () => { }, [searchTerm]) const listEmpty = useMemo( - () => ( - - - 输入关键词搜索用户、 - 话题标签或者 - 嘟文 - - - 高级搜索格式 - - - @username@domain - {' '} - 搜索用户 - - - #example - {' '}搜索话题标签 - - - URL - {' '}搜索指定嘟文 - - - URL - {' '}搜索指定用户 - - - ), - [] + () => + status === 'loading' ? ( + + + + ) : ( + + + 输入关键词搜索用户、 + 话题标签或者 + 嘟文 + + + 高级搜索格式 + + + @username@domain + {' '} + 搜索用户 + + + #example + {' '}搜索话题标签 + + + URL + {' '}搜索指定嘟文 + + + URL + {' '}搜索指定用户 + + + ), + [status] ) const sectionHeader = useCallback( ({ section: { title } }) => ( @@ -247,7 +253,6 @@ const ScreenSharedSearch: React.FC = () => { stickySectionHeadersEnabled sections={setctionData} ListEmptyComponent={listEmpty} - refreshing={status === 'loading'} keyboardShouldPersistTaps='always' renderSectionHeader={sectionHeader} renderSectionFooter={sectionFooter} diff --git a/src/utils/fetches/timelineFetch.ts b/src/utils/fetches/timelineFetch.ts index 7dc471be..213be553 100644 --- a/src/utils/fetches/timelineFetch.ts +++ b/src/utils/fetches/timelineFetch.ts @@ -20,10 +20,18 @@ export const timelineFetch = async ({ if (pageParam) { switch (pageParam.direction) { case 'prev': - params.min_id = pageParam.id + if (page === 'Bookmarks' || page === 'Favourites') { + params.max_id = pageParam.id + } else { + params.min_id = pageParam.id + } break case 'next': - params.max_id = pageParam.id + if (page === 'Bookmarks' || page === 'Favourites') { + params.min_id = pageParam.id + } else { + params.max_id = pageParam.id + } break } }