1
0
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:
Zhiyuan Zheng 2020-12-03 01:28:56 +01:00
parent 8986680c6d
commit 7d7c907fa3
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
35 changed files with 1125 additions and 766 deletions

View File

@ -17,6 +17,7 @@
"expo": "~39.0.4", "expo": "~39.0.4",
"expo-auth-session": "~2.0.0", "expo-auth-session": "~2.0.0",
"expo-av": "~8.6.0", "expo-av": "~8.6.0",
"expo-blur": "~8.2.0",
"expo-image-picker": "~9.1.1", "expo-image-picker": "~9.1.1",
"expo-linear-gradient": "~8.3.0", "expo-linear-gradient": "~8.3.0",
"expo-localization": "^9.0.0", "expo-localization": "^9.0.0",

View File

@ -48,10 +48,10 @@ declare namespace Mastodon {
// Others // Others
remote_url?: string remote_url?: string
text_url?: string text_url?: string
meta: { meta?: {
original: { width: number; height: number; size: string; aspect: number } original?: { width: number; height: number; size: string; aspect: number }
small: { width: number; height: number; size: string; aspect: number } small?: { width: number; height: number; size: string; aspect: number }
focus: focus?:
| { x: number; y: number } | { x: number; y: number }
| { | {
length: string length: string

View File

@ -103,7 +103,6 @@ const styles = StyleSheet.create({
justifyContent: 'flex-end' justifyContent: 'flex-end'
}, },
container: { container: {
padding: StyleConstants.Spacing.L,
paddingTop: StyleConstants.Spacing.M paddingTop: StyleConstants.Spacing.M
}, },
handle: { handle: {
@ -115,9 +114,10 @@ const styles = StyleSheet.create({
}, },
cancel: { cancel: {
padding: StyleConstants.Spacing.S, padding: StyleConstants.Spacing.S,
marginLeft: StyleConstants.Spacing.L,
marginRight: StyleConstants.Spacing.L,
borderWidth: 1, borderWidth: 1,
borderRadius: 100, borderRadius: 100
// marginBottom: StyleConstants.Spacing.L
}, },
text: { text: {
fontSize: StyleConstants.Font.Size.L, fontSize: StyleConstants.Font.Size.L,

View File

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

View File

@ -34,7 +34,7 @@ const styles = StyleSheet.create({
paddingRight: StyleConstants.Spacing.S paddingRight: StyleConstants.Spacing.S
}, },
text: { text: {
fontSize: StyleConstants.Font.Size.L fontSize: StyleConstants.Font.Size.M
} }
}) })

View File

@ -6,6 +6,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
type PropsBase = { type PropsBase = {
disabled?: boolean
onPress: () => void onPress: () => void
} }
@ -20,6 +21,7 @@ export interface PropsIcon extends PropsBase {
} }
const HeaderRight: React.FC<PropsText | PropsIcon> = ({ const HeaderRight: React.FC<PropsText | PropsIcon> = ({
disabled,
onPress, onPress,
text, text,
icon icon
@ -27,12 +29,21 @@ const HeaderRight: React.FC<PropsText | PropsIcon> = ({
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<Pressable onPress={onPress} style={styles.base}> <Pressable {...(!disabled && { onPress })} style={styles.base}>
{text && <Text style={[styles.text, { color: theme.primary }]}>{text}</Text>} {text && (
<Text
style={[
styles.text,
{ color: disabled ? theme.secondary : theme.primary }
]}
>
{text}
</Text>
)}
{icon && ( {icon && (
<Feather <Feather
name={icon} name={icon}
color={theme.primary} color={disabled ? theme.secondary : theme.primary}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
/> />
)} )}
@ -45,7 +56,7 @@ const styles = StyleSheet.create({
paddingLeft: StyleConstants.Spacing.S paddingLeft: StyleConstants.Spacing.S
}, },
text: { text: {
fontSize: StyleConstants.Font.Size.L fontSize: StyleConstants.Font.Size.M
} }
}) })

View File

@ -1,5 +1,5 @@
import MenuContainer from './Menu/Container' import MenuContainer from './Menu/Container'
import MenuHeader from './Menu/Header' import MenuHeader from './Menu/Header'
import MenuItem from './Menu/Item' import MenuRow from './Menu/Row'
export { MenuContainer, MenuHeader, MenuItem } export { MenuContainer, MenuHeader, MenuRow }

View File

@ -1,36 +1,36 @@
import React from 'react' import React, { Children } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
children: React.ReactNode children: React.ReactNode
marginTop?: boolean
} }
const MenuContainer: React.FC<Props> = ({ ...props }) => { const MenuContainer: React.FC<Props> = ({ children }) => {
const { theme } = useTheme() const { theme } = useTheme()
// @ts-ignore
const firstChild = Children.toArray(children)[0].type.name
return ( return (
<View <View
style={[ style={[
styles.base, styles.base,
{ {
borderTopColor: theme.separator, ...(firstChild !== 'MenuHeader' && {
marginTop: props.marginTop borderTopColor: theme.separator,
? StyleConstants.Spacing.Global.PagePadding borderTopWidth: 1
: 0 })
} }
]} ]}
> >
{props.children} {children}
</View> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
borderTopWidth: 1,
marginBottom: StyleConstants.Spacing.L marginBottom: StyleConstants.Spacing.L
} }
}) })

View File

@ -1,19 +1,28 @@
import React from 'react' 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 { export interface Props {
heading: string heading: string
} }
const MenuHeader: React.FC<Props> = ({ heading }) => { 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({ const styles = StyleSheet.create({
header: { base: {
marginTop: 12, borderBottomWidth: 1,
paddingLeft: 12, paddingLeft: StyleConstants.Spacing.Global.PagePadding,
paddingRight: 12 paddingRight: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.S
} }
}) })

View File

@ -43,29 +43,31 @@ const Core: React.FC<Props> = ({
{title} {title}
</Text> </Text>
</View> </View>
<View style={styles.back}> {(content || iconBack) && (
{content && ( <View style={styles.back}>
<Text {content && (
style={[styles.content, { color: theme.secondary }]} <Text
numberOfLines={1} style={[styles.content, { color: theme.secondary }]}
> numberOfLines={1}
{content} >
</Text> {content}
)} </Text>
{iconBack && ( )}
<Feather {iconBack && (
name={iconBack} <Feather
size={StyleConstants.Font.Size.M + 2} name={iconBack}
color={theme[iconBackColor]} size={StyleConstants.Font.Size.M + 2}
style={styles.iconBack} color={theme[iconBackColor]}
/> style={styles.iconBack}
)} />
</View> )}
</View>
)}
</View> </View>
) )
} }
const MenuItem: React.FC<Props> = ({ ...props }) => { const MenuRow: React.FC<Props> = ({ ...props }) => {
const { theme } = useTheme() const { theme } = useTheme()
return props.onPress ? ( return props.onPress ? (
@ -95,11 +97,13 @@ const styles = StyleSheet.create({
}, },
front: { front: {
flex: 1, flex: 1,
flexBasis: '70%',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
back: { back: {
flex: 1, flex: 1,
flexBasis: '30%',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-end', justifyContent: 'flex-end',
alignItems: 'center' alignItems: 'center'
@ -108,6 +112,7 @@ const styles = StyleSheet.create({
marginRight: 8 marginRight: 8
}, },
text: { text: {
flex: 1,
fontSize: StyleConstants.Font.Size.M fontSize: StyleConstants.Font.Size.M
}, },
content: { content: {
@ -118,4 +123,4 @@ const styles = StyleSheet.create({
} }
}) })
export default MenuItem export default MenuRow

View File

@ -2,14 +2,15 @@ import React, { useCallback, useMemo } from 'react'
import { Dimensions, Pressable, StyleSheet, View } from 'react-native' import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import Actioned from './Shared/Actioned' import TimelineActioned from './Shared/Actioned'
import Avatar from './Shared/Avatar' import TimelineActions from './Shared/Actions'
import HeaderDefault from './Shared/HeaderDefault' import TimelineAttachment from './Shared/Attachment'
import Content from './Shared/Content' import TimelineAvatar from './Shared/Avatar'
import Poll from './Shared/Poll' import TimelineCard from './Shared/Card'
import Attachment from './Shared/Attachment' import TimelineContent from './Shared/Content'
import Card from './Shared/Card' import TimelineHeaderDefault from './Shared/HeaderDefault'
import ActionsStatus from './Shared/ActionsStatus' import TimelinePoll from './Shared/Poll'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
@ -22,6 +23,11 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
const navigation = useNavigation() const navigation = useNavigation()
let actualStatus = item.reblog ? item.reblog : item 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( const pressableToot = useCallback(
() => () =>
@ -33,73 +39,37 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
const childrenToot = useMemo( const childrenToot = useMemo(
() => ( () => (
<> <>
{actualStatus.content ? ( {actualStatus.content.length > 0 && (
<Content <TimelineContent status={actualStatus} />
content={actualStatus.content} )}
emojis={actualStatus.emojis} {actualStatus.poll && (
mentions={actualStatus.mentions} <TimelinePoll queryKey={queryKey} poll={actualStatus.poll} />
spoiler_text={actualStatus.spoiler_text}
// tags={actualStatus.tags}
/>
) : (
<></>
)} )}
{actualStatus.poll && <Poll poll={actualStatus.poll} />}
{actualStatus.media_attachments.length > 0 && ( {actualStatus.media_attachments.length > 0 && (
<Attachment <TimelineAttachment status={actualStatus} width={contentWidth} />
media_attachments={actualStatus.media_attachments}
sensitive={actualStatus.sensitive}
width={
Dimensions.get('window').width - StyleConstants.Spacing.M * 2 - 50 - 8
}
/>
)} )}
{actualStatus.card && <Card card={actualStatus.card} />} {actualStatus.card && <TimelineCard card={actualStatus.card} />}
</> </>
), ),
[] [actualStatus.poll?.voted]
) )
const statusView = useMemo(() => { return (
return ( <View style={styles.statusView}>
<View style={styles.statusView}> {item.reblog && (
{item.reblog && ( <TimelineActioned action='reblog' account={item.account} />
<Actioned )}
action='reblog' <View style={styles.status}>
name={item.account.display_name || item.account.username} <TimelineAvatar account={actualStatus.account} />
emojis={item.account.emojis} <View style={styles.details}>
/> <TimelineHeaderDefault queryKey={queryKey} status={actualStatus} />
)} {/* Can pass toot info to next page to speed up performance */}
<View style={styles.status}> <Pressable onPress={pressableToot} children={childrenToot} />
<Avatar <TimelineActions queryKey={queryKey} status={actualStatus} />
uri={actualStatus.account.avatar}
id={actualStatus.account.id}
/>
<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}
/>
{/* Can pass toot info to next page to speed up performance */}
<Pressable onPress={pressableToot} children={childrenToot} />
<ActionsStatus queryKey={queryKey} status={actualStatus} />
</View>
</View> </View>
</View> </View>
) </View>
}, [item]) )
return statusView
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -114,8 +84,7 @@ const styles = StyleSheet.create({
flexDirection: 'row' flexDirection: 'row'
}, },
details: { details: {
flex: 1, flex: 1
flexGrow: 1
} }
}) })

View File

@ -9,7 +9,7 @@ import Content from './Shared/Content'
import Poll from './Shared/Poll' import Poll from './Shared/Poll'
import Attachment from './Shared/Attachment' import Attachment from './Shared/Attachment'
import Card from './Shared/Card' import Card from './Shared/Card'
import ActionsStatus from './Shared/ActionsStatus' import ActionsStatus from './Shared/Actions'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {

View File

@ -7,19 +7,19 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
account: Mastodon.Account
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog' action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog'
name?: string
emojis?: Mastodon.Emoji[]
notification?: boolean notification?: boolean
} }
const Actioned: React.FC<Props> = ({ const TimelineActioned: React.FC<Props> = ({
account,
action, action,
name,
emojis,
notification = false notification = false
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const name = account.display_name || account.username
const iconColor = theme.primary const iconColor = theme.primary
let icon let icon
@ -74,20 +74,18 @@ const Actioned: React.FC<Props> = ({
return ( return (
<View style={styles.actioned}> <View style={styles.actioned}>
{icon} {icon}
{content ? ( {content && (
<View style={styles.content}> <View style={styles.content}>
{emojis ? ( {account.emojis ? (
<Emojis <Emojis
content={content} content={content}
emojis={emojis} emojis={account.emojis}
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
/> />
) : ( ) : (
<Text>{content}</Text> <Text>{content}</Text>
)} )}
</View> </View>
) : (
<></>
)} )}
</View> </View>
) )
@ -107,4 +105,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(Actioned) export default React.memo(TimelineActioned, () => true)

View File

@ -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 { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryCache } from 'react-query' import { useMutation, useQueryCache } from 'react-query'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import client from 'src/api/client' import client from 'src/api/client'
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import { toast } from 'src/components/toast' import { toast } from 'src/components/toast'
import { useSelector } from 'react-redux'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import BottomSheet from 'src/components/BottomSheet'
import BottomSheetRow from 'src/components/BottomSheet/Row'
const fireMutation = async ({ const fireMutation = async ({
id, id,
@ -19,14 +15,8 @@ const fireMutation = async ({
prevState prevState
}: { }: {
id: string id: string
type: 'favourite' | 'reblog' | 'bookmark' | 'mute' | 'pin' | 'delete' type: 'favourite' | 'reblog' | 'bookmark'
stateKey: stateKey: 'favourited' | 'reblogged' | 'bookmarked'
| 'favourited'
| 'reblogged'
| 'bookmarked'
| 'muted'
| 'pinned'
| 'id'
prevState?: boolean prevState?: boolean
}) => { }) => {
let res let res
@ -34,8 +24,6 @@ const fireMutation = async ({
case 'favourite': case 'favourite':
case 'reblog': case 'reblog':
case 'bookmark': case 'bookmark':
case 'mute':
case 'pin':
res = await client({ res = await client({
method: 'post', method: 'post',
instance: 'local', instance: 'local',
@ -50,21 +38,6 @@ const fireMutation = async ({
return Promise.reject() return Promise.reject()
} }
break 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 status: Mastodon.Status
} }
const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => { const TimelineActions: React.FC<Props> = ({ queryKey, status }) => {
const { theme } = useTheme() const { theme } = useTheme()
const iconColor = theme.secondary const iconColor = theme.secondary
const iconColorAction = (state: boolean) => const iconColorAction = (state: boolean) =>
state ? theme.primary : theme.secondary state ? theme.primary : theme.secondary
const localAccountId = useSelector(getLocalAccountId)
const [bottomSheetVisible, setBottomSheetVisible] = useState(false)
const queryCache = useQueryCache() const queryCache = useQueryCache()
const [mutateAction] = useMutation(fireMutation, { const [mutateAction] = useMutation(fireMutation, {
onMutate: ({ id, type, stateKey, prevState }) => { onMutate: ({ id, type, stateKey, prevState }) => {
@ -92,8 +62,6 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
case 'favourite': case 'favourite':
case 'reblog': case 'reblog':
case 'bookmark': case 'bookmark':
case 'mute':
case 'pin':
queryCache.setQueryData(queryKey, old => queryCache.setQueryData(queryKey, old =>
(old as {}[]).map((paging: any) => ({ (old as {}[]).map((paging: any) => ({
toots: paging.toots.map((toot: any) => { toots: paging.toots.map((toot: any) => {
@ -107,19 +75,6 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
})) }))
) )
break 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 return oldData
@ -161,7 +116,23 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
}), }),
[status.bookmarked] [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( const childrenReply = useMemo(
() => ( () => (
@ -268,86 +239,6 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
children={childrenShare} children={childrenShare}
/> />
</View> </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%', width: '20%',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center' 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

View File

@ -1,65 +1,58 @@
import React from 'react' import { BlurView } from 'expo-blur'
import { Text, View } from 'react-native' 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 AttachmentImage from './Attachment/AttachmentImage'
import AttachmentVideo from './Attachment/AttachmentVideo' import AttachmentVideo from './Attachment/AttachmentVideo'
export interface Props { export interface Props {
media_attachments: Mastodon.Attachment[] status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
sensitive: boolean
width: number width: number
} }
const Attachment: React.FC<Props> = ({ const TimelineAttachment: React.FC<Props> = ({ status, width }) => {
media_attachments, const { mode, theme } = useTheme()
sensitive, const allTypes = status.media_attachments.map(m => m.type)
width
}) => {
let attachment let attachment
let attachmentHeight let attachmentHeight
// if (width) {}
switch (media_attachments[0].type) { if (allTypes.includes('image')) {
case 'unknown': attachment = (
attachment = <Text></Text> <AttachmentImage
attachmentHeight = 25 media_attachments={status.media_attachments}
break width={width}
case 'image': />
attachment = ( )
<AttachmentImage attachmentHeight = (width / 16) * 9
media_attachments={media_attachments} } else if (allTypes.includes('gifv')) {
sensitive={sensitive} attachment = (
width={width} <AttachmentVideo
/> media_attachments={status.media_attachments}
) width={width}
attachmentHeight = width / 2 />
break )
case 'gifv': attachmentHeight =
attachment = ( status.media_attachments[0].meta?.original?.width &&
<AttachmentVideo status.media_attachments[0].meta?.original?.height
media_attachments={media_attachments} ? (width / status.media_attachments[0].meta.original.width) *
sensitive={sensitive} status.media_attachments[0].meta.original.height
width={width} : (width / 16) * 9
/> } else if (allTypes.includes('video')) {
) attachment = (
attachmentHeight = <AttachmentVideo
(width / media_attachments[0].meta.original.width) * media_attachments={status.media_attachments}
media_attachments[0].meta.original.height width={width}
break />
// Support multiple video )
// Supoort when video meta is empty attachmentHeight =
// case 'video': status.media_attachments[0].meta?.original?.width &&
// attachment = ( status.media_attachments[0].meta?.original?.height
// <AttachmentVideo ? (width / status.media_attachments[0].meta.original.width) *
// media_attachments={media_attachments} status.media_attachments[0].meta.original.height
// sensitive={sensitive} : (width / 16) * 9
// width={width} } else if (allTypes.includes('audio')) {
// />
// )
// attachmentHeight =
// (width / media_attachments[0].meta.original.width) *
// media_attachments[0].meta.original.height
// break
// case 'audio':
// attachment = ( // attachment = (
// <AttachmentAudio // <AttachmentAudio
// media_attachments={media_attachments} // media_attachments={media_attachments}
@ -67,21 +60,60 @@ const Attachment: React.FC<Props> = ({
// width={width} // 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 ( return (
<View <View
style={{ style={{
width: width + 8, width: width + StyleConstants.Spacing.XS,
height: attachmentHeight, height: attachmentHeight,
marginTop: 4, marginTop: StyleConstants.Spacing.S,
marginLeft: -4 marginLeft: -StyleConstants.Spacing.XS / 2
}} }}
> >
{attachment} {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> </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)

View File

@ -1,75 +1,46 @@
import React, { useEffect, useState } from 'react' import React, { useState } from 'react'
import { Button, Image, Modal, StyleSheet, Pressable, View } from 'react-native' import { Image, Modal, StyleSheet, Pressable, View } from 'react-native'
import ImageViewer from 'react-native-image-zoom-viewer' import ImageViewer from 'react-native-image-zoom-viewer'
import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
media_attachments: Mastodon.Attachment[] media_attachments: Mastodon.Attachment[]
sensitive: boolean
width: number width: number
} }
const AttachmentImage: React.FC<Props> = ({ const AttachmentImage: React.FC<Props> = ({ media_attachments }) => {
media_attachments,
sensitive,
width
}) => {
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
const [imageModalVisible, setImageModalVisible] = useState(false) const [imageModalVisible, setImageModalVisible] = useState(false)
const [imageModalIndex, setImageModalIndex] = useState(0) 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) => { const imagesNode = media_attachments.map((m, i) => {
images.push({ images.push({
url: m.url, url: m.url,
width: m.meta.original.width, width: m.meta?.original?.width || undefined,
height: m.meta.original.height height: m.meta?.original?.height || undefined
}) })
return ( return (
<Pressable <Pressable
key={i} key={i}
style={{ flexGrow: 1, height: width / 2, margin: 4 }} style={[styles.imageContainer]}
onPress={() => { onPress={() => {
setImageModalIndex(i) setImageModalIndex(i)
setImageModalVisible(true) setImageModalVisible(true)
}} }}
> >
<Image <Image source={{ uri: m.preview_url }} style={styles.image} />
source={{ uri: m.preview_url }}
style={styles.image}
blurRadius={mediaSensitive ? width / 5 : 0}
/>
</Pressable> </Pressable>
) )
}) })
return ( return (
<> <>
<View style={styles.media}> <View style={[styles.media]}>{imagesNode}</View>
{imagesNode}
{mediaSensitive && (
<View
style={{
position: 'absolute',
width: '100%',
height: '100%'
}}
>
<Button
title='Press me'
onPress={() => {
setMediaSensitive(false)
}}
/>
</View>
)}
</View>
<Modal <Modal
visible={imageModalVisible} visible={imageModalVisible}
transparent={true} transparent={true}
@ -82,6 +53,7 @@ const AttachmentImage: React.FC<Props> = ({
enableSwipeDown={true} enableSwipeDown={true}
swipeDownThreshold={100} swipeDownThreshold={100}
useNativeDriver={true} useNativeDriver={true}
saveToLocalByLongPress={false}
/> />
</Modal> </Modal>
</> </>
@ -91,16 +63,18 @@ const AttachmentImage: React.FC<Props> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
media: { media: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'stretch',
alignContent: 'stretch' alignContent: 'stretch'
}, },
imageContainer: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2
},
image: { image: {
width: '100%', flex: 1
height: '100%'
} }
}) })
export default AttachmentImage export default React.memo(AttachmentImage, () => true)

View File

@ -1,27 +1,29 @@
import React, { useRef, useState } from 'react' import React, { useCallback, useRef, useState } from 'react'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { Video } from 'expo-av' import { Video } from 'expo-av'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
export interface Props { export interface Props {
media_attachments: Mastodon.Attachment[] media_attachments: Mastodon.Attachment[]
sensitive: boolean
width: number width: number
} }
const AttachmentVideo: React.FC<Props> = ({ const AttachmentVideo: React.FC<Props> = ({ media_attachments, width }) => {
media_attachments, const videoPlayer = useRef<Video>(null)
sensitive,
width
}) => {
const videoPlayer = useRef()
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
const [videoPlay, setVideoPlay] = useState(false) const [videoPlay, setVideoPlay] = useState(false)
const video = media_attachments[0] const video = media_attachments[0]
const videoWidth = width const videoWidth = width
const videoHeight = 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 ( return (
<View <View
@ -39,17 +41,13 @@ const AttachmentVideo: React.FC<Props> = ({
}} }}
resizeMode='cover' resizeMode='cover'
usePoster usePoster
posterSourceThe={{ uri: video.preview_url }} posterSource={{ uri: video.preview_url }}
useNativeControls useNativeControls
shouldPlay={videoPlay} shouldPlay={videoPlay}
/> />
{!videoPlay && ( {videoPlayer.current && !videoPlay && (
<Pressable <Pressable
onPress={() => { onPress={onPressVideo}
setMediaSensitive(false)
videoPlayer.current.presentFullscreenPlayer()
setVideoPlay(true)
}}
style={{ style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,

View File

@ -4,22 +4,21 @@ import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
uri: string account: Mastodon.Account
id: string
} }
const Avatar: React.FC<Props> = ({ uri, id }) => { const TimelineAvatar: React.FC<Props> = ({ account }) => {
const navigation = useNavigation() const navigation = useNavigation()
// Need to fix go back root // Need to fix go back root
const onPress = useCallback(() => { const onPress = useCallback(() => {
navigation.navigate('Screen-Shared-Account', { navigation.navigate('Screen-Shared-Account', {
id: id id: account.id
}) })
}, []) }, [])
return ( return (
<Pressable style={styles.avatar} onPress={onPress}> <Pressable style={styles.avatar} onPress={onPress}>
<Image source={{ uri: uri }} style={styles.image} /> <Image source={{ uri: account.avatar }} style={styles.image} />
</Pressable> </Pressable>
) )
} }
@ -37,4 +36,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(Avatar, () => true) export default React.memo(TimelineAvatar, () => true)

View File

@ -8,7 +8,7 @@ export interface Props {
card: Mastodon.Card card: Mastodon.Card
} }
const Card: React.FC<Props> = ({ card }) => { const TimelineCard: React.FC<Props> = ({ card }) => {
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
const onPress = useCallback(() => { const onPress = useCallback(() => {
@ -83,4 +83,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(Card, () => true) export default React.memo(TimelineCard, () => true)

View File

@ -8,60 +8,50 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
content: string status: Mastodon.Status
emojis: Mastodon.Emoji[]
mentions: Mastodon.Mention[]
spoiler_text?: string
numberOfLines?: number numberOfLines?: number
} }
const Content: React.FC<Props> = ({ const TimelineContent: React.FC<Props> = ({ status, numberOfLines }) => {
content,
emojis,
mentions,
spoiler_text,
numberOfLines
}) => {
const { theme } = useTheme() const { theme } = useTheme()
const [spoilerCollapsed, setSpoilerCollapsed] = useState(true) const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
return ( return (
<> <>
{content && {status.spoiler_text ? (
(spoiler_text ? ( <>
<> <Text style={{ fontSize: StyleConstants.Font.Size.M }}>
<Text style={{ fontSize: StyleConstants.Font.Size.M }}> {status.spoiler_text}{' '}
{spoiler_text}{' '} <Text
<Text onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}
onPress={() => setSpoilerCollapsed(!spoilerCollapsed)} style={{
style={{ color: theme.link
color: theme.link }}
}} >
> {spoilerCollapsed ? '点击展开' : '点击收起'}
{spoilerCollapsed ? '点击展开' : '点击收起'}
</Text>
</Text> </Text>
<Collapsible collapsed={spoilerCollapsed}> </Text>
<ParseContent <Collapsible collapsed={spoilerCollapsed}>
content={content} <ParseContent
size={StyleConstants.Font.Size.M} content={status.content}
emojis={emojis} size={StyleConstants.Font.Size.M}
mentions={mentions} emojis={status.emojis}
{...(numberOfLines && { numberOfLines: numberOfLines })} mentions={status.mentions}
/> {...(numberOfLines && { numberOfLines: numberOfLines })}
</Collapsible> />
</> </Collapsible>
) : ( </>
<ParseContent ) : (
content={content} <ParseContent
size={StyleConstants.Font.Size.M} content={status.content}
emojis={emojis} size={StyleConstants.Font.Size.M}
mentions={mentions} emojis={status.emojis}
{...(numberOfLines && { numberOfLines: numberOfLines })} mentions={status.mentions}
/> {...(numberOfLines && { numberOfLines: numberOfLines })}
))} />
)}
</> </>
) )
} }
export default Content export default React.memo(TimelineContent, () => true)

View File

@ -10,13 +10,15 @@ export interface Props {
emojis: Mastodon.Emoji[] emojis: Mastodon.Emoji[]
size: number size: number
fontBold?: boolean fontBold?: boolean
numberOfLines?: number
} }
const Emojis: React.FC<Props> = ({ const Emojis: React.FC<Props> = ({
content, content,
emojis, emojis,
size, size,
fontBold = false fontBold = false,
numberOfLines
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -35,7 +37,7 @@ const Emojis: React.FC<Props> = ({
const hasEmojis = content.match(regexEmoji) const hasEmojis = content.match(regexEmoji)
return ( return (
<Text> <Text numberOfLines={numberOfLines || undefined}>
{content.split(regexEmoji).map((str, i) => { {content.split(regexEmoji).map((str, i) => {
if (str.match(regexEmoji)) { if (str.match(regexEmoji)) {
const emojiShortcode = str.split(regexEmoji)[1] const emojiShortcode = str.split(regexEmoji)[1]

View File

@ -2,152 +2,47 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import { useMutation, useQueryCache } from 'react-query'
import Emojis from './Emojis' import Emojis from './Emojis'
import relativeTime from 'src/utils/relativeTime' import relativeTime from 'src/utils/relativeTime'
import client from 'src/api/client'
import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice' import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import BottomSheet from 'src/components/BottomSheet' 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 { useSelector } from 'react-redux'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import HeaderDefaultActionsAccount from './HeaderDefault/ActionsAccount'
const fireMutation = async ({ import HeaderDefaultActionsStatus from './HeaderDefault/ActionsStatus'
id, import HeaderDefaultActionsDomain from './HeaderDefault/ActionsDomain'
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
}
}
export interface Props { export interface Props {
queryKey: App.QueryKey queryKey: App.QueryKey
accountId: string status: Mastodon.Status
domain: string
name: string
emojis?: Mastodon.Emoji[]
account: string
created_at: string
visibility: Mastodon.Status['visibility']
application?: Mastodon.Application
} }
const HeaderDefault: React.FC<Props> = ({ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
queryKey, const domain = status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
accountId, const name = status.account.display_name || status.account.username
domain, const emojis = status.account.emojis
name, const account = status.account.acct
emojis,
account,
created_at,
visibility,
application
}) => {
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
const localAccountId = useSelector(getLocalAccountId) const localAccountId = useSelector(getLocalAccountId)
const localDomain = useSelector(getLocalUrl) const localDomain = useSelector(getLocalUrl)
const [since, setSince] = useState(relativeTime(created_at)) const [since, setSince] = useState(relativeTime(status.created_at))
const [modalVisible, setModalVisible] = useState(false) const [modalVisible, setBottomSheetVisible] = 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)
}
})
// causing full re-render // causing full re-render
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setSince(relativeTime(created_at)) setSince(relativeTime(status.created_at))
}, 1000) }, 1000)
}, [since]) }, [since])
const onPressAction = useCallback(() => setModalVisible(true), []) const onPressAction = useCallback(() => setBottomSheetVisible(true), [])
const onPressApplication = useCallback(() => { const onPressApplication = useCallback(() => {
navigation.navigate('Webview', { navigation.navigate('Webview', {
uri: application!.website uri: status.application!.website
}) })
}, []) }, [])
@ -188,13 +83,11 @@ const HeaderDefault: React.FC<Props> = ({
@{account} @{account}
</Text> </Text>
</View> </View>
{(accountId !== localAccountId || domain !== localDomain) && ( <Pressable
<Pressable style={styles.action}
style={styles.action} onPress={onPressAction}
onPress={onPressAction} children={pressableAction}
children={pressableAction} />
/>
)}
</View> </View>
<View style={styles.meta}> <View style={styles.meta}>
@ -203,7 +96,7 @@ const HeaderDefault: React.FC<Props> = ({
{since} {since}
</Text> </Text>
</View> </View>
{visibility === 'private' && ( {status.visibility === 'private' && (
<Feather <Feather
name='lock' name='lock'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
@ -211,73 +104,44 @@ const HeaderDefault: React.FC<Props> = ({
style={styles.visibility} style={styles.visibility}
/> />
)} )}
{application && application.name !== 'Web' && ( {status.application && status.application.name !== 'Web' && (
<View> <View>
<Text <Text
onPress={onPressApplication} onPress={onPressApplication}
style={[styles.application, { color: theme.secondary }]} style={[styles.application, { color: theme.secondary }]}
> >
- {application.name} - {status.application.name}
</Text> </Text>
</View> </View>
)} )}
</View> </View>
<BottomSheet <BottomSheet
visible={modalVisible} visible={modalVisible}
handleDismiss={() => setModalVisible(false)} handleDismiss={() => setBottomSheetVisible(false)}
> >
{accountId !== localAccountId && ( {status.account.id !== localAccountId && (
<BottomSheetRow <HeaderDefaultActionsAccount
onPress={() => { queryKey={queryKey}
setModalVisible(false) accountId={status.account.id}
mutateAction({ account={status.account.acct}
id: accountId, setBottomSheetVisible={setBottomSheetVisible}
type: 'mute',
stateKey: 'muting'
})
}}
icon='eye-off'
text={`隐藏 @${account} 的嘟嘟`}
/> />
)} )}
{accountId !== localAccountId && (
<BottomSheetRow {status.account.id === localAccountId && (
onPress={() => { <HeaderDefaultActionsStatus
setModalVisible(false) queryKey={queryKey}
mutateAction({ status={status}
id: accountId, setBottomSheetVisible={setBottomSheetVisible}
type: 'block',
stateKey: 'blocking'
})
}}
icon='x-circle'
text={`屏蔽用户 @${account}`}
/> />
)} )}
{domain !== localDomain && ( {domain !== localDomain && (
<BottomSheetRow <HeaderDefaultActionsDomain
onPress={() => { queryKey={queryKey}
setModalVisible(false) domain={domain}
mutateAction({ setBottomSheetVisible={setBottomSheetVisible}
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}`}
/> />
)} )}
</BottomSheet> </BottomSheet>
@ -325,4 +189,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(HeaderDefault, () => true) export default React.memo(TimelineHeaderDefault, () => true)

View File

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

View File

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

View File

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

View File

@ -1,52 +1,302 @@
import React from 'react' import { Feather } from '@expo/vector-icons'
import { StyleSheet, Text, View } from 'react-native' 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' 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 { export interface Props {
queryKey: App.QueryKey
poll: Mastodon.Poll 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 (
<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 ( return (
<View> <View style={styles.base}>
{poll.options.map((option, index) => ( {poll.options.map((option, index) =>
<View key={index}> poll.voted ? (
<View style={{ flexDirection: 'row' }}> <View key={index} style={styles.poll}>
<Text> <View style={styles.optionSelected}>
{Math.round((option.votes_count / poll.votes_count) * 100)}% <View style={styles.contentSelected}>
</Text> <Emojis
<Emojis content={option.title}
content={option.title} emojis={poll.emojis}
emojis={poll.emojis} size={StyleConstants.Font.Size.M}
size={14} 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={[
styles.background,
{
width: `${Math.round(
(option.votes_count / poll.votes_count) * 100
)}%`,
backgroundColor: theme.border
}
]}
/> />
</View> </View>
<View ) : (
style={{ <View key={index} style={styles.poll}>
width: `${Math.round( <Pressable
(option.votes_count / poll.votes_count) * 100 style={[styles.optionUnselected]}
)}%`, onPress={() => {
height: 5, if (poll.multiple) {
backgroundColor: 'blue' setMultipleOptions({
}} ...multipleOptions,
/> [index]: !multipleOptions[index]
</View> })
))} } 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> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
avatar: { base: {
width: 50, marginTop: StyleConstants.Spacing.M
height: 50,
marginRight: 8
}, },
image: { poll: {
width: '100%', minHeight: StyleConstants.Font.LineHeight.M * 1.5,
height: '100%' 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

View File

@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
import React from 'react' import React from 'react'
import { ActivityIndicator, Text } from 'react-native' import { ActivityIndicator, Text } from 'react-native'
import { useQuery } from 'react-query' 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' import { listsFetch } from 'src/utils/fetches/listsFetch'
@ -20,7 +20,7 @@ const ScreenMeLists: React.FC = () => {
break break
case 'success': case 'success':
lists = data?.map((d: Mastodon.List, i: number) => ( lists = data?.map((d: Mastodon.List, i: number) => (
<MenuItem <MenuRow
key={i} key={i}
iconFront='list' iconFront='list'
title={d.title} title={d.title}

View File

@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MenuContainer, MenuItem } from 'src/components/Menu' import { MenuContainer, MenuRow } from 'src/components/Menu'
const Collections: React.FC = () => { const Collections: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
@ -10,22 +10,22 @@ const Collections: React.FC = () => {
return ( return (
<MenuContainer> <MenuContainer>
<MenuItem <MenuRow
iconFront='mail' iconFront='mail'
title={t('content.collections.conversations')} title={t('content.collections.conversations')}
onPress={() => navigation.navigate('Screen-Me-Conversations')} onPress={() => navigation.navigate('Screen-Me-Conversations')}
/> />
<MenuItem <MenuRow
iconFront='bookmark' iconFront='bookmark'
title={t('content.collections.bookmarks')} title={t('content.collections.bookmarks')}
onPress={() => navigation.navigate('Screen-Me-Bookmarks')} onPress={() => navigation.navigate('Screen-Me-Bookmarks')}
/> />
<MenuItem <MenuRow
iconFront='star' iconFront='star'
title={t('content.collections.favourites')} title={t('content.collections.favourites')}
onPress={() => navigation.navigate('Screen-Me-Favourites')} onPress={() => navigation.navigate('Screen-Me-Favourites')}
/> />
<MenuItem <MenuRow
iconFront='list' iconFront='list'
title={t('content.collections.lists')} title={t('content.collections.lists')}
onPress={() => navigation.navigate('Screen-Me-Lists')} onPress={() => navigation.navigate('Screen-Me-Lists')}

View File

@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MenuContainer, MenuItem } from 'src/components/Menu' import { MenuContainer, MenuRow } from 'src/components/Menu'
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
@ -10,7 +10,7 @@ const Settings: React.FC = () => {
return ( return (
<MenuContainer> <MenuContainer>
<MenuItem <MenuRow
iconFront='settings' iconFront='settings'
title={t('content.settings')} title={t('content.settings')}
onPress={() => navigation.navigate('Screen-Me-Settings')} onPress={() => navigation.navigate('Screen-Me-Settings')}

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { ActionSheetIOS, StyleSheet, Text } from 'react-native' import { ActionSheetIOS, StyleSheet, Text } from 'react-native'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { MenuContainer, MenuItem } from 'src/components/Menu' import { MenuContainer, MenuRow } from 'src/components/Menu'
import { import {
changeLanguage, changeLanguage,
changeTheme, changeTheme,
@ -23,7 +23,7 @@ const ScreenMeSettings: React.FC = () => {
return ( return (
<> <>
<MenuContainer marginTop={true}> <MenuContainer marginTop={true}>
<MenuItem <MenuRow
title={t('content.language.heading')} title={t('content.language.heading')}
content={t(`content.language.options.${settingsLanguage}`)} content={t(`content.language.options.${settingsLanguage}`)}
iconBack='chevron-right' iconBack='chevron-right'
@ -52,7 +52,7 @@ const ScreenMeSettings: React.FC = () => {
) )
} }
/> />
<MenuItem <MenuRow
title={t('content.theme.heading')} title={t('content.theme.heading')}
content={t(`content.theme.options.${settingsTheme}`)} content={t(`content.theme.options.${settingsTheme}`)}
iconBack='chevron-right' iconBack='chevron-right'
@ -87,10 +87,10 @@ const ScreenMeSettings: React.FC = () => {
/> />
</MenuContainer> </MenuContainer>
<MenuContainer> <MenuContainer>
<MenuItem <MenuRow
title={t('content.copyrights.heading')} title={t('content.copyrights.heading')}
iconBack='chevron-right' iconBack='chevron-right'
></MenuItem> />
<Text style={[styles.version, { color: theme.secondary }]}> <Text style={[styles.version, { color: theme.secondary }]}>
{t('content.version', { version: '1.0.0' })} {t('content.version', { version: '1.0.0' })}
</Text> </Text>

View File

@ -1,11 +1,5 @@
import React, { ReactNode, useEffect, useReducer, useState } from 'react' import React, { ReactNode, useEffect, useReducer, useState } from 'react'
import { import { Alert, Keyboard, KeyboardAvoidingView } from 'react-native'
Alert,
Keyboard,
KeyboardAvoidingView,
Pressable,
Text
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
@ -14,6 +8,7 @@ import { store } from 'src/store'
import PostMain from './Compose/PostMain' import PostMain from './Compose/PostMain'
import client from 'src/api/client' import client from 'src/api/client'
import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice' import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice'
import { HeaderLeft, HeaderRight } from 'src/components/Header'
const Stack = createNativeStackNavigator() const Stack = createNativeStackNavigator()
@ -271,7 +266,7 @@ const Compose: React.FC = () => {
name='PostMain' name='PostMain'
options={{ options={{
headerLeft: () => ( headerLeft: () => (
<Pressable <HeaderLeft
onPress={() => onPress={() =>
Alert.alert('确认取消编辑?', '', [ Alert.alert('确认取消编辑?', '', [
{ text: '继续编辑', style: 'cancel' }, { text: '继续编辑', style: 'cancel' },
@ -282,19 +277,20 @@ const Compose: React.FC = () => {
} }
]) ])
} }
> text='退出编辑'
<Text>退</Text> />
</Pressable>
), ),
headerCenter: () => <></>, headerCenter: () => <></>,
headerRight: () => ( headerRight: () => (
<Pressable onPress={async () => tootPost()}> <HeaderRight
<Text></Text> onPress={async () => tootPost()}
</Pressable> text='发嘟嘟'
disabled={postState.text.raw.length < 1}
/>
) )
}} }}
> >
{props => ( {() => (
<PostMain postState={postState} postDispatch={postDispatch} /> <PostMain postState={postState} postDispatch={postDispatch} />
)} )}
</Stack.Screen> </Stack.Screen>

View File

@ -5,9 +5,9 @@ import { ActionSheetIOS } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { WebView } from 'react-native-webview' import { WebView } from 'react-native-webview'
import BottomSheet from 'src/components/BottomSheet' import BottomSheet from 'src/components/BottomSheet'
import BottomSheetRow from 'src/components/BottomSheet/Row'
import { HeaderLeft, HeaderRight } from 'src/components/Header' import { HeaderLeft, HeaderRight } from 'src/components/Header'
import { MenuContainer, MenuRow } from 'src/components/Menu'
const Stack = createNativeStackNavigator() const Stack = createNativeStackNavigator()
@ -63,35 +63,37 @@ const ScreenSharedWebview: React.FC<Props> = ({
visible={bottomSheet} visible={bottomSheet}
handleDismiss={() => showBottomSheet(false)} handleDismiss={() => showBottomSheet(false)}
> >
<BottomSheetRow <MenuContainer>
onPress={() => { <MenuRow
ActionSheetIOS.showShareActionSheetWithOptions( onPress={() => {
{ ActionSheetIOS.showShareActionSheetWithOptions(
url: uri, {
excludedActivityTypes: [ url: uri,
'com.apple.UIKit.activity.Mail', excludedActivityTypes: [
'com.apple.UIKit.activity.Print', 'com.apple.UIKit.activity.Mail',
'com.apple.UIKit.activity.SaveToCameraRoll', 'com.apple.UIKit.activity.Print',
'com.apple.UIKit.activity.OpenInIBooks' 'com.apple.UIKit.activity.SaveToCameraRoll',
] 'com.apple.UIKit.activity.OpenInIBooks'
}, ]
() => {}, },
() => { () => {},
showBottomSheet(false) () => {
} showBottomSheet(false)
) }
}} )
icon='share' }}
text={'分享链接'} iconFront='share'
/> title={'分享链接'}
<BottomSheetRow />
onPress={() => { <MenuRow
showBottomSheet(false) onPress={() => {
webview.current?.reload() showBottomSheet(false)
}} webview.current?.reload()
icon='refresh-cw' }}
text='刷新' iconFront='refresh-cw'
/> title='刷新'
/>
</MenuContainer>
</BottomSheet> </BottomSheet>
</> </>
)} )}

View File

@ -2,11 +2,13 @@ import { DefaultTheme, DarkTheme } from '@react-navigation/native'
export type ColorDefinitions = export type ColorDefinitions =
| 'primary' | 'primary'
| 'primaryOverlay'
| 'secondary' | 'secondary'
| 'disabled' | 'disabled'
| 'background' | 'background'
| 'backgroundGradientStart' | 'backgroundGradientStart'
| 'backgroundGradientEnd' | 'backgroundGradientEnd'
| 'backgroundOverlay'
| 'link' | 'link'
| 'border' | 'border'
| 'separator' | 'separator'
@ -24,6 +26,10 @@ const themeColors: {
light: 'rgb(0, 0, 0)', light: 'rgb(0, 0, 0)',
dark: 'rgb(255, 255, 255)' dark: 'rgb(255, 255, 255)'
}, },
primaryOverlay: {
light: 'rgb(255, 255, 255)',
dark: 'rgb(0, 0, 0)'
},
secondary: { secondary: {
light: 'rgb(153, 153, 153)', light: 'rgb(153, 153, 153)',
dark: 'rgb(117, 117, 117)' dark: 'rgb(117, 117, 117)'
@ -44,6 +50,10 @@ const themeColors: {
light: 'rgba(255, 255, 255, 1)', light: 'rgba(255, 255, 255, 1)',
dark: 'rgba(0, 0, 0, 1)' dark: 'rgba(0, 0, 0, 1)'
}, },
backgroundOverlay: {
light: 'rgba(0, 0, 0, 0.5)',
dark: 'rgb(255, 255, 255, 0.5)'
},
link: { link: {
light: 'rgb(0, 122, 255)', light: 'rgb(0, 122, 255)',
dark: 'rgb(10, 132, 255)' dark: 'rgb(10, 132, 255)'

View File

@ -2701,6 +2701,11 @@ expo-av@~8.6.0:
lodash "^4.17.15" lodash "^4.17.15"
nullthrows "^1.1.0" 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: expo-constants@^9.2.0, expo-constants@~9.2.0:
version "9.2.0" version "9.2.0"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-9.2.0.tgz#e86a38793deaff9018878afac65bce2543c80a4c" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-9.2.0.tgz#e86a38793deaff9018878afac65bce2543c80a4c"