mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Updates
This commit is contained in:
1
src/@types/app.d.ts
vendored
1
src/@types/app.d.ts
vendored
@ -57,7 +57,6 @@ declare namespace QueryKey {
|
|||||||
type Timeline = [
|
type Timeline = [
|
||||||
Pages,
|
Pages,
|
||||||
{
|
{
|
||||||
page: Pages
|
|
||||||
hashtag?: Mastodon.Tag['name']
|
hashtag?: Mastodon.Tag['name']
|
||||||
list?: Mastodon.List['id']
|
list?: Mastodon.List['id']
|
||||||
toot?: Mastodon.Status
|
toot?: Mastodon.Status
|
||||||
|
@ -84,8 +84,7 @@ const renderNode = ({
|
|||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Screen-Shared-Webview', {
|
navigation.navigate('Screen-Shared-Webview', {
|
||||||
uri: href,
|
uri: href
|
||||||
domain: domain[1]
|
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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 = ({
|
const renderScene = ({
|
||||||
route
|
route
|
||||||
}: {
|
}: {
|
||||||
@ -47,7 +52,11 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
key: App.Pages
|
key: App.Pages
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
return <Timeline page={route.key} />
|
return (
|
||||||
|
(localRegistered || route.key === 'RemotePublic') && (
|
||||||
|
<Timeline page={route.key} />
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -40,7 +40,6 @@ const Timeline: React.FC<Props> = ({
|
|||||||
const queryKey: QueryKey.Timeline = [
|
const queryKey: QueryKey.Timeline = [
|
||||||
page,
|
page,
|
||||||
{
|
{
|
||||||
page,
|
|
||||||
...(hashtag && { hashtag }),
|
...(hashtag && { hashtag }),
|
||||||
...(list && { list }),
|
...(list && { list }),
|
||||||
...(toot && { toot }),
|
...(toot && { toot }),
|
||||||
@ -91,27 +90,32 @@ const Timeline: React.FC<Props> = ({
|
|||||||
}, [status])
|
}, [status])
|
||||||
|
|
||||||
const flKeyExtrator = useCallback(({ id }) => id, [])
|
const flKeyExtrator = useCallback(({ id }) => id, [])
|
||||||
const flRenderItem = useCallback(({ item, index }) => {
|
const flRenderItem = useCallback(
|
||||||
switch (page) {
|
({ item, index }) => {
|
||||||
case 'Conversations':
|
switch (page) {
|
||||||
return <TimelineConversation item={item} queryKey={queryKey} />
|
case 'Conversations':
|
||||||
case 'Notifications':
|
return <TimelineConversation item={item} queryKey={queryKey} />
|
||||||
return <TimelineNotifications notification={item} queryKey={queryKey} />
|
case 'Notifications':
|
||||||
default:
|
return (
|
||||||
return (
|
<TimelineNotifications notification={item} queryKey={queryKey} />
|
||||||
<TimelineDefault
|
)
|
||||||
item={item}
|
default:
|
||||||
queryKey={queryKey}
|
return (
|
||||||
index={index}
|
<TimelineDefault
|
||||||
{...(flattenPinnedLength &&
|
item={item}
|
||||||
flattenPinnedLength[0] && {
|
queryKey={queryKey}
|
||||||
pinnedLength: flattenPinnedLength[0]
|
index={index}
|
||||||
})}
|
{...(flattenPinnedLength &&
|
||||||
{...(toot && toot.id === item.id && { highlighted: true })}
|
flattenPinnedLength[0] && {
|
||||||
/>
|
pinnedLength: flattenPinnedLength[0]
|
||||||
)
|
})}
|
||||||
}
|
{...(toot && toot.id === item.id && { highlighted: true })}
|
||||||
}, [])
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[flattenPinnedLength[0]]
|
||||||
|
)
|
||||||
const flItemSeparatorComponent = useCallback(
|
const flItemSeparatorComponent = useCallback(
|
||||||
({ leadingItem }) => (
|
({ leadingItem }) => (
|
||||||
<TimelineSeparator
|
<TimelineSeparator
|
||||||
|
@ -74,7 +74,12 @@ const TimelineConversation: React.FC<Props> = ({
|
|||||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
: 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>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -12,6 +12,8 @@ import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderD
|
|||||||
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||||
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getLocalAccountId } from '@root/utils/slices/instancesSlice'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
item: Mastodon.Status
|
item: Mastodon.Status
|
||||||
@ -29,6 +31,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
pinnedLength,
|
pinnedLength,
|
||||||
highlighted = false
|
highlighted = false
|
||||||
}) => {
|
}) => {
|
||||||
|
const localAccountId = useSelector(getLocalAccountId)
|
||||||
const isRemotePublic = queryKey[0] === 'RemotePublic'
|
const isRemotePublic = queryKey[0] === 'RemotePublic'
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
@ -68,6 +71,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
poll={actualStatus.poll}
|
poll={actualStatus.poll}
|
||||||
reblog={item.reblog ? true : false}
|
reblog={item.reblog ? true : false}
|
||||||
|
sameAccount={actualStatus.account.id === localAccountId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{actualStatus.media_attachments.length > 0 && (
|
{actualStatus.media_attachments.length > 0 && (
|
||||||
@ -76,7 +80,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
{actualStatus.card && <TimelineCard card={actualStatus.card} />}
|
{actualStatus.card && <TimelineCard card={actualStatus.card} />}
|
||||||
</View>
|
</View>
|
||||||
),
|
),
|
||||||
[actualStatus.poll?.voted]
|
[actualStatus.poll]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -95,6 +99,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
<TimelineHeaderDefault
|
<TimelineHeaderDefault
|
||||||
{...(!isRemotePublic && { queryKey })}
|
{...(!isRemotePublic && { queryKey })}
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
|
sameAccount={actualStatus.account.id === localAccountId}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -112,6 +117,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
reblog={item.reblog ? true : false}
|
reblog={item.reblog ? true : false}
|
||||||
|
sameAccountRoot={item.account.id === localAccountId}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
@ -12,6 +12,8 @@ import TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/He
|
|||||||
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||||
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getLocalAccountId } from '@root/utils/slices/instancesSlice'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
notification: Mastodon.Notification
|
notification: Mastodon.Notification
|
||||||
@ -24,6 +26,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
|||||||
queryKey,
|
queryKey,
|
||||||
highlighted = false
|
highlighted = false
|
||||||
}) => {
|
}) => {
|
||||||
|
const localAccountId = useSelector(getLocalAccountId)
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const actualAccount = notification.status
|
const actualAccount = notification.status
|
||||||
? notification.status.account
|
? notification.status.account
|
||||||
@ -61,7 +64,12 @@ const TimelineNotifications: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{notification.status.poll && (
|
{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 && (
|
{notification.status.media_attachments.length > 0 && (
|
||||||
<TimelineAttachment
|
<TimelineAttachment
|
||||||
@ -100,7 +108,12 @@ const TimelineNotifications: React.FC<Props> = ({
|
|||||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
: 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>
|
</View>
|
||||||
|
@ -11,6 +11,8 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import getCurrentTab from '@utils/getCurrentTab'
|
import getCurrentTab from '@utils/getCurrentTab'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import { TimelineData } from '../../Timeline'
|
import { TimelineData } from '../../Timeline'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getLocalAccountId } from '@root/utils/slices/instancesSlice'
|
||||||
|
|
||||||
const fireMutation = async ({
|
const fireMutation = async ({
|
||||||
id,
|
id,
|
||||||
@ -49,9 +51,15 @@ export interface Props {
|
|||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKey.Timeline
|
||||||
status: Mastodon.Status
|
status: Mastodon.Status
|
||||||
reblog: boolean
|
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 navigation = useNavigation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const iconColor = theme.secondary
|
const iconColor = theme.secondary
|
||||||
@ -65,9 +73,28 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
const oldData = queryClient.getQueryData(queryKey)
|
const oldData = queryClient.getQueryData(queryKey)
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
// Update each specific page
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
case 'bookmark':
|
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 => {
|
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
||||||
let tootIndex = -1
|
let tootIndex = -1
|
||||||
const pageIndex = findIndex(old?.pages, page => {
|
const pageIndex = findIndex(old?.pages, page => {
|
||||||
|
@ -23,7 +23,7 @@ const fireMutation = async ({ id }: { id: string }) => {
|
|||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `conversations/${id}`
|
url: `conversations/${id}`
|
||||||
})
|
})
|
||||||
console.log(res)
|
|
||||||
if (!res.body.error) {
|
if (!res.body.error) {
|
||||||
toast({ type: 'success', content: '删除私信成功' })
|
toast({ type: 'success', content: '删除私信成功' })
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
@ -5,7 +5,7 @@ import { Feather } from '@expo/vector-icons'
|
|||||||
|
|
||||||
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
|
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
|
||||||
import relativeTime from '@utils/relativeTime'
|
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 { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import BottomSheet from '@components/BottomSheet'
|
import BottomSheet from '@components/BottomSheet'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
@ -17,9 +17,14 @@ import HeaderDefaultActionsDomain from '@components/Timelines/Timeline/Shared/He
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey?: QueryKey.Timeline
|
queryKey?: QueryKey.Timeline
|
||||||
status: Mastodon.Status
|
status: Mastodon.Status
|
||||||
|
sameAccount: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
const TimelineHeaderDefault: React.FC<Props> = ({
|
||||||
|
queryKey,
|
||||||
|
status,
|
||||||
|
sameAccount
|
||||||
|
}) => {
|
||||||
const domain = status.uri
|
const domain = status.uri
|
||||||
? status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
? status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
||||||
: ''
|
: ''
|
||||||
@ -29,7 +34,6 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const localAccountId = useSelector(getLocalAccountId)
|
|
||||||
const localDomain = useSelector(getLocalUrl)
|
const localDomain = useSelector(getLocalUrl)
|
||||||
const [since, setSince] = useState(relativeTime(status.created_at))
|
const [since, setSince] = useState(relativeTime(status.created_at))
|
||||||
const [modalVisible, setBottomSheetVisible] = useState(false)
|
const [modalVisible, setBottomSheetVisible] = useState(false)
|
||||||
@ -46,9 +50,10 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
|
|
||||||
const onPressAction = useCallback(() => setBottomSheetVisible(true), [])
|
const onPressAction = useCallback(() => setBottomSheetVisible(true), [])
|
||||||
const onPressApplication = useCallback(() => {
|
const onPressApplication = useCallback(() => {
|
||||||
navigation.navigate('Screen-Shared-Webview', {
|
status.application!.website &&
|
||||||
uri: status.application!.website
|
navigation.navigate('Screen-Shared-Webview', {
|
||||||
})
|
uri: status.application!.website
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const pressableAction = useMemo(
|
const pressableAction = useMemo(
|
||||||
@ -128,7 +133,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
visible={modalVisible}
|
visible={modalVisible}
|
||||||
handleDismiss={() => setBottomSheetVisible(false)}
|
handleDismiss={() => setBottomSheetVisible(false)}
|
||||||
>
|
>
|
||||||
{status.account.id !== localAccountId && (
|
{!sameAccount && (
|
||||||
<HeaderDefaultActionsAccount
|
<HeaderDefaultActionsAccount
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
accountId={status.account.id}
|
accountId={status.account.id}
|
||||||
@ -137,7 +142,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.account.id === localAccountId && (
|
{sameAccount && (
|
||||||
<HeaderDefaultActionsStatus
|
<HeaderDefaultActionsStatus
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
status={status}
|
status={status}
|
||||||
|
@ -99,7 +99,6 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
console.log('deleting toot')
|
|
||||||
queryClient.setQueryData<TimelineData>(
|
queryClient.setQueryData<TimelineData>(
|
||||||
queryKey,
|
queryKey,
|
||||||
old =>
|
old =>
|
||||||
|
@ -18,26 +18,26 @@ const fireMutation = async ({
|
|||||||
options
|
options
|
||||||
}: {
|
}: {
|
||||||
id: string
|
id: string
|
||||||
options: { [key: number]: boolean }
|
options?: { [key: number]: boolean }
|
||||||
}) => {
|
}) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
Object.keys(options).forEach(option => {
|
options &&
|
||||||
// @ts-ignore
|
Object.keys(options).forEach(option => {
|
||||||
if (options[option]) {
|
// @ts-ignore
|
||||||
formData.append('choices[]', option)
|
if (options[option]) {
|
||||||
}
|
formData.append('choices[]', option)
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const res = await client({
|
const res = await client({
|
||||||
method: 'post',
|
method: options ? 'post' : 'get',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `polls/${id}/votes`,
|
url: options ? `polls/${id}/votes` : `polls/${id}`,
|
||||||
body: formData
|
...(options && { body: formData })
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.body.id === id) {
|
if (res.body.id === id) {
|
||||||
toast({ type: 'success', content: '投票成功成功' })
|
return Promise.resolve(res.body as Mastodon.Poll)
|
||||||
return Promise.resolve()
|
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -52,40 +52,20 @@ export interface Props {
|
|||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKey.Timeline
|
||||||
poll: NonNullable<Mastodon.Status['poll']>
|
poll: NonNullable<Mastodon.Status['poll']>
|
||||||
reblog: boolean
|
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 { theme } = useTheme()
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { mutate } = useMutation(fireMutation, {
|
const mutation = useMutation(fireMutation, {
|
||||||
onMutate: ({ id, options }) => {
|
onSuccess: (data, { id }) => {
|
||||||
queryClient.cancelQueries(queryKey)
|
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 => {
|
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
||||||
let tootIndex = -1
|
let tootIndex = -1
|
||||||
@ -104,28 +84,49 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, poll, reblog }) => {
|
|||||||
|
|
||||||
if (pageIndex >= 0 && tootIndex >= 0) {
|
if (pageIndex >= 0 && tootIndex >= 0) {
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = updatePoll(
|
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = data
|
||||||
old!.pages[pageIndex].toots[tootIndex].reblog!.poll!
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
old!.pages[pageIndex].toots[tootIndex].poll = updatePoll(
|
old!.pages[pageIndex].toots[tootIndex].poll = data
|
||||||
old!.pages[pageIndex].toots[tootIndex].poll!
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return old
|
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(() => {
|
const pollExpiration = useMemo(() => {
|
||||||
// how many voted
|
|
||||||
if (poll.expired) {
|
if (poll.expired) {
|
||||||
return (
|
return (
|
||||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||||
@ -178,7 +179,10 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, poll, reblog }) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.percentage, { color: theme.primary }]}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -187,7 +191,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, poll, reblog }) => {
|
|||||||
styles.background,
|
styles.background,
|
||||||
{
|
{
|
||||||
width: `${Math.round(
|
width: `${Math.round(
|
||||||
(option.votes_count / poll.votes_count) * 100
|
(option.votes_count / poll.voters_count) * 100
|
||||||
)}%`,
|
)}%`,
|
||||||
backgroundColor: theme.border
|
backgroundColor: theme.border
|
||||||
}
|
}
|
||||||
@ -234,20 +238,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, poll, reblog }) => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
{!poll.expired && !poll.own_votes?.length && (
|
{pollButton()}
|
||||||
<View style={styles.button}>
|
|
||||||
<ButtonRow
|
|
||||||
onPress={() => {
|
|
||||||
if (poll.multiple) {
|
|
||||||
mutate({ id: poll.id, options: multipleOptions })
|
|
||||||
} else {
|
|
||||||
mutate({ id: poll.id, options: singleOptions })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
text='投票'
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<Text style={[styles.votes, { color: theme.secondary }]}>
|
<Text style={[styles.votes, { color: theme.secondary }]}>
|
||||||
已投{poll.voters_count || 0}人{' • '}
|
已投{poll.voters_count || 0}人{' • '}
|
||||||
</Text>
|
</Text>
|
||||||
@ -299,7 +290,7 @@ const styles = StyleSheet.create({
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
minWidth: 1,
|
minWidth: 2,
|
||||||
borderTopRightRadius: 6,
|
borderTopRightRadius: 6,
|
||||||
borderBottomRightRadius: 6
|
borderBottomRightRadius: 6
|
||||||
},
|
},
|
||||||
@ -320,7 +311,4 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default React.memo(
|
export default TimelinePoll
|
||||||
TimelinePoll,
|
|
||||||
(prev, next) => prev.poll.voted === next.poll.voted
|
|
||||||
)
|
|
||||||
|
@ -29,6 +29,7 @@ import { HeaderLeft, HeaderRight } from '@components/Header'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import formatText from '@screens/Shared/Compose/formatText'
|
import formatText from '@screens/Shared/Compose/formatText'
|
||||||
|
import { useQueryClient } from 'react-query'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
@ -316,6 +317,7 @@ export interface Props {
|
|||||||
|
|
||||||
const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const [hasKeyboard, setHasKeyboard] = useState(false)
|
const [hasKeyboard, setHasKeyboard] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -449,7 +451,8 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
|||||||
composeState.poll.expire +
|
composeState.poll.expire +
|
||||||
composeState.attachments.sensitive +
|
composeState.attachments.sensitive +
|
||||||
composeState.attachments.uploads.map(upload => upload.id) +
|
composeState.attachments.uploads.map(upload => upload.id) +
|
||||||
composeState.visibility
|
composeState.visibility +
|
||||||
|
(params?.type === 'edit' ? Math.random() : '')
|
||||||
).toString()
|
).toString()
|
||||||
},
|
},
|
||||||
body: formData
|
body: formData
|
||||||
@ -462,7 +465,7 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
|||||||
{
|
{
|
||||||
text: '好的',
|
text: '好的',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
// clear homepage cache
|
queryClient.invalidateQueries(['Following'])
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { useTheme } from '@root/utils/styles/ThemeManager'
|
|||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
Image,
|
Image,
|
||||||
Pressable,
|
Pressable,
|
||||||
SectionList,
|
SectionList,
|
||||||
@ -70,42 +71,47 @@ const ScreenSharedSearch: React.FC = () => {
|
|||||||
}, [searchTerm])
|
}, [searchTerm])
|
||||||
|
|
||||||
const listEmpty = useMemo(
|
const listEmpty = useMemo(
|
||||||
() => (
|
() =>
|
||||||
<View style={styles.emptyBase}>
|
status === 'loading' ? (
|
||||||
<Text
|
<View style={styles.emptyBase}>
|
||||||
style={[
|
<ActivityIndicator />
|
||||||
styles.emptyDefault,
|
</View>
|
||||||
styles.emptyFontSize,
|
) : (
|
||||||
{ color: theme.primary }
|
<View style={styles.emptyBase}>
|
||||||
]}
|
<Text
|
||||||
>
|
style={[
|
||||||
输入关键词搜索<Text style={styles.emptyFontBold}>用户</Text>、
|
styles.emptyDefault,
|
||||||
<Text style={styles.emptyFontBold}>话题标签</Text>或者
|
styles.emptyFontSize,
|
||||||
<Text style={styles.emptyFontBold}>嘟文</Text>
|
{ color: theme.primary }
|
||||||
</Text>
|
]}
|
||||||
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
>
|
||||||
高级搜索格式
|
输入关键词搜索<Text style={styles.emptyFontBold}>用户</Text>、
|
||||||
</Text>
|
<Text style={styles.emptyFontBold}>话题标签</Text>或者
|
||||||
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
<Text style={styles.emptyFontBold}>嘟文</Text>
|
||||||
<Text style={{ color: theme.secondary }}>@username@domain</Text>
|
</Text>
|
||||||
{' '}
|
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
||||||
搜索用户
|
高级搜索格式
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
||||||
<Text style={{ color: theme.secondary }}>#example</Text>
|
<Text style={{ color: theme.secondary }}>@username@domain</Text>
|
||||||
{' '}搜索话题标签
|
{' '}
|
||||||
</Text>
|
搜索用户
|
||||||
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
</Text>
|
||||||
<Text style={{ color: theme.secondary }}>URL</Text>
|
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
||||||
{' '}搜索指定嘟文
|
<Text style={{ color: theme.secondary }}>#example</Text>
|
||||||
</Text>
|
{' '}搜索话题标签
|
||||||
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
</Text>
|
||||||
<Text style={{ color: theme.secondary }}>URL</Text>
|
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
||||||
{' '}搜索指定用户
|
<Text style={{ color: theme.secondary }}>URL</Text>
|
||||||
</Text>
|
{' '}搜索指定嘟文
|
||||||
</View>
|
</Text>
|
||||||
),
|
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
|
||||||
[]
|
<Text style={{ color: theme.secondary }}>URL</Text>
|
||||||
|
{' '}搜索指定用户
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
[status]
|
||||||
)
|
)
|
||||||
const sectionHeader = useCallback(
|
const sectionHeader = useCallback(
|
||||||
({ section: { title } }) => (
|
({ section: { title } }) => (
|
||||||
@ -247,7 +253,6 @@ const ScreenSharedSearch: React.FC = () => {
|
|||||||
stickySectionHeadersEnabled
|
stickySectionHeadersEnabled
|
||||||
sections={setctionData}
|
sections={setctionData}
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={listEmpty}
|
||||||
refreshing={status === 'loading'}
|
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
renderSectionHeader={sectionHeader}
|
renderSectionHeader={sectionHeader}
|
||||||
renderSectionFooter={sectionFooter}
|
renderSectionFooter={sectionFooter}
|
||||||
|
@ -20,10 +20,18 @@ export const timelineFetch = async ({
|
|||||||
if (pageParam) {
|
if (pageParam) {
|
||||||
switch (pageParam.direction) {
|
switch (pageParam.direction) {
|
||||||
case 'prev':
|
case 'prev':
|
||||||
params.min_id = pageParam.id
|
if (page === 'Bookmarks' || page === 'Favourites') {
|
||||||
|
params.max_id = pageParam.id
|
||||||
|
} else {
|
||||||
|
params.min_id = pageParam.id
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'next':
|
case 'next':
|
||||||
params.max_id = pageParam.id
|
if (page === 'Bookmarks' || page === 'Favourites') {
|
||||||
|
params.min_id = pageParam.id
|
||||||
|
} else {
|
||||||
|
params.max_id = pageParam.id
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user