This commit is contained in:
Zhiyuan Zheng 2020-12-20 17:53:24 +01:00
parent 98a60df9d1
commit 09b960d368
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
15 changed files with 228 additions and 158 deletions

1
src/@types/app.d.ts vendored
View File

@ -57,7 +57,6 @@ declare namespace QueryKey {
type Timeline = [
Pages,
{
page: Pages
hashtag?: Mastodon.Tag['name']
list?: Mastodon.List['id']
toot?: Mastodon.Status

View File

@ -84,8 +84,7 @@ const renderNode = ({
}}
onPress={() => {
navigation.navigate('Screen-Shared-Webview', {
uri: href,
domain: domain[1]
uri: href
})
}}
>

View File

@ -39,7 +39,12 @@ const Timelines: React.FC<Props> = ({ 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<Props> = ({ name, content }) => {
key: App.Pages
}
}) => {
return <Timeline page={route.key} />
return (
(localRegistered || route.key === 'RemotePublic') && (
<Timeline page={route.key} />
)
)
}
return (

View File

@ -40,7 +40,6 @@ const Timeline: React.FC<Props> = ({
const queryKey: QueryKey.Timeline = [
page,
{
page,
...(hashtag && { hashtag }),
...(list && { list }),
...(toot && { toot }),
@ -91,27 +90,32 @@ const Timeline: React.FC<Props> = ({
}, [status])
const flKeyExtrator = useCallback(({ id }) => id, [])
const flRenderItem = useCallback(({ item, index }) => {
switch (page) {
case 'Conversations':
return <TimelineConversation item={item} queryKey={queryKey} />
case 'Notifications':
return <TimelineNotifications notification={item} queryKey={queryKey} />
default:
return (
<TimelineDefault
item={item}
queryKey={queryKey}
index={index}
{...(flattenPinnedLength &&
flattenPinnedLength[0] && {
pinnedLength: flattenPinnedLength[0]
})}
{...(toot && toot.id === item.id && { highlighted: true })}
/>
)
}
}, [])
const flRenderItem = useCallback(
({ item, index }) => {
switch (page) {
case 'Conversations':
return <TimelineConversation item={item} queryKey={queryKey} />
case 'Notifications':
return (
<TimelineNotifications notification={item} queryKey={queryKey} />
)
default:
return (
<TimelineDefault
item={item}
queryKey={queryKey}
index={index}
{...(flattenPinnedLength &&
flattenPinnedLength[0] && {
pinnedLength: flattenPinnedLength[0]
})}
{...(toot && toot.id === item.id && { highlighted: true })}
/>
)
}
},
[flattenPinnedLength[0]]
)
const flItemSeparatorComponent = useCallback(
({ leadingItem }) => (
<TimelineSeparator

View File

@ -74,7 +74,12 @@ const TimelineConversation: React.FC<Props> = ({
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<TimelineActions queryKey={queryKey} status={item.last_status!} />
<TimelineActions
queryKey={queryKey}
status={item.last_status!}
reblog={false}
sameAccountRoot={false}
/>
</View>
</View>
)

View File

@ -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<Props> = ({
pinnedLength,
highlighted = false
}) => {
const localAccountId = useSelector(getLocalAccountId)
const isRemotePublic = queryKey[0] === 'RemotePublic'
const navigation = useNavigation()
@ -68,6 +71,7 @@ const TimelineDefault: React.FC<Props> = ({
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<Props> = ({
{actualStatus.card && <TimelineCard card={actualStatus.card} />}
</View>
),
[actualStatus.poll?.voted]
[actualStatus.poll]
)
return (
@ -95,6 +99,7 @@ const TimelineDefault: React.FC<Props> = ({
<TimelineHeaderDefault
{...(!isRemotePublic && { queryKey })}
status={actualStatus}
sameAccount={actualStatus.account.id === localAccountId}
/>
</View>
@ -112,6 +117,7 @@ const TimelineDefault: React.FC<Props> = ({
queryKey={queryKey}
status={actualStatus}
reblog={item.reblog ? true : false}
sameAccountRoot={item.account.id === localAccountId}
/>
</View>
)}

View File

@ -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<Props> = ({
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<Props> = ({
/>
)}
{notification.status.poll && (
<TimelinePoll queryKey={queryKey} status={notification.status} />
<TimelinePoll
queryKey={queryKey}
poll={notification.status.poll}
reblog={false}
sameAccount={notification.account.id === localAccountId}
/>
)}
{notification.status.media_attachments.length > 0 && (
<TimelineAttachment
@ -100,7 +108,12 @@ const TimelineNotifications: React.FC<Props> = ({
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<TimelineActions queryKey={queryKey} status={notification.status} />
<TimelineActions
queryKey={queryKey}
status={notification.status}
reblog={false}
sameAccountRoot={notification.account.id === localAccountId}
/>
</View>
)}
</View>

View File

@ -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<Props> = ({ queryKey, status, reblog }) => {
const TimelineActions: React.FC<Props> = ({
queryKey,
status,
reblog,
sameAccountRoot
}) => {
const navigation = useNavigation()
const { theme } = useTheme()
const iconColor = theme.secondary
@ -65,9 +73,28 @@ const TimelineActions: React.FC<Props> = ({ 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<TimelineData>(queryKey, old => {
let tootIndex = -1
const pageIndex = findIndex(old?.pages, page => {

View File

@ -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()

View File

@ -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<Props> = ({ queryKey, status }) => {
const TimelineHeaderDefault: React.FC<Props> = ({
queryKey,
status,
sameAccount
}) => {
const domain = status.uri
? status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
: ''
@ -29,7 +34,6 @@ const TimelineHeaderDefault: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ queryKey, status }) => {
visible={modalVisible}
handleDismiss={() => setBottomSheetVisible(false)}
>
{status.account.id !== localAccountId && (
{!sameAccount && (
<HeaderDefaultActionsAccount
queryKey={queryKey}
accountId={status.account.id}
@ -137,7 +142,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
/>
)}
{status.account.id === localAccountId && (
{sameAccount && (
<HeaderDefaultActionsStatus
queryKey={queryKey}
status={status}

View File

@ -99,7 +99,6 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
})
break
case 'delete':
console.log('deleting toot')
queryClient.setQueryData<TimelineData>(
queryKey,
old =>

View File

@ -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<Mastodon.Status['poll']>
reblog: boolean
sameAccount: boolean
}
const TimelinePoll: React.FC<Props> = ({ queryKey, poll, reblog }) => {
const TimelinePoll: React.FC<Props> = ({
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<TimelineData>(queryKey, old => {
let tootIndex = -1
@ -104,28 +84,49 @@ const TimelinePoll: React.FC<Props> = ({ 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 (
<View style={styles.button}>
<ButtonRow
onPress={() => {
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}
/>
</View>
)
} else {
return (
<View style={styles.button}>
<ButtonRow
onPress={() => mutation.mutate({ id: poll.id })}
{...(mutation.isLoading ? { icon: 'loader' } : { text: '刷新' })}
disabled={mutation.isLoading}
/>
</View>
)
}
}
}
const pollExpiration = useMemo(() => {
// how many voted
if (poll.expired) {
return (
<Text style={[styles.expiration, { color: theme.secondary }]}>
@ -178,7 +179,10 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, poll, reblog }) => {
)}
</View>
<Text style={[styles.percentage, { color: theme.primary }]}>
{Math.round((option.votes_count / poll.votes_count) * 100)}%
{poll.votes_count
? Math.round((option.votes_count / poll.voters_count) * 100)
: 0}
%
</Text>
</View>
@ -187,7 +191,7 @@ const TimelinePoll: React.FC<Props> = ({ 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<Props> = ({ queryKey, poll, reblog }) => {
)
)}
<View style={styles.meta}>
{!poll.expired && !poll.own_votes?.length && (
<View style={styles.button}>
<ButtonRow
onPress={() => {
if (poll.multiple) {
mutate({ id: poll.id, options: multipleOptions })
} else {
mutate({ id: poll.id, options: singleOptions })
}
}}
text='投票'
/>
</View>
)}
{pollButton()}
<Text style={[styles.votes, { color: theme.secondary }]}>
{poll.voters_count || 0}{' • '}
</Text>
@ -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

View File

@ -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<Props> = ({ route: { params }, navigation }) => {
const { theme } = useTheme()
const queryClient = useQueryClient()
const [hasKeyboard, setHasKeyboard] = useState(false)
useEffect(() => {
@ -449,7 +451,8 @@ const Compose: React.FC<Props> = ({ 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<Props> = ({ route: { params }, navigation }) => {
{
text: '好的',
onPress: () => {
// clear homepage cache
queryClient.invalidateQueries(['Following'])
navigation.goBack()
}
}

View File

@ -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(
() => (
<View style={styles.emptyBase}>
<Text
style={[
styles.emptyDefault,
styles.emptyFontSize,
{ color: theme.primary }
]}
>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>@username@domain</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>#example</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
</View>
),
[]
() =>
status === 'loading' ? (
<View style={styles.emptyBase}>
<ActivityIndicator />
</View>
) : (
<View style={styles.emptyBase}>
<Text
style={[
styles.emptyDefault,
styles.emptyFontSize,
{ color: theme.primary }
]}
>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>@username@domain</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>#example</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
</View>
),
[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}

View File

@ -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
}
}