mirror of
https://github.com/tooot-app/app
synced 2025-02-25 16:17:43 +01:00
Poll working
But did not re-render
This commit is contained in:
parent
8986680c6d
commit
7d7c907fa3
@ -17,6 +17,7 @@
|
||||
"expo": "~39.0.4",
|
||||
"expo-auth-session": "~2.0.0",
|
||||
"expo-av": "~8.6.0",
|
||||
"expo-blur": "~8.2.0",
|
||||
"expo-image-picker": "~9.1.1",
|
||||
"expo-linear-gradient": "~8.3.0",
|
||||
"expo-localization": "^9.0.0",
|
||||
|
8
src/@types/mastodon.d.ts
vendored
8
src/@types/mastodon.d.ts
vendored
@ -48,10 +48,10 @@ declare namespace Mastodon {
|
||||
// Others
|
||||
remote_url?: string
|
||||
text_url?: string
|
||||
meta: {
|
||||
original: { width: number; height: number; size: string; aspect: number }
|
||||
small: { width: number; height: number; size: string; aspect: number }
|
||||
focus:
|
||||
meta?: {
|
||||
original?: { width: number; height: number; size: string; aspect: number }
|
||||
small?: { width: number; height: number; size: string; aspect: number }
|
||||
focus?:
|
||||
| { x: number; y: number }
|
||||
| {
|
||||
length: string
|
||||
|
@ -103,7 +103,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'flex-end'
|
||||
},
|
||||
container: {
|
||||
padding: StyleConstants.Spacing.L,
|
||||
paddingTop: StyleConstants.Spacing.M
|
||||
},
|
||||
handle: {
|
||||
@ -115,9 +114,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
cancel: {
|
||||
padding: StyleConstants.Spacing.S,
|
||||
marginLeft: StyleConstants.Spacing.L,
|
||||
marginRight: StyleConstants.Spacing.L,
|
||||
borderWidth: 1,
|
||||
borderRadius: 100,
|
||||
// marginBottom: StyleConstants.Spacing.L
|
||||
borderRadius: 100
|
||||
},
|
||||
text: {
|
||||
fontSize: StyleConstants.Font.Size.L,
|
||||
|
@ -1,42 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet, Text } from 'react-native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
onPress: () => void
|
||||
icon: string
|
||||
text: string
|
||||
}
|
||||
|
||||
const BottomSheetRow: React.FC<Props> = ({ onPress, icon, text }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} style={styles.pressable}>
|
||||
<Feather
|
||||
name={icon}
|
||||
color={theme.primary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
<Text style={[styles.text, { color: theme.primary }]}>{text}</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressable: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
},
|
||||
text: {
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
lineHeight: StyleConstants.Font.Size.L,
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
export default BottomSheetRow
|
46
src/components/Button.tsx
Normal file
46
src/components/Button.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet, Text } from 'react-native'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
onPress: () => void
|
||||
text: string
|
||||
fontSize?: 'S' | 'M' | 'L'
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({ onPress, text, fontSize = 'M' }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={[styles.button, { borderColor: theme.primary }]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
{ color: theme.primary, fontSize: StyleConstants.Font.Size[fontSize] }
|
||||
]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
paddingTop: StyleConstants.Spacing.S,
|
||||
paddingBottom: StyleConstants.Spacing.S,
|
||||
paddingLeft: StyleConstants.Spacing.M,
|
||||
paddingRight: StyleConstants.Spacing.M,
|
||||
borderWidth: 1,
|
||||
borderRadius: 100
|
||||
},
|
||||
text: {
|
||||
textAlign: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default Button
|
@ -34,7 +34,7 @@ const styles = StyleSheet.create({
|
||||
paddingRight: StyleConstants.Spacing.S
|
||||
},
|
||||
text: {
|
||||
fontSize: StyleConstants.Font.Size.L
|
||||
fontSize: StyleConstants.Font.Size.M
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
type PropsBase = {
|
||||
disabled?: boolean
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
@ -20,6 +21,7 @@ export interface PropsIcon extends PropsBase {
|
||||
}
|
||||
|
||||
const HeaderRight: React.FC<PropsText | PropsIcon> = ({
|
||||
disabled,
|
||||
onPress,
|
||||
text,
|
||||
icon
|
||||
@ -27,12 +29,21 @@ const HeaderRight: React.FC<PropsText | PropsIcon> = ({
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} style={styles.base}>
|
||||
{text && <Text style={[styles.text, { color: theme.primary }]}>{text}</Text>}
|
||||
<Pressable {...(!disabled && { onPress })} style={styles.base}>
|
||||
{text && (
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
{ color: disabled ? theme.secondary : theme.primary }
|
||||
]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
)}
|
||||
{icon && (
|
||||
<Feather
|
||||
name={icon}
|
||||
color={theme.primary}
|
||||
color={disabled ? theme.secondary : theme.primary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
)}
|
||||
@ -45,7 +56,7 @@ const styles = StyleSheet.create({
|
||||
paddingLeft: StyleConstants.Spacing.S
|
||||
},
|
||||
text: {
|
||||
fontSize: StyleConstants.Font.Size.L
|
||||
fontSize: StyleConstants.Font.Size.M
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import MenuContainer from './Menu/Container'
|
||||
import MenuHeader from './Menu/Header'
|
||||
import MenuItem from './Menu/Item'
|
||||
import MenuRow from './Menu/Row'
|
||||
|
||||
export { MenuContainer, MenuHeader, MenuItem }
|
||||
export { MenuContainer, MenuHeader, MenuRow }
|
||||
|
@ -1,36 +1,36 @@
|
||||
import React from 'react'
|
||||
import React, { Children } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode
|
||||
marginTop?: boolean
|
||||
}
|
||||
|
||||
const MenuContainer: React.FC<Props> = ({ ...props }) => {
|
||||
const MenuContainer: React.FC<Props> = ({ children }) => {
|
||||
const { theme } = useTheme()
|
||||
// @ts-ignore
|
||||
const firstChild = Children.toArray(children)[0].type.name
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.base,
|
||||
{
|
||||
...(firstChild !== 'MenuHeader' && {
|
||||
borderTopColor: theme.separator,
|
||||
marginTop: props.marginTop
|
||||
? StyleConstants.Spacing.Global.PagePadding
|
||||
: 0
|
||||
borderTopWidth: 1
|
||||
})
|
||||
}
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderTopWidth: 1,
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
}
|
||||
})
|
||||
|
@ -1,19 +1,28 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
heading: string
|
||||
}
|
||||
|
||||
const MenuHeader: React.FC<Props> = ({ heading }) => {
|
||||
return <Text style={styles.header}>{heading}</Text>
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[styles.base, { borderBottomColor: theme.separator }]}>
|
||||
<Text>{heading}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
marginTop: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12
|
||||
base: {
|
||||
borderBottomWidth: 1,
|
||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -43,6 +43,7 @@ const Core: React.FC<Props> = ({
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
{(content || iconBack) && (
|
||||
<View style={styles.back}>
|
||||
{content && (
|
||||
<Text
|
||||
@ -61,11 +62,12 @@ const Core: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const MenuItem: React.FC<Props> = ({ ...props }) => {
|
||||
const MenuRow: React.FC<Props> = ({ ...props }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return props.onPress ? (
|
||||
@ -95,11 +97,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
front: {
|
||||
flex: 1,
|
||||
flexBasis: '70%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
back: {
|
||||
flex: 1,
|
||||
flexBasis: '30%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center'
|
||||
@ -108,6 +112,7 @@ const styles = StyleSheet.create({
|
||||
marginRight: 8
|
||||
},
|
||||
text: {
|
||||
flex: 1,
|
||||
fontSize: StyleConstants.Font.Size.M
|
||||
},
|
||||
content: {
|
||||
@ -118,4 +123,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default MenuItem
|
||||
export default MenuRow
|
@ -2,14 +2,15 @@ import React, { useCallback, useMemo } from 'react'
|
||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import Actioned from './Shared/Actioned'
|
||||
import Avatar from './Shared/Avatar'
|
||||
import HeaderDefault from './Shared/HeaderDefault'
|
||||
import Content from './Shared/Content'
|
||||
import Poll from './Shared/Poll'
|
||||
import Attachment from './Shared/Attachment'
|
||||
import Card from './Shared/Card'
|
||||
import ActionsStatus from './Shared/ActionsStatus'
|
||||
import TimelineActioned from './Shared/Actioned'
|
||||
import TimelineActions from './Shared/Actions'
|
||||
import TimelineAttachment from './Shared/Attachment'
|
||||
import TimelineAvatar from './Shared/Avatar'
|
||||
import TimelineCard from './Shared/Card'
|
||||
import TimelineContent from './Shared/Content'
|
||||
import TimelineHeaderDefault from './Shared/HeaderDefault'
|
||||
import TimelinePoll from './Shared/Poll'
|
||||
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
@ -22,6 +23,11 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
let actualStatus = item.reblog ? item.reblog : item
|
||||
const contentWidth =
|
||||
Dimensions.get('window').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 - // Global page padding on both sides
|
||||
StyleConstants.Avatar.S - // Avatar width
|
||||
StyleConstants.Spacing.S // Avatar margin to the right
|
||||
|
||||
const pressableToot = useCallback(
|
||||
() =>
|
||||
@ -33,73 +39,37 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
const childrenToot = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{actualStatus.content ? (
|
||||
<Content
|
||||
content={actualStatus.content}
|
||||
emojis={actualStatus.emojis}
|
||||
mentions={actualStatus.mentions}
|
||||
spoiler_text={actualStatus.spoiler_text}
|
||||
// tags={actualStatus.tags}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
{actualStatus.content.length > 0 && (
|
||||
<TimelineContent status={actualStatus} />
|
||||
)}
|
||||
{actualStatus.poll && (
|
||||
<TimelinePoll queryKey={queryKey} poll={actualStatus.poll} />
|
||||
)}
|
||||
{actualStatus.poll && <Poll poll={actualStatus.poll} />}
|
||||
{actualStatus.media_attachments.length > 0 && (
|
||||
<Attachment
|
||||
media_attachments={actualStatus.media_attachments}
|
||||
sensitive={actualStatus.sensitive}
|
||||
width={
|
||||
Dimensions.get('window').width - StyleConstants.Spacing.M * 2 - 50 - 8
|
||||
}
|
||||
/>
|
||||
<TimelineAttachment status={actualStatus} width={contentWidth} />
|
||||
)}
|
||||
{actualStatus.card && <Card card={actualStatus.card} />}
|
||||
{actualStatus.card && <TimelineCard card={actualStatus.card} />}
|
||||
</>
|
||||
),
|
||||
[]
|
||||
[actualStatus.poll?.voted]
|
||||
)
|
||||
|
||||
const statusView = useMemo(() => {
|
||||
return (
|
||||
<View style={styles.statusView}>
|
||||
{item.reblog && (
|
||||
<Actioned
|
||||
action='reblog'
|
||||
name={item.account.display_name || item.account.username}
|
||||
emojis={item.account.emojis}
|
||||
/>
|
||||
<TimelineActioned action='reblog' account={item.account} />
|
||||
)}
|
||||
<View style={styles.status}>
|
||||
<Avatar
|
||||
uri={actualStatus.account.avatar}
|
||||
id={actualStatus.account.id}
|
||||
/>
|
||||
<TimelineAvatar account={actualStatus.account} />
|
||||
<View style={styles.details}>
|
||||
<HeaderDefault
|
||||
queryKey={queryKey}
|
||||
accountId={actualStatus.account.id}
|
||||
domain={actualStatus.uri.split(new RegExp(/\/\/(.*?)\//))[1]}
|
||||
name={
|
||||
actualStatus.account.display_name ||
|
||||
actualStatus.account.username
|
||||
}
|
||||
emojis={actualStatus.account.emojis}
|
||||
account={actualStatus.account.acct}
|
||||
created_at={item.created_at}
|
||||
visibility={item.visibility}
|
||||
application={item.application}
|
||||
/>
|
||||
<TimelineHeaderDefault queryKey={queryKey} status={actualStatus} />
|
||||
{/* Can pass toot info to next page to speed up performance */}
|
||||
<Pressable onPress={pressableToot} children={childrenToot} />
|
||||
<ActionsStatus queryKey={queryKey} status={actualStatus} />
|
||||
<TimelineActions queryKey={queryKey} status={actualStatus} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}, [item])
|
||||
|
||||
return statusView
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@ -114,8 +84,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row'
|
||||
},
|
||||
details: {
|
||||
flex: 1,
|
||||
flexGrow: 1
|
||||
flex: 1
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -9,7 +9,7 @@ import Content from './Shared/Content'
|
||||
import Poll from './Shared/Poll'
|
||||
import Attachment from './Shared/Attachment'
|
||||
import Card from './Shared/Card'
|
||||
import ActionsStatus from './Shared/ActionsStatus'
|
||||
import ActionsStatus from './Shared/Actions'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
|
@ -7,19 +7,19 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account
|
||||
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog'
|
||||
name?: string
|
||||
emojis?: Mastodon.Emoji[]
|
||||
notification?: boolean
|
||||
}
|
||||
|
||||
const Actioned: React.FC<Props> = ({
|
||||
const TimelineActioned: React.FC<Props> = ({
|
||||
account,
|
||||
|
||||
action,
|
||||
name,
|
||||
emojis,
|
||||
notification = false
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const name = account.display_name || account.username
|
||||
const iconColor = theme.primary
|
||||
|
||||
let icon
|
||||
@ -74,20 +74,18 @@ const Actioned: React.FC<Props> = ({
|
||||
return (
|
||||
<View style={styles.actioned}>
|
||||
{icon}
|
||||
{content ? (
|
||||
{content && (
|
||||
<View style={styles.content}>
|
||||
{emojis ? (
|
||||
{account.emojis ? (
|
||||
<Emojis
|
||||
content={content}
|
||||
emojis={emojis}
|
||||
emojis={account.emojis}
|
||||
size={StyleConstants.Font.Size.S}
|
||||
/>
|
||||
) : (
|
||||
<Text>{content}</Text>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
@ -107,4 +105,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(Actioned)
|
||||
export default React.memo(TimelineActioned, () => true)
|
||||
|
@ -1,16 +1,12 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
import client from 'src/api/client'
|
||||
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { toast } from 'src/components/toast'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import BottomSheet from 'src/components/BottomSheet'
|
||||
import BottomSheetRow from 'src/components/BottomSheet/Row'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
@ -19,14 +15,8 @@ const fireMutation = async ({
|
||||
prevState
|
||||
}: {
|
||||
id: string
|
||||
type: 'favourite' | 'reblog' | 'bookmark' | 'mute' | 'pin' | 'delete'
|
||||
stateKey:
|
||||
| 'favourited'
|
||||
| 'reblogged'
|
||||
| 'bookmarked'
|
||||
| 'muted'
|
||||
| 'pinned'
|
||||
| 'id'
|
||||
type: 'favourite' | 'reblog' | 'bookmark'
|
||||
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
|
||||
prevState?: boolean
|
||||
}) => {
|
||||
let res
|
||||
@ -34,8 +24,6 @@ const fireMutation = async ({
|
||||
case 'favourite':
|
||||
case 'reblog':
|
||||
case 'bookmark':
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
@ -50,21 +38,6 @@ const fireMutation = async ({
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
case 'delete':
|
||||
res = await client({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
endpoint: `statuses/${id}`
|
||||
})
|
||||
|
||||
if (res.body[stateKey] === id) {
|
||||
toast({ type: 'success', content: '删除成功' })
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
toast({ type: 'error', content: '删除失败' })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,15 +46,12 @@ export interface Props {
|
||||
status: Mastodon.Status
|
||||
}
|
||||
|
||||
const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
const TimelineActions: React.FC<Props> = ({ queryKey, status }) => {
|
||||
const { theme } = useTheme()
|
||||
const iconColor = theme.secondary
|
||||
const iconColorAction = (state: boolean) =>
|
||||
state ? theme.primary : theme.secondary
|
||||
|
||||
const localAccountId = useSelector(getLocalAccountId)
|
||||
const [bottomSheetVisible, setBottomSheetVisible] = useState(false)
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: ({ id, type, stateKey, prevState }) => {
|
||||
@ -92,8 +62,6 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
case 'favourite':
|
||||
case 'reblog':
|
||||
case 'bookmark':
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
queryCache.setQueryData(queryKey, old =>
|
||||
(old as {}[]).map((paging: any) => ({
|
||||
toots: paging.toots.map((toot: any) => {
|
||||
@ -107,19 +75,6 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
}))
|
||||
)
|
||||
break
|
||||
case 'delete':
|
||||
queryCache.setQueryData(queryKey, old =>
|
||||
(old as {}[]).map((paging: any) => ({
|
||||
toots: paging.toots.map((toot: any, index: number) => {
|
||||
if (toot.id === id) {
|
||||
paging.toots.splice(index, 1)
|
||||
}
|
||||
return toot
|
||||
}),
|
||||
pointer: paging.pointer
|
||||
}))
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
return oldData
|
||||
@ -161,7 +116,23 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
}),
|
||||
[status.bookmarked]
|
||||
)
|
||||
const onPressShare = useCallback(() => setBottomSheetVisible(true), [])
|
||||
const onPressShare = useCallback(
|
||||
() =>
|
||||
ActionSheetIOS.showShareActionSheetWithOptions(
|
||||
{
|
||||
url: status.uri,
|
||||
excludedActivityTypes: [
|
||||
'com.apple.UIKit.activity.Mail',
|
||||
'com.apple.UIKit.activity.Print',
|
||||
'com.apple.UIKit.activity.SaveToCameraRoll',
|
||||
'com.apple.UIKit.activity.OpenInIBooks'
|
||||
]
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const childrenReply = useMemo(
|
||||
() => (
|
||||
@ -268,86 +239,6 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
children={childrenShare}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<BottomSheet
|
||||
visible={bottomSheetVisible}
|
||||
handleDismiss={() => setBottomSheetVisible(false)}
|
||||
>
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
ActionSheetIOS.showShareActionSheetWithOptions(
|
||||
{
|
||||
url: status.uri,
|
||||
excludedActivityTypes: [
|
||||
'com.apple.UIKit.activity.Mail',
|
||||
'com.apple.UIKit.activity.Print',
|
||||
'com.apple.UIKit.activity.SaveToCameraRoll',
|
||||
'com.apple.UIKit.activity.OpenInIBooks'
|
||||
]
|
||||
},
|
||||
() => {},
|
||||
() => {
|
||||
setBottomSheetVisible(false)
|
||||
toast({ type: 'success', content: '分享成功' })
|
||||
}
|
||||
)
|
||||
}}
|
||||
icon='share'
|
||||
text={'分享嘟嘟'}
|
||||
/>
|
||||
{status.account.id === localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'delete',
|
||||
stateKey: 'id'
|
||||
})
|
||||
}}
|
||||
icon='trash'
|
||||
text='删除嘟嘟'
|
||||
/>
|
||||
)}
|
||||
{status.account.id === localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
console.warn('功能未开发')
|
||||
}}
|
||||
icon='trash'
|
||||
text='删除并重发'
|
||||
/>
|
||||
)}
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'mute',
|
||||
stateKey: 'muted',
|
||||
prevState: status.muted
|
||||
})
|
||||
}}
|
||||
icon='volume-x'
|
||||
text={status.muted ? '取消静音' : '静音'}
|
||||
/>
|
||||
{/* Also note that reblogs cannot be pinned. */}
|
||||
{status.account.id === localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'pin',
|
||||
stateKey: 'pinned',
|
||||
prevState: status.pinned
|
||||
})
|
||||
}}
|
||||
icon='anchor'
|
||||
text={status.pinned ? '取消置顶' : '置顶'}
|
||||
/>
|
||||
)}
|
||||
</BottomSheet>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -363,22 +254,7 @@ const styles = StyleSheet.create({
|
||||
width: '20%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
modalBackground: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
modalSheet: {
|
||||
width: '100%',
|
||||
height: '50%',
|
||||
backgroundColor: 'white',
|
||||
flex: 1
|
||||
}
|
||||
})
|
||||
|
||||
export default ActionsStatus
|
||||
export default TimelineActions
|
@ -1,65 +1,58 @@
|
||||
import React from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import { BlurView } from 'expo-blur'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
import AttachmentImage from './Attachment/AttachmentImage'
|
||||
import AttachmentVideo from './Attachment/AttachmentVideo'
|
||||
|
||||
export interface Props {
|
||||
media_attachments: Mastodon.Attachment[]
|
||||
sensitive: boolean
|
||||
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
|
||||
width: number
|
||||
}
|
||||
|
||||
const Attachment: React.FC<Props> = ({
|
||||
media_attachments,
|
||||
sensitive,
|
||||
width
|
||||
}) => {
|
||||
const TimelineAttachment: React.FC<Props> = ({ status, width }) => {
|
||||
const { mode, theme } = useTheme()
|
||||
const allTypes = status.media_attachments.map(m => m.type)
|
||||
let attachment
|
||||
let attachmentHeight
|
||||
// if (width) {}
|
||||
switch (media_attachments[0].type) {
|
||||
case 'unknown':
|
||||
attachment = <Text>文件不支持</Text>
|
||||
attachmentHeight = 25
|
||||
break
|
||||
case 'image':
|
||||
|
||||
if (allTypes.includes('image')) {
|
||||
attachment = (
|
||||
<AttachmentImage
|
||||
media_attachments={media_attachments}
|
||||
sensitive={sensitive}
|
||||
media_attachments={status.media_attachments}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
attachmentHeight = width / 2
|
||||
break
|
||||
case 'gifv':
|
||||
attachmentHeight = (width / 16) * 9
|
||||
} else if (allTypes.includes('gifv')) {
|
||||
attachment = (
|
||||
<AttachmentVideo
|
||||
media_attachments={media_attachments}
|
||||
sensitive={sensitive}
|
||||
media_attachments={status.media_attachments}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
attachmentHeight =
|
||||
(width / media_attachments[0].meta.original.width) *
|
||||
media_attachments[0].meta.original.height
|
||||
break
|
||||
// Support multiple video
|
||||
// Supoort when video meta is empty
|
||||
// case 'video':
|
||||
// attachment = (
|
||||
// <AttachmentVideo
|
||||
// media_attachments={media_attachments}
|
||||
// sensitive={sensitive}
|
||||
// width={width}
|
||||
// />
|
||||
// )
|
||||
// attachmentHeight =
|
||||
// (width / media_attachments[0].meta.original.width) *
|
||||
// media_attachments[0].meta.original.height
|
||||
// break
|
||||
// case 'audio':
|
||||
status.media_attachments[0].meta?.original?.width &&
|
||||
status.media_attachments[0].meta?.original?.height
|
||||
? (width / status.media_attachments[0].meta.original.width) *
|
||||
status.media_attachments[0].meta.original.height
|
||||
: (width / 16) * 9
|
||||
} else if (allTypes.includes('video')) {
|
||||
attachment = (
|
||||
<AttachmentVideo
|
||||
media_attachments={status.media_attachments}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
attachmentHeight =
|
||||
status.media_attachments[0].meta?.original?.width &&
|
||||
status.media_attachments[0].meta?.original?.height
|
||||
? (width / status.media_attachments[0].meta.original.width) *
|
||||
status.media_attachments[0].meta.original.height
|
||||
: (width / 16) * 9
|
||||
} else if (allTypes.includes('audio')) {
|
||||
// attachment = (
|
||||
// <AttachmentAudio
|
||||
// media_attachments={media_attachments}
|
||||
@ -67,21 +60,60 @@ const Attachment: React.FC<Props> = ({
|
||||
// width={width}
|
||||
// />
|
||||
// )
|
||||
// break
|
||||
} else {
|
||||
attachment = <Text>文件不支持</Text>
|
||||
attachmentHeight = 25
|
||||
}
|
||||
|
||||
const [sensitiveShown, setSensitiveShown] = useState(true)
|
||||
const onPressBlurView = useCallback(() => {
|
||||
setSensitiveShown(false)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (status.sensitive && sensitiveShown === false) {
|
||||
setTimeout(() => {
|
||||
setSensitiveShown(true)
|
||||
}, 10000)
|
||||
}
|
||||
}, [sensitiveShown])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: width + 8,
|
||||
width: width + StyleConstants.Spacing.XS,
|
||||
height: attachmentHeight,
|
||||
marginTop: 4,
|
||||
marginLeft: -4
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginLeft: -StyleConstants.Spacing.XS / 2
|
||||
}}
|
||||
>
|
||||
{attachment}
|
||||
{status.sensitive && sensitiveShown && (
|
||||
<BlurView tint={mode} intensity={100} style={styles.blurView}>
|
||||
<Pressable onPress={onPressBlurView} style={styles.blurViewPressable}>
|
||||
<Text style={[styles.sensitiveText, { color: theme.primary }]}>
|
||||
显示敏感内容
|
||||
</Text>
|
||||
</Pressable>
|
||||
</BlurView>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Attachment, () => true)
|
||||
const styles = StyleSheet.create({
|
||||
blurView: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
blurViewPressable: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
sensitiveText: {
|
||||
fontSize: StyleConstants.Font.Size.M
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(TimelineAttachment, () => true)
|
||||
|
@ -1,75 +1,46 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Button, Image, Modal, StyleSheet, Pressable, View } from 'react-native'
|
||||
import React, { useState } from 'react'
|
||||
import { Image, Modal, StyleSheet, Pressable, View } from 'react-native'
|
||||
import ImageViewer from 'react-native-image-zoom-viewer'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
media_attachments: Mastodon.Attachment[]
|
||||
sensitive: boolean
|
||||
width: number
|
||||
}
|
||||
|
||||
const AttachmentImage: React.FC<Props> = ({
|
||||
media_attachments,
|
||||
sensitive,
|
||||
width
|
||||
}) => {
|
||||
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
|
||||
const AttachmentImage: React.FC<Props> = ({ media_attachments }) => {
|
||||
const [imageModalVisible, setImageModalVisible] = useState(false)
|
||||
const [imageModalIndex, setImageModalIndex] = useState(0)
|
||||
useEffect(() => {
|
||||
if (sensitive && mediaSensitive === false) {
|
||||
setTimeout(() => {
|
||||
setMediaSensitive(true)
|
||||
}, 10000)
|
||||
}
|
||||
}, [mediaSensitive])
|
||||
|
||||
let images: { url: string; width: number; height: number }[] = []
|
||||
let images: {
|
||||
url: string
|
||||
width: number | undefined
|
||||
height: number | undefined
|
||||
}[] = []
|
||||
const imagesNode = media_attachments.map((m, i) => {
|
||||
images.push({
|
||||
url: m.url,
|
||||
width: m.meta.original.width,
|
||||
height: m.meta.original.height
|
||||
width: m.meta?.original?.width || undefined,
|
||||
height: m.meta?.original?.height || undefined
|
||||
})
|
||||
return (
|
||||
<Pressable
|
||||
key={i}
|
||||
style={{ flexGrow: 1, height: width / 2, margin: 4 }}
|
||||
style={[styles.imageContainer]}
|
||||
onPress={() => {
|
||||
setImageModalIndex(i)
|
||||
setImageModalVisible(true)
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: m.preview_url }}
|
||||
style={styles.image}
|
||||
blurRadius={mediaSensitive ? width / 5 : 0}
|
||||
/>
|
||||
<Image source={{ uri: m.preview_url }} style={styles.image} />
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.media}>
|
||||
{imagesNode}
|
||||
{mediaSensitive && (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title='Press me'
|
||||
onPress={() => {
|
||||
setMediaSensitive(false)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={[styles.media]}>{imagesNode}</View>
|
||||
|
||||
<Modal
|
||||
visible={imageModalVisible}
|
||||
transparent={true}
|
||||
@ -82,6 +53,7 @@ const AttachmentImage: React.FC<Props> = ({
|
||||
enableSwipeDown={true}
|
||||
swipeDownThreshold={100}
|
||||
useNativeDriver={true}
|
||||
saveToLocalByLongPress={false}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
@ -91,16 +63,18 @@ const AttachmentImage: React.FC<Props> = ({
|
||||
const styles = StyleSheet.create({
|
||||
media: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'stretch',
|
||||
alignContent: 'stretch'
|
||||
},
|
||||
imageContainer: {
|
||||
flex: 1,
|
||||
flexBasis: '50%',
|
||||
padding: StyleConstants.Spacing.XS / 2
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
flex: 1
|
||||
}
|
||||
})
|
||||
|
||||
export default AttachmentImage
|
||||
export default React.memo(AttachmentImage, () => true)
|
||||
|
@ -1,27 +1,29 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { Video } from 'expo-av'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
export interface Props {
|
||||
media_attachments: Mastodon.Attachment[]
|
||||
sensitive: boolean
|
||||
width: number
|
||||
}
|
||||
|
||||
const AttachmentVideo: React.FC<Props> = ({
|
||||
media_attachments,
|
||||
sensitive,
|
||||
width
|
||||
}) => {
|
||||
const videoPlayer = useRef()
|
||||
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
|
||||
const AttachmentVideo: React.FC<Props> = ({ media_attachments, width }) => {
|
||||
const videoPlayer = useRef<Video>(null)
|
||||
const [videoPlay, setVideoPlay] = useState(false)
|
||||
|
||||
const video = media_attachments[0]
|
||||
const videoWidth = width
|
||||
const videoHeight =
|
||||
(width / video.meta.original.width) * video.meta.original.height
|
||||
video.meta?.original?.width && video.meta?.original?.height
|
||||
? (width / video.meta.original.width) * video.meta.original.height
|
||||
: (width / 16) * 9
|
||||
|
||||
const onPressVideo = useCallback(() => {
|
||||
// @ts-ignore
|
||||
videoPlayer.current.presentFullscreenPlayer()
|
||||
setVideoPlay(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View
|
||||
@ -39,17 +41,13 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||
}}
|
||||
resizeMode='cover'
|
||||
usePoster
|
||||
posterSourceThe={{ uri: video.preview_url }}
|
||||
posterSource={{ uri: video.preview_url }}
|
||||
useNativeControls
|
||||
shouldPlay={videoPlay}
|
||||
/>
|
||||
{!videoPlay && (
|
||||
{videoPlayer.current && !videoPlay && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setMediaSensitive(false)
|
||||
videoPlayer.current.presentFullscreenPlayer()
|
||||
setVideoPlay(true)
|
||||
}}
|
||||
onPress={onPressVideo}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
@ -4,22 +4,21 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
uri: string
|
||||
id: string
|
||||
account: Mastodon.Account
|
||||
}
|
||||
|
||||
const Avatar: React.FC<Props> = ({ uri, id }) => {
|
||||
const TimelineAvatar: React.FC<Props> = ({ account }) => {
|
||||
const navigation = useNavigation()
|
||||
// Need to fix go back root
|
||||
const onPress = useCallback(() => {
|
||||
navigation.navigate('Screen-Shared-Account', {
|
||||
id: id
|
||||
id: account.id
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Pressable style={styles.avatar} onPress={onPress}>
|
||||
<Image source={{ uri: uri }} style={styles.image} />
|
||||
<Image source={{ uri: account.avatar }} style={styles.image} />
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
@ -37,4 +36,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(Avatar, () => true)
|
||||
export default React.memo(TimelineAvatar, () => true)
|
||||
|
@ -8,7 +8,7 @@ export interface Props {
|
||||
card: Mastodon.Card
|
||||
}
|
||||
|
||||
const Card: React.FC<Props> = ({ card }) => {
|
||||
const TimelineCard: React.FC<Props> = ({ card }) => {
|
||||
const { theme } = useTheme()
|
||||
const navigation = useNavigation()
|
||||
const onPress = useCallback(() => {
|
||||
@ -83,4 +83,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(Card, () => true)
|
||||
export default React.memo(TimelineCard, () => true)
|
||||
|
@ -8,30 +8,20 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
content: string
|
||||
emojis: Mastodon.Emoji[]
|
||||
mentions: Mastodon.Mention[]
|
||||
spoiler_text?: string
|
||||
status: Mastodon.Status
|
||||
numberOfLines?: number
|
||||
}
|
||||
|
||||
const Content: React.FC<Props> = ({
|
||||
content,
|
||||
emojis,
|
||||
mentions,
|
||||
spoiler_text,
|
||||
numberOfLines
|
||||
}) => {
|
||||
const TimelineContent: React.FC<Props> = ({ status, numberOfLines }) => {
|
||||
const { theme } = useTheme()
|
||||
const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
{content &&
|
||||
(spoiler_text ? (
|
||||
{status.spoiler_text ? (
|
||||
<>
|
||||
<Text style={{ fontSize: StyleConstants.Font.Size.M }}>
|
||||
{spoiler_text}{' '}
|
||||
{status.spoiler_text}{' '}
|
||||
<Text
|
||||
onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}
|
||||
style={{
|
||||
@ -43,25 +33,25 @@ const Content: React.FC<Props> = ({
|
||||
</Text>
|
||||
<Collapsible collapsed={spoilerCollapsed}>
|
||||
<ParseContent
|
||||
content={content}
|
||||
content={status.content}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
emojis={emojis}
|
||||
mentions={mentions}
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
{...(numberOfLines && { numberOfLines: numberOfLines })}
|
||||
/>
|
||||
</Collapsible>
|
||||
</>
|
||||
) : (
|
||||
<ParseContent
|
||||
content={content}
|
||||
content={status.content}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
emojis={emojis}
|
||||
mentions={mentions}
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
{...(numberOfLines && { numberOfLines: numberOfLines })}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Content
|
||||
export default React.memo(TimelineContent, () => true)
|
||||
|
@ -10,13 +10,15 @@ export interface Props {
|
||||
emojis: Mastodon.Emoji[]
|
||||
size: number
|
||||
fontBold?: boolean
|
||||
numberOfLines?: number
|
||||
}
|
||||
|
||||
const Emojis: React.FC<Props> = ({
|
||||
content,
|
||||
emojis,
|
||||
size,
|
||||
fontBold = false
|
||||
fontBold = false,
|
||||
numberOfLines
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const styles = StyleSheet.create({
|
||||
@ -35,7 +37,7 @@ const Emojis: React.FC<Props> = ({
|
||||
const hasEmojis = content.match(regexEmoji)
|
||||
|
||||
return (
|
||||
<Text>
|
||||
<Text numberOfLines={numberOfLines || undefined}>
|
||||
{content.split(regexEmoji).map((str, i) => {
|
||||
if (str.match(regexEmoji)) {
|
||||
const emojiShortcode = str.split(regexEmoji)[1]
|
||||
|
@ -2,152 +2,47 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
|
||||
import Emojis from './Emojis'
|
||||
import relativeTime from 'src/utils/relativeTime'
|
||||
import client from 'src/api/client'
|
||||
import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import BottomSheet from 'src/components/BottomSheet'
|
||||
import BottomSheetRow from 'src/components/BottomSheet/Row'
|
||||
import { toast } from 'src/components/toast'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
type,
|
||||
stateKey
|
||||
}: {
|
||||
id: string
|
||||
type: 'mute' | 'block' | 'domain_blocks' | 'reports'
|
||||
stateKey?: 'muting' | 'blocking'
|
||||
}) => {
|
||||
let res
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'block':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `accounts/${id}/${type}`
|
||||
})
|
||||
|
||||
if (res.body[stateKey!] === true) {
|
||||
toast({ type: 'success', content: '功能成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({ type: 'error', content: '功能错误', autoHide: false })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
case 'domain_blocks':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `domain_blocks`,
|
||||
query: {
|
||||
domain: id || ''
|
||||
}
|
||||
})
|
||||
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '隐藏域名成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '隐藏域名失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
case 'reports':
|
||||
console.log('reporting')
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `reports`,
|
||||
query: {
|
||||
account_id: id || ''
|
||||
}
|
||||
})
|
||||
console.log(res.body)
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '举报账户成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '举报账户失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
import HeaderDefaultActionsAccount from './HeaderDefault/ActionsAccount'
|
||||
import HeaderDefaultActionsStatus from './HeaderDefault/ActionsStatus'
|
||||
import HeaderDefaultActionsDomain from './HeaderDefault/ActionsDomain'
|
||||
|
||||
export interface Props {
|
||||
queryKey: App.QueryKey
|
||||
accountId: string
|
||||
domain: string
|
||||
name: string
|
||||
emojis?: Mastodon.Emoji[]
|
||||
account: string
|
||||
created_at: string
|
||||
visibility: Mastodon.Status['visibility']
|
||||
application?: Mastodon.Application
|
||||
status: Mastodon.Status
|
||||
}
|
||||
|
||||
const HeaderDefault: React.FC<Props> = ({
|
||||
queryKey,
|
||||
accountId,
|
||||
domain,
|
||||
name,
|
||||
emojis,
|
||||
account,
|
||||
created_at,
|
||||
visibility,
|
||||
application
|
||||
}) => {
|
||||
const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
||||
const domain = status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
||||
const name = status.account.display_name || status.account.username
|
||||
const emojis = status.account.emojis
|
||||
const account = status.account.acct
|
||||
const { theme } = useTheme()
|
||||
|
||||
const navigation = useNavigation()
|
||||
const localAccountId = useSelector(getLocalAccountId)
|
||||
const localDomain = useSelector(getLocalUrl)
|
||||
const [since, setSince] = useState(relativeTime(created_at))
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: () => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
},
|
||||
onSettled: () => {
|
||||
queryCache.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
const [since, setSince] = useState(relativeTime(status.created_at))
|
||||
const [modalVisible, setBottomSheetVisible] = useState(false)
|
||||
|
||||
// causing full re-render
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setSince(relativeTime(created_at))
|
||||
setSince(relativeTime(status.created_at))
|
||||
}, 1000)
|
||||
}, [since])
|
||||
|
||||
const onPressAction = useCallback(() => setModalVisible(true), [])
|
||||
const onPressAction = useCallback(() => setBottomSheetVisible(true), [])
|
||||
const onPressApplication = useCallback(() => {
|
||||
navigation.navigate('Webview', {
|
||||
uri: application!.website
|
||||
uri: status.application!.website
|
||||
})
|
||||
}, [])
|
||||
|
||||
@ -188,13 +83,11 @@ const HeaderDefault: React.FC<Props> = ({
|
||||
@{account}
|
||||
</Text>
|
||||
</View>
|
||||
{(accountId !== localAccountId || domain !== localDomain) && (
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={onPressAction}
|
||||
children={pressableAction}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.meta}>
|
||||
@ -203,7 +96,7 @@ const HeaderDefault: React.FC<Props> = ({
|
||||
{since}
|
||||
</Text>
|
||||
</View>
|
||||
{visibility === 'private' && (
|
||||
{status.visibility === 'private' && (
|
||||
<Feather
|
||||
name='lock'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
@ -211,73 +104,44 @@ const HeaderDefault: React.FC<Props> = ({
|
||||
style={styles.visibility}
|
||||
/>
|
||||
)}
|
||||
{application && application.name !== 'Web' && (
|
||||
{status.application && status.application.name !== 'Web' && (
|
||||
<View>
|
||||
<Text
|
||||
onPress={onPressApplication}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
发自于 - {application.name}
|
||||
发自于 - {status.application.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<BottomSheet
|
||||
visible={modalVisible}
|
||||
handleDismiss={() => setModalVisible(false)}
|
||||
handleDismiss={() => setBottomSheetVisible(false)}
|
||||
>
|
||||
{accountId !== localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'mute',
|
||||
stateKey: 'muting'
|
||||
})
|
||||
}}
|
||||
icon='eye-off'
|
||||
text={`隐藏 @${account} 的嘟嘟`}
|
||||
{status.account.id !== localAccountId && (
|
||||
<HeaderDefaultActionsAccount
|
||||
queryKey={queryKey}
|
||||
accountId={status.account.id}
|
||||
account={status.account.acct}
|
||||
setBottomSheetVisible={setBottomSheetVisible}
|
||||
/>
|
||||
)}
|
||||
{accountId !== localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'block',
|
||||
stateKey: 'blocking'
|
||||
})
|
||||
}}
|
||||
icon='x-circle'
|
||||
text={`屏蔽用户 @${account}`}
|
||||
|
||||
{status.account.id === localAccountId && (
|
||||
<HeaderDefaultActionsStatus
|
||||
queryKey={queryKey}
|
||||
status={status}
|
||||
setBottomSheetVisible={setBottomSheetVisible}
|
||||
/>
|
||||
)}
|
||||
|
||||
{domain !== localDomain && (
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: domain,
|
||||
type: 'domain_blocks'
|
||||
})
|
||||
}}
|
||||
icon='cloud-off'
|
||||
text={`屏蔽域名 ${domain}`}
|
||||
/>
|
||||
)}
|
||||
{accountId !== localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'reports'
|
||||
})
|
||||
}}
|
||||
icon='alert-triangle'
|
||||
text={`举报 @${account}`}
|
||||
<HeaderDefaultActionsDomain
|
||||
queryKey={queryKey}
|
||||
domain={domain}
|
||||
setBottomSheetVisible={setBottomSheetVisible}
|
||||
/>
|
||||
)}
|
||||
</BottomSheet>
|
||||
@ -325,4 +189,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(HeaderDefault, () => true)
|
||||
export default React.memo(TimelineHeaderDefault, () => true)
|
||||
|
@ -0,0 +1,130 @@
|
||||
import React from 'react'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
import client from 'src/api/client'
|
||||
import { MenuContainer, MenuHeader, MenuRow } from 'src/components/Menu'
|
||||
import { toast } from 'src/components/toast'
|
||||
|
||||
const fireMutation = async ({
|
||||
type,
|
||||
id,
|
||||
stateKey
|
||||
}: {
|
||||
type: 'mute' | 'block' | 'reports'
|
||||
id: string
|
||||
stateKey?: 'muting' | 'blocking'
|
||||
}) => {
|
||||
let res
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'block':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `accounts/${id}/${type}`
|
||||
})
|
||||
|
||||
if (res.body[stateKey!] === true) {
|
||||
toast({ type: 'success', content: '功能成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({ type: 'error', content: '功能错误', autoHide: false })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
case 'reports':
|
||||
console.log('reporting')
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `reports`,
|
||||
query: {
|
||||
account_id: id!
|
||||
}
|
||||
})
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '举报账户成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '举报账户失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
queryKey: App.QueryKey
|
||||
accountId: string
|
||||
account: string
|
||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
||||
queryKey,
|
||||
accountId,
|
||||
account,
|
||||
setBottomSheetVisible
|
||||
}) => {
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: () => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
},
|
||||
onSettled: () => {
|
||||
queryCache.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading='关于账户' />
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
type: 'mute',
|
||||
id: accountId,
|
||||
stateKey: 'muting'
|
||||
})
|
||||
}}
|
||||
iconFront='eye-off'
|
||||
title={`隐藏 @${account} 的嘟嘟`}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
type: 'block',
|
||||
id: accountId,
|
||||
stateKey: 'blocking'
|
||||
})
|
||||
}}
|
||||
iconFront='x-circle'
|
||||
title={`屏蔽用户 @${account}`}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
type: 'reports',
|
||||
id: accountId
|
||||
})
|
||||
}}
|
||||
iconFront='alert-triangle'
|
||||
title={`举报 @${account}`}
|
||||
/>
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderDefaultActionsAccount
|
@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
import client from 'src/api/client'
|
||||
import MenuContainer from 'src/components/Menu/Container'
|
||||
import MenuHeader from 'src/components/Menu/Header'
|
||||
import MenuRow from 'src/components/Menu/Row'
|
||||
import { toast } from 'src/components/toast'
|
||||
|
||||
const fireMutation = async ({ domain }: { domain: string }) => {
|
||||
const res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `domain_blocks`,
|
||||
query: {
|
||||
domain: domain!
|
||||
}
|
||||
})
|
||||
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '隐藏域名成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '隐藏域名失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
queryKey: App.QueryKey
|
||||
domain: string
|
||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const HeaderDefaultActionsDomain: React.FC<Props> = ({
|
||||
queryKey,
|
||||
domain,
|
||||
setBottomSheetVisible
|
||||
}) => {
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: () => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
},
|
||||
onSettled: () => {
|
||||
queryCache.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading='关于域名' />
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({ domain })
|
||||
}}
|
||||
iconFront='cloud-off'
|
||||
title={`屏蔽域名 ${domain}`}
|
||||
/>
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderDefaultActionsDomain
|
@ -0,0 +1,160 @@
|
||||
import React from 'react'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
import client from 'src/api/client'
|
||||
import { MenuContainer, MenuHeader, MenuRow } from 'src/components/Menu'
|
||||
import { toast } from 'src/components/toast'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
type,
|
||||
stateKey,
|
||||
prevState
|
||||
}: {
|
||||
id: string
|
||||
type: 'mute' | 'pin' | 'delete'
|
||||
stateKey: 'muted' | 'pinned' | 'id'
|
||||
prevState?: boolean
|
||||
}) => {
|
||||
let res
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
|
||||
}) // bug in response from Mastodon
|
||||
|
||||
if (!res.body[stateKey] === prevState) {
|
||||
toast({ type: 'success', content: '功能成功' })
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
toast({ type: 'error', content: '功能错误' })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
case 'delete':
|
||||
res = await client({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
endpoint: `statuses/${id}`
|
||||
})
|
||||
|
||||
if (res.body[stateKey] === id) {
|
||||
toast({ type: 'success', content: '删除成功' })
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
toast({ type: 'error', content: '删除失败' })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
queryKey: App.QueryKey
|
||||
status: Mastodon.Status
|
||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
queryKey,
|
||||
status,
|
||||
setBottomSheetVisible
|
||||
}) => {
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: ({ id, type, stateKey, prevState }) => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
queryCache.setQueryData(queryKey, old =>
|
||||
(old as {}[]).map((paging: any) => ({
|
||||
toots: paging.toots.map((toot: any) => {
|
||||
if (toot.id === id) {
|
||||
toot[stateKey] =
|
||||
typeof prevState === 'boolean' ? !prevState : true
|
||||
}
|
||||
return toot
|
||||
}),
|
||||
pointer: paging.pointer
|
||||
}))
|
||||
)
|
||||
break
|
||||
case 'delete':
|
||||
queryCache.setQueryData(queryKey, old =>
|
||||
(old as {}[]).map((paging: any) => ({
|
||||
toots: paging.toots.filter((toot: any) => toot.id !== id),
|
||||
pointer: paging.pointer
|
||||
}))
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试' })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading='关于嘟嘟' />
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
type: 'delete',
|
||||
id: status.id,
|
||||
stateKey: 'id'
|
||||
})
|
||||
}}
|
||||
iconFront='trash'
|
||||
title='删除嘟嘟'
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
console.warn('功能未开发')
|
||||
}}
|
||||
iconFront='trash'
|
||||
title='删除并重发'
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
type: 'mute',
|
||||
id: status.id,
|
||||
stateKey: 'muted',
|
||||
prevState: status.muted
|
||||
})
|
||||
}}
|
||||
iconFront='volume-x'
|
||||
title={status.muted ? '取消隐藏对话' : '隐藏对话'}
|
||||
/>
|
||||
{/* Also note that reblogs cannot be pinned. */}
|
||||
{(status.visibility === 'public' || status.visibility === 'unlisted') && (
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
type: 'pin',
|
||||
id: status.id,
|
||||
stateKey: 'pinned',
|
||||
prevState: status.pinned
|
||||
})
|
||||
}}
|
||||
iconFront='anchor'
|
||||
title={status.pinned ? '取消置顶' : '置顶'}
|
||||
/>
|
||||
)}
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderDefaultActionsStatus
|
@ -1,52 +1,302 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
import client from 'src/api/client'
|
||||
import Button from 'src/components/Button'
|
||||
import { toast } from 'src/components/toast'
|
||||
import relativeTime from 'src/utils/relativeTime'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
import Emojis from './Emojis'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
options
|
||||
}: {
|
||||
id: string
|
||||
options: { [key: number]: boolean }
|
||||
}) => {
|
||||
const formData = new FormData()
|
||||
Object.keys(options).forEach(option => {
|
||||
// @ts-ignore
|
||||
if (options[option]) {
|
||||
formData.append('choices[]', option)
|
||||
}
|
||||
})
|
||||
console.log(formData)
|
||||
|
||||
const res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `polls/${id}/votes`,
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (res.body.id === id) {
|
||||
toast({ type: 'success', content: '投票成功成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '隐藏域名失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
queryKey: App.QueryKey
|
||||
poll: Mastodon.Poll
|
||||
}
|
||||
// When haven't voted, result should not be shown but intead let people vote
|
||||
const Poll: React.FC<Props> = ({ poll }) => {
|
||||
|
||||
const TimelinePoll: React.FC<Props> = ({ queryKey, poll }) => {
|
||||
console.log('render poll ' + Math.random())
|
||||
console.log(poll)
|
||||
const { theme } = useTheme()
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: ({ id, options }) => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
|
||||
queryCache.setQueryData(queryKey, old =>
|
||||
(old as {}[]).map((paging: any) => ({
|
||||
toots: paging.toots.map((toot: any) => {
|
||||
if (toot.poll?.id === id) {
|
||||
const poll = toot.poll
|
||||
console.log('update votes')
|
||||
console.log(
|
||||
Object.keys(options)
|
||||
.filter(option => options[option])
|
||||
.map(option => parseInt(option))
|
||||
)
|
||||
console.log(toot.poll)
|
||||
toot.poll = {
|
||||
...toot.poll,
|
||||
voters_count: poll.voters_count ? poll.voters_count + 1 : 1,
|
||||
voted: true,
|
||||
own_votes: [
|
||||
Object.keys(options)
|
||||
// @ts-ignore
|
||||
.filter(option => options[option])
|
||||
.map(option => parseInt(option))
|
||||
]
|
||||
}
|
||||
console.log(toot.poll)
|
||||
}
|
||||
return toot
|
||||
}),
|
||||
pointer: paging.pointer
|
||||
}))
|
||||
)
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试' })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
|
||||
const pollExpiration = useMemo(() => {
|
||||
// how many voted
|
||||
if (poll.expired) {
|
||||
return (
|
||||
<View>
|
||||
{poll.options.map((option, index) => (
|
||||
<View key={index}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Text>
|
||||
{Math.round((option.votes_count / poll.votes_count) * 100)}%
|
||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||
投票已结束
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||
截止至{relativeTime(poll.expires_at)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [singleOptions, setSingleOptions] = useState({
|
||||
...[false, false, false, false].slice(0, poll.options.length)
|
||||
})
|
||||
const [multipleOptions, setMultipleOptions] = useState({
|
||||
...[false, false, false, false].slice(0, poll.options.length)
|
||||
})
|
||||
const isSelected = (index: number) => {
|
||||
if (poll.multiple) {
|
||||
return multipleOptions[index] ? 'check-square' : 'square'
|
||||
} else {
|
||||
return singleOptions[index] ? 'check-circle' : 'circle'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{poll.options.map((option, index) =>
|
||||
poll.voted ? (
|
||||
<View key={index} style={styles.poll}>
|
||||
<View style={styles.optionSelected}>
|
||||
<View style={styles.contentSelected}>
|
||||
<Emojis
|
||||
content={option.title}
|
||||
emojis={poll.emojis}
|
||||
size={14}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
{poll.own_votes!.includes(index) && (
|
||||
<Feather
|
||||
style={styles.voted}
|
||||
name={poll.multiple ? 'check-square' : 'check-circle'}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={theme.primary}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.percentage, { color: theme.primary }]}>
|
||||
{Math.round((option.votes_count / poll.votes_count) * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
style={[
|
||||
styles.background,
|
||||
{
|
||||
width: `${Math.round(
|
||||
(option.votes_count / poll.votes_count) * 100
|
||||
)}%`,
|
||||
height: 5,
|
||||
backgroundColor: 'blue'
|
||||
}}
|
||||
backgroundColor: theme.border
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
) : (
|
||||
<View key={index} style={styles.poll}>
|
||||
<Pressable
|
||||
style={[styles.optionUnselected]}
|
||||
onPress={() => {
|
||||
if (poll.multiple) {
|
||||
setMultipleOptions({
|
||||
...multipleOptions,
|
||||
[index]: !multipleOptions[index]
|
||||
})
|
||||
} else {
|
||||
setSingleOptions({
|
||||
...[
|
||||
index === 0,
|
||||
index === 1,
|
||||
index === 2,
|
||||
index === 3
|
||||
].slice(0, poll.options.length)
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Feather
|
||||
style={styles.votedNot}
|
||||
name={isSelected(index)}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<View style={styles.contentUnselected}>
|
||||
<Emojis
|
||||
content={option.title}
|
||||
emojis={poll.emojis}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
<View style={styles.meta}>
|
||||
{!poll.expired && !poll.own_votes?.length && (
|
||||
<View style={styles.button}>
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (poll.multiple) {
|
||||
console.log(multipleOptions)
|
||||
mutateAction({ id: poll.id, options: multipleOptions })
|
||||
} else {
|
||||
mutateAction({ id: poll.id, options: singleOptions })
|
||||
}
|
||||
}}
|
||||
text='投票'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Text style={[styles.votes, { color: theme.secondary }]}>
|
||||
已投{poll.voters_count}人{' • '}
|
||||
</Text>
|
||||
{pollExpiration}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 8
|
||||
base: {
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
poll: {
|
||||
minHeight: StyleConstants.Font.LineHeight.M * 1.5,
|
||||
marginBottom: StyleConstants.Spacing.XS
|
||||
},
|
||||
optionSelected: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingLeft: StyleConstants.Spacing.M,
|
||||
paddingRight: StyleConstants.Spacing.M
|
||||
},
|
||||
optionUnselected: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
contentSelected: {
|
||||
flexBasis: '80%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
contentUnselected: {
|
||||
flexBasis: '90%'
|
||||
},
|
||||
voted: {
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
},
|
||||
votedNot: {
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
},
|
||||
percentage: {
|
||||
fontSize: StyleConstants.Font.Size.M
|
||||
},
|
||||
background: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
minWidth: 1,
|
||||
borderTopRightRadius: 6,
|
||||
borderBottomRightRadius: 6
|
||||
},
|
||||
meta: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: StyleConstants.Spacing.XS
|
||||
},
|
||||
button: {
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
},
|
||||
votes: {
|
||||
fontSize: StyleConstants.Font.Size.S
|
||||
},
|
||||
expiration: {
|
||||
fontSize: StyleConstants.Font.Size.S
|
||||
}
|
||||
})
|
||||
|
||||
export default Poll
|
||||
export default TimelinePoll
|
||||
|
@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import { ActivityIndicator, Text } from 'react-native'
|
||||
import { useQuery } from 'react-query'
|
||||
import { MenuContainer, MenuItem } from 'src/components/Menu'
|
||||
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
||||
|
||||
import { listsFetch } from 'src/utils/fetches/listsFetch'
|
||||
|
||||
@ -20,7 +20,7 @@ const ScreenMeLists: React.FC = () => {
|
||||
break
|
||||
case 'success':
|
||||
lists = data?.map((d: Mastodon.List, i: number) => (
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
key={i}
|
||||
iconFront='list'
|
||||
title={d.title}
|
||||
|
@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { MenuContainer, MenuItem } from 'src/components/Menu'
|
||||
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
||||
|
||||
const Collections: React.FC = () => {
|
||||
const { t } = useTranslation('meRoot')
|
||||
@ -10,22 +10,22 @@ const Collections: React.FC = () => {
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
iconFront='mail'
|
||||
title={t('content.collections.conversations')}
|
||||
onPress={() => navigation.navigate('Screen-Me-Conversations')}
|
||||
/>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
iconFront='bookmark'
|
||||
title={t('content.collections.bookmarks')}
|
||||
onPress={() => navigation.navigate('Screen-Me-Bookmarks')}
|
||||
/>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
iconFront='star'
|
||||
title={t('content.collections.favourites')}
|
||||
onPress={() => navigation.navigate('Screen-Me-Favourites')}
|
||||
/>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
iconFront='list'
|
||||
title={t('content.collections.lists')}
|
||||
onPress={() => navigation.navigate('Screen-Me-Lists')}
|
||||
|
@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { MenuContainer, MenuItem } from 'src/components/Menu'
|
||||
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { t } = useTranslation('meRoot')
|
||||
@ -10,7 +10,7 @@ const Settings: React.FC = () => {
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
iconFront='settings'
|
||||
title={t('content.settings')}
|
||||
onPress={() => navigation.navigate('Screen-Me-Settings')}
|
||||
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { ActionSheetIOS, StyleSheet, Text } from 'react-native'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { MenuContainer, MenuItem } from 'src/components/Menu'
|
||||
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
||||
import {
|
||||
changeLanguage,
|
||||
changeTheme,
|
||||
@ -23,7 +23,7 @@ const ScreenMeSettings: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<MenuContainer marginTop={true}>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
title={t('content.language.heading')}
|
||||
content={t(`content.language.options.${settingsLanguage}`)}
|
||||
iconBack='chevron-right'
|
||||
@ -52,7 +52,7 @@ const ScreenMeSettings: React.FC = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
title={t('content.theme.heading')}
|
||||
content={t(`content.theme.options.${settingsTheme}`)}
|
||||
iconBack='chevron-right'
|
||||
@ -87,10 +87,10 @@ const ScreenMeSettings: React.FC = () => {
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuItem
|
||||
<MenuRow
|
||||
title={t('content.copyrights.heading')}
|
||||
iconBack='chevron-right'
|
||||
></MenuItem>
|
||||
/>
|
||||
<Text style={[styles.version, { color: theme.secondary }]}>
|
||||
{t('content.version', { version: '1.0.0' })}
|
||||
</Text>
|
||||
|
@ -1,11 +1,5 @@
|
||||
import React, { ReactNode, useEffect, useReducer, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
Pressable,
|
||||
Text
|
||||
} from 'react-native'
|
||||
import { Alert, Keyboard, KeyboardAvoidingView } from 'react-native'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
@ -14,6 +8,7 @@ import { store } from 'src/store'
|
||||
import PostMain from './Compose/PostMain'
|
||||
import client from 'src/api/client'
|
||||
import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice'
|
||||
import { HeaderLeft, HeaderRight } from 'src/components/Header'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
|
||||
@ -271,7 +266,7 @@ const Compose: React.FC = () => {
|
||||
name='PostMain'
|
||||
options={{
|
||||
headerLeft: () => (
|
||||
<Pressable
|
||||
<HeaderLeft
|
||||
onPress={() =>
|
||||
Alert.alert('确认取消编辑?', '', [
|
||||
{ text: '继续编辑', style: 'cancel' },
|
||||
@ -282,19 +277,20 @@ const Compose: React.FC = () => {
|
||||
}
|
||||
])
|
||||
}
|
||||
>
|
||||
<Text>退出编辑</Text>
|
||||
</Pressable>
|
||||
text='退出编辑'
|
||||
/>
|
||||
),
|
||||
headerCenter: () => <></>,
|
||||
headerRight: () => (
|
||||
<Pressable onPress={async () => tootPost()}>
|
||||
<Text>发嘟嘟</Text>
|
||||
</Pressable>
|
||||
<HeaderRight
|
||||
onPress={async () => tootPost()}
|
||||
text='发嘟嘟'
|
||||
disabled={postState.text.raw.length < 1}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
{props => (
|
||||
{() => (
|
||||
<PostMain postState={postState} postDispatch={postDispatch} />
|
||||
)}
|
||||
</Stack.Screen>
|
||||
|
@ -5,9 +5,9 @@ import { ActionSheetIOS } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { WebView } from 'react-native-webview'
|
||||
import BottomSheet from 'src/components/BottomSheet'
|
||||
import BottomSheetRow from 'src/components/BottomSheet/Row'
|
||||
|
||||
import { HeaderLeft, HeaderRight } from 'src/components/Header'
|
||||
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
|
||||
@ -63,7 +63,8 @@ const ScreenSharedWebview: React.FC<Props> = ({
|
||||
visible={bottomSheet}
|
||||
handleDismiss={() => showBottomSheet(false)}
|
||||
>
|
||||
<BottomSheetRow
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
ActionSheetIOS.showShareActionSheetWithOptions(
|
||||
{
|
||||
@ -81,17 +82,18 @@ const ScreenSharedWebview: React.FC<Props> = ({
|
||||
}
|
||||
)
|
||||
}}
|
||||
icon='share'
|
||||
text={'分享链接'}
|
||||
iconFront='share'
|
||||
title={'分享链接'}
|
||||
/>
|
||||
<BottomSheetRow
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
showBottomSheet(false)
|
||||
webview.current?.reload()
|
||||
}}
|
||||
icon='refresh-cw'
|
||||
text='刷新'
|
||||
iconFront='refresh-cw'
|
||||
title='刷新'
|
||||
/>
|
||||
</MenuContainer>
|
||||
</BottomSheet>
|
||||
</>
|
||||
)}
|
||||
|
@ -2,11 +2,13 @@ import { DefaultTheme, DarkTheme } from '@react-navigation/native'
|
||||
|
||||
export type ColorDefinitions =
|
||||
| 'primary'
|
||||
| 'primaryOverlay'
|
||||
| 'secondary'
|
||||
| 'disabled'
|
||||
| 'background'
|
||||
| 'backgroundGradientStart'
|
||||
| 'backgroundGradientEnd'
|
||||
| 'backgroundOverlay'
|
||||
| 'link'
|
||||
| 'border'
|
||||
| 'separator'
|
||||
@ -24,6 +26,10 @@ const themeColors: {
|
||||
light: 'rgb(0, 0, 0)',
|
||||
dark: 'rgb(255, 255, 255)'
|
||||
},
|
||||
primaryOverlay: {
|
||||
light: 'rgb(255, 255, 255)',
|
||||
dark: 'rgb(0, 0, 0)'
|
||||
},
|
||||
secondary: {
|
||||
light: 'rgb(153, 153, 153)',
|
||||
dark: 'rgb(117, 117, 117)'
|
||||
@ -44,6 +50,10 @@ const themeColors: {
|
||||
light: 'rgba(255, 255, 255, 1)',
|
||||
dark: 'rgba(0, 0, 0, 1)'
|
||||
},
|
||||
backgroundOverlay: {
|
||||
light: 'rgba(0, 0, 0, 0.5)',
|
||||
dark: 'rgb(255, 255, 255, 0.5)'
|
||||
},
|
||||
link: {
|
||||
light: 'rgb(0, 122, 255)',
|
||||
dark: 'rgb(10, 132, 255)'
|
||||
|
@ -2701,6 +2701,11 @@ expo-av@~8.6.0:
|
||||
lodash "^4.17.15"
|
||||
nullthrows "^1.1.0"
|
||||
|
||||
expo-blur@~8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-8.2.0.tgz#4f586b5019c70f93c9914b2ff6ecadda4a382d51"
|
||||
integrity sha512-LXx8tyVMk1pE4Ug9fHNTIsZnUPewmWXyFhiEwUkeP5SKlg92CgWGbTiGkdhFiU5X0vDxgFKsVVilLcNQxWYSfQ==
|
||||
|
||||
expo-constants@^9.2.0, expo-constants@~9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-9.2.0.tgz#e86a38793deaff9018878afac65bce2543c80a4c"
|
||||
|
Loading…
x
Reference in New Issue
Block a user