Add account actions

This commit is contained in:
Zhiyuan Zheng 2020-12-21 21:47:15 +01:00
parent 25a80cc57e
commit 33b0b6b8ff
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
15 changed files with 257 additions and 83 deletions

View File

@ -1,6 +1,6 @@
import { Feather } from '@expo/vector-icons'
import React from 'react'
import { Pressable, StyleSheet, Text } from 'react-native'
import { Pressable, StyleProp, StyleSheet, Text, ViewStyle } from 'react-native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -9,6 +9,7 @@ type PropsBase = {
disabled?: boolean
buttonSize?: 'S' | 'M'
size?: 'S' | 'M' | 'L'
style?: StyleProp<ViewStyle>
}
export interface PropsText extends PropsBase {
@ -27,7 +28,8 @@ const ButtonRow: React.FC<PropsText | PropsIcon> = ({
buttonSize = 'M',
text,
icon,
size = 'M'
size = 'M',
style: customStyle
}) => {
const { theme } = useTheme()
@ -35,8 +37,15 @@ const ButtonRow: React.FC<PropsText | PropsIcon> = ({
<Pressable
{...(!disabled && { onPress })}
style={[
customStyle,
styles.button,
{
paddingLeft:
StyleConstants.Spacing.M -
(icon ? StyleConstants.Font.Size[size] / 2 : 0),
paddingRight:
StyleConstants.Spacing.M -
(icon ? StyleConstants.Font.Size[size] / 2 : 0),
borderColor: disabled ? theme.secondary : theme.primary,
paddingTop: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS'],
paddingBottom: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS']
@ -68,8 +77,6 @@ const ButtonRow: React.FC<PropsText | PropsIcon> = ({
const styles = StyleSheet.create({
button: {
paddingLeft: StyleConstants.Spacing.M,
paddingRight: StyleConstants.Spacing.M,
borderWidth: 1.25,
borderRadius: 100,
alignItems: 'center'

View File

@ -64,7 +64,7 @@ const renderNode = ({
m => m.username === username[1]
)
navigation.push('Screen-Shared-Account', {
id: mentions[usernameIndex].id
account: mentions[usernameIndex]
})
}}
>

View File

@ -137,8 +137,9 @@ const TimelineActions: React.FC<Props> = ({
navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose',
params: {
type: status.visibility === 'direct' ? 'conversation' : 'reply',
incomingStatus: status
type: 'reply',
incomingStatus: status,
visibilityLock: status.visibility === 'direct'
}
})
}, [])

View File

@ -12,10 +12,7 @@ const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => {
const navigation = useNavigation()
// Need to fix go back root
const onPress = useCallback(() => {
queryKey &&
navigation.push('Screen-Shared-Account', {
id: account.id
})
queryKey && navigation.push('Screen-Shared-Account', { account })
}, [])
return (

View File

@ -134,8 +134,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({
{!sameAccount && (
<HeaderDefaultActionsAccount
queryKey={queryKey}
accountId={status.account.id}
account={status.account.acct}
account={status.account}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}

View File

@ -56,22 +56,20 @@ const fireMutation = async ({
}
export interface Props {
queryKey: QueryKey.Timeline
accountId: string
account: string
queryKey?: QueryKey.Timeline
account: Mastodon.Account
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
}
const HeaderDefaultActionsAccount: React.FC<Props> = ({
queryKey,
accountId,
account,
setBottomSheetVisible
}) => {
const queryClient = useQueryClient()
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
queryKey && queryClient.invalidateQueries(queryKey)
}
})
@ -83,35 +81,35 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
setBottomSheetVisible(false)
mutate({
type: 'mute',
id: accountId,
id: account.id,
stateKey: 'muting'
})
}}
iconFront='eye-off'
title={`隐藏 @${account} 的嘟嘟`}
title={`隐藏 @${account.acct} 的嘟嘟`}
/>
<MenuRow
onPress={() => {
setBottomSheetVisible(false)
mutate({
type: 'block',
id: accountId,
id: account.id,
stateKey: 'blocking'
})
}}
iconFront='x-circle'
title={`屏蔽用户 @${account}`}
title={`屏蔽用户 @${account.acct}`}
/>
<MenuRow
onPress={() => {
setBottomSheetVisible(false)
mutate({
type: 'reports',
id: accountId
id: account.id
})
}}
iconFront='flag'
title={`举报 @${account}`}
title={`举报 @${account.acct}`}
/>
</MenuContainer>
)

View File

@ -6,7 +6,6 @@ import {
Text,
View
} from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons'
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
import relativeTime from '@utils/relativeTime'
@ -29,7 +28,6 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const account = notification.account.acct
const { theme } = useTheme()
const navigation = useNavigation()
const [since, setSince] = useState(relativeTime(notification.created_at))
const { status, data, refetch } = useQuery(

View File

@ -1,8 +1,6 @@
import React, { useReducer, useRef } from 'react'
import React, { useEffect, useReducer, useRef, useState } from 'react'
import { Animated, ScrollView } from 'react-native'
// import * as relationshipsSlice from 'src/stacks/common/relationshipsSlice'
import { useQuery } from 'react-query'
import { accountFetch } from '@utils/fetches/accountFetch'
import AccountToots from '@screens/Shared/Account/Toots'
@ -10,15 +8,24 @@ import AccountHeader from '@screens/Shared/Account/Header'
import AccountInformation from '@screens/Shared/Account/Information'
import AccountNav from './Account/Nav'
import AccountSegmentedControl from './Account/SegmentedControl'
import { HeaderRight } from '@root/components/Header'
import BottomSheet from '@root/components/BottomSheet'
import { useSelector } from 'react-redux'
import {
getLocalAccountId,
getLocalUrl
} from '@root/utils/slices/instancesSlice'
import HeaderDefaultActionsAccount from '@root/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
// Moved account example: https://m.cmx.im/web/accounts/27812
export interface Props {
route: {
params: {
id: string
account: Mastodon.Account
}
}
navigation: any
}
export type AccountState = {
@ -65,10 +72,13 @@ const accountReducer = (
const ScreenSharedAccount: React.FC<Props> = ({
route: {
params: { id }
}
params: { account }
},
navigation
}) => {
const { data } = useQuery(['Account', { id }], accountFetch)
const localAccountId = useSelector(getLocalAccountId)
const localDomain = useSelector(getLocalUrl)
const { data } = useQuery(['Account', { id: account.id }], accountFetch)
// const stateRelationships = useSelector(relationshipsState)
const scrollY = useRef(new Animated.Value(0)).current
@ -77,6 +87,20 @@ const ScreenSharedAccount: React.FC<Props> = ({
AccountInitialState
)
const [modalVisible, setBottomSheetVisible] = useState(false)
useEffect(() => {
const updateHeaderRight = () =>
navigation.setOptions({
headerRight: () => (
<HeaderRight
icon='more-horizontal'
onPress={() => setBottomSheetVisible(true)}
/>
)
})
return updateHeaderRight()
}, [])
return (
<>
<AccountNav
@ -106,9 +130,22 @@ const ScreenSharedAccount: React.FC<Props> = ({
<AccountToots
accountState={accountState}
accountDispatch={accountDispatch}
id={id}
id={account.id}
/>
</ScrollView>
<BottomSheet
visible={modalVisible}
handleDismiss={() => setBottomSheetVisible(false)}
>
{/* 添加到列表 */}
{localAccountId !== account.id && (
<HeaderDefaultActionsAccount
account={account}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}
</BottomSheet>
</>
)
}

View File

@ -1,4 +1,4 @@
import React, { createRef, Dispatch, useEffect, useState } from 'react'
import React, { createRef, Dispatch, useEffect, useMemo, useState } from 'react'
import { Animated, Image, StyleSheet, Text, View } from 'react-native'
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'
import { Feather } from '@expo/vector-icons'
@ -10,6 +10,41 @@ import { useTranslation } from 'react-i18next'
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
import { LinearGradient } from 'expo-linear-gradient'
import { AccountAction } from '../Account'
import { ButtonRow } from '@root/components/Button'
import { useMutation, useQuery, useQueryClient } from 'react-query'
import { relationshipFetch } from '@root/utils/fetches/relationshipFetch'
import client from '@root/api/client'
import { useNavigation } from '@react-navigation/native'
import getCurrentTab from '@root/utils/getCurrentTab'
const fireMutation = async ({
type,
id,
stateKey,
prevState
}: {
type: 'follow'
id: string
stateKey: 'following'
prevState: boolean
}) => {
let res
switch (type) {
case 'follow':
res = await client({
method: 'post',
instance: 'local',
url: `accounts/${id}/${prevState ? 'un' : ''}${type}`
})
if (res.body[stateKey] === !prevState) {
return Promise.resolve()
} else {
return Promise.reject()
}
break
}
}
export interface Props {
accountDispatch?: Dispatch<AccountAction>
@ -17,10 +52,42 @@ export interface Props {
}
const AccountInformation: React.FC<Props> = ({ accountDispatch, account }) => {
const navigation = useNavigation()
const { t } = useTranslation('sharedAccount')
const { theme } = useTheme()
const [avatarLoaded, setAvatarLoaded] = useState(false)
const relationshipQueryKey = ['Relationship', { id: account?.id }]
const { status, data, refetch } = useQuery(
relationshipQueryKey,
relationshipFetch,
{
enabled: false
}
)
useEffect(() => {
if (account?.id) {
refetch()
}
}, [account])
const queryClient = useQueryClient()
const { mutate, status: mutateStatus } = useMutation(fireMutation, {
onMutate: () => {
queryClient.cancelQueries(relationshipQueryKey)
const oldData = queryClient.getQueryData(relationshipQueryKey)
queryClient.setQueryData(relationshipQueryKey, (old: any) => {
old && (old.following = !old?.following)
return old
})
return oldData
},
onError: (err, _, oldData) => {
queryClient.setQueryData(relationshipQueryKey, oldData)
}
})
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
const shimmerAvatarRef = createRef<any>()
const shimmerNameRef = createRef<any>()
@ -44,9 +111,35 @@ const AccountInformation: React.FC<Props> = ({ accountDispatch, account }) => {
Animated.loop(informationAnimated).start()
}, [])
const followingButton = useMemo(
() => (
<ButtonRow
{...(data
? status !== 'success' ||
(mutateStatus !== 'success' && mutateStatus !== 'idle')
? { icon: 'loader' }
: { text: `${data.following ? '正在' : ''}关注` }
: { icon: 'loader' })}
onPress={() =>
mutate({
type: 'follow',
id: account!.id,
stateKey: 'following',
prevState: data!.following
})
}
disabled={
status !== 'success' ||
(mutateStatus !== 'success' && mutateStatus !== 'idle')
}
/>
),
[data, status, mutateStatus]
)
return (
<View
style={styles.information}
style={styles.base}
onLayout={({ nativeEvent }) =>
accountDispatch &&
accountDispatch({
@ -59,18 +152,36 @@ const AccountInformation: React.FC<Props> = ({ accountDispatch, account }) => {
}
>
{/* <Text>Moved or not: {account.moved}</Text> */}
<ShimmerPlaceholder
ref={shimmerAvatarRef}
visible={avatarLoaded}
width={StyleConstants.Avatar.L}
height={StyleConstants.Avatar.L}
>
<Image
source={{ uri: account?.avatar }}
style={styles.avatar}
onLoadEnd={() => setAvatarLoaded(true)}
/>
</ShimmerPlaceholder>
<View style={styles.avatarAndActions}>
<ShimmerPlaceholder
ref={shimmerAvatarRef}
visible={avatarLoaded}
width={StyleConstants.Avatar.L}
height={StyleConstants.Avatar.L}
>
<Image
source={{ uri: account?.avatar }}
style={styles.avatar}
onLoadEnd={() => setAvatarLoaded(true)}
/>
</ShimmerPlaceholder>
<View style={styles.actions}>
<ButtonRow
icon='mail'
onPress={() =>
navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose',
params: {
type: 'conversation',
incomingStatus: { account }
}
})
}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
{followingButton}
</View>
</View>
<ShimmerPlaceholder
ref={shimmerNameRef}
@ -261,15 +372,23 @@ const AccountInformation: React.FC<Props> = ({ accountDispatch, account }) => {
}
const styles = StyleSheet.create({
information: {
base: {
marginTop: -StyleConstants.Spacing.Global.PagePadding * 3,
padding: StyleConstants.Spacing.Global.PagePadding
},
avatarAndActions: {
flexDirection: 'row',
justifyContent: 'space-between'
},
avatar: {
width: StyleConstants.Avatar.L,
height: StyleConstants.Avatar.L,
borderRadius: 8
},
actions: {
alignSelf: 'flex-end',
flexDirection: 'row'
},
display_name: {
flexDirection: 'row',
marginTop: StyleConstants.Spacing.M,

View File

@ -1,4 +1,4 @@
import React, { Dispatch } from 'react'
import React, { Dispatch, useCallback } from 'react'
import { Dimensions, StyleSheet } from 'react-native'
import { TabView } from 'react-native-tab-view'
@ -23,16 +23,18 @@ const AccountToots: React.FC<Props> = ({
{ key: 'Account_Media' }
]
const renderScene = ({
route
}: {
route: {
key: App.Pages
}
}) => {
console.log(route)
return <Timeline page={route.key} account={id} disableRefresh />
}
const renderScene = useCallback(
({
route
}: {
route: {
key: App.Pages
}
}) => {
return <Timeline page={route.key} account={id} disableRefresh />
},
[]
)
return (
<TabView

View File

@ -173,14 +173,15 @@ const composeInitialState: ComposeState = {
}
const composeExistingState = ({
type,
incomingStatus
incomingStatus,
visibilityLock
}: {
type: 'reply' | 'conversation' | 'edit'
incomingStatus: Mastodon.Status
visibilityLock?: boolean
}): ComposeState => {
switch (type) {
case 'edit':
console.log(incomingStatus)
return {
...composeInitialState,
...(incomingStatus.spoiler_text?.length && {
@ -226,12 +227,12 @@ const composeExistingState = ({
...(incomingStatus.visibility === 'direct' && { visibilityLock: true })
}
case 'reply':
case 'conversation':
const actualStatus = incomingStatus.reblog || incomingStatus
const allMentions = actualStatus.mentions.map(
mention => `@${mention.acct}`
)
const allMentions = Array.isArray(actualStatus.mentions)
? actualStatus.mentions.map(mention => `@${mention.acct}`)
: []
let replyPlaceholder = allMentions.join(' ')
if (replyPlaceholder.length === 0) {
replyPlaceholder = `@${actualStatus.account.acct} `
} else {
@ -245,11 +246,23 @@ const composeExistingState = ({
formatted: undefined,
selection: { start: 0, end: 0 }
},
...(type === 'conversation' && {
...(visibilityLock && {
visibility: 'direct',
visibilityLock: true
}),
replyToStatus: incomingStatus.reblog || incomingStatus
replyToStatus: actualStatus
}
case 'conversation':
return {
...composeInitialState,
text: {
count: incomingStatus.account.acct.length + 2,
raw: `@${incomingStatus.account.acct} `,
formatted: undefined,
selection: { start: 0, end: 0 }
},
visibility: 'direct',
visibilityLock: true
}
}
}
@ -309,6 +322,7 @@ export interface Props {
| {
type?: 'reply' | 'conversation' | 'edit'
incomingStatus: Mastodon.Status
visibilityLock?: boolean
}
| undefined
}
@ -342,7 +356,8 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
params?.type && params?.incomingStatus
? composeExistingState({
type: params.type,
incomingStatus: params.incomingStatus
incomingStatus: params.incomingStatus,
visibilityLock: params.visibilityLock
})
: {
...composeInitialState,
@ -372,7 +387,6 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
})
break
case 'reply':
case 'conversation':
const actualStatus =
params.incomingStatus.reblog || params.incomingStatus
const allMentions = actualStatus.mentions.map(
@ -391,6 +405,14 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
disableDebounce: true
})
break
case 'conversation':
formatText({
textInput: 'text',
composeDispatch,
content: `@${params.incomingStatus.account.acct} `,
disableDebounce: true
})
break
}
}, [params?.type])

View File

@ -26,7 +26,7 @@ const ComposeReply: React.FC = () => {
<View style={[styles.status, { borderTopColor: theme.border }]}>
<TimelineAvatar account={replyToStatus!.account} />
<View style={styles.details}>
<TimelineHeaderDefault status={replyToStatus!} />
<TimelineHeaderDefault status={replyToStatus!} sameAccount={false} />
{replyToStatus!.content.length > 0 && (
<TimelineContent status={replyToStatus!} />
)}

View File

@ -194,7 +194,7 @@ const ComposeRoot: React.FC = () => {
<FlatList
keyboardShouldPersistTaps='handled'
ListHeaderComponent={<ComposeRootHeader />}
ListFooterComponent={<ComposeRootFooter textInputRef={textInputRef} />}
ListFooterComponent={<ComposeRootFooter />}
ListEmptyComponent={listEmpty}
data={data as Mastodon.Account[] & Mastodon.Tag[]}
keyExtractor={listKey}

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react'
import { StyleSheet, TextInput, View } from 'react-native'
import { StyleSheet, View } from 'react-native'
import { StyleConstants } from '@utils/styles/constants'
import { ComposeContext } from '@screens/Shared/Compose'
import ComposeAttachments from '@screens/Shared/Compose/Attachments'
@ -7,18 +7,14 @@ import ComposeEmojis from '@screens/Shared/Compose/Emojis'
import ComposePoll from '@screens/Shared/Compose/Poll'
import ComposeReply from '@screens/Shared/Compose/Reply'
export interface Props {
textInputRef: React.RefObject<TextInput>
}
const ComposeRootFooter: React.FC<Props> = ({ textInputRef }) => {
const ComposeRootFooter: React.FC = () => {
const { composeState } = useContext(ComposeContext)
console.log(composeState)
return (
<>
{composeState.emoji.active && (
<View style={styles.emojis}>
<ComposeEmojis textInputRef={textInputRef} />
<ComposeEmojis />
</View>
)}

View File

@ -157,9 +157,7 @@ const ScreenSharedSearch: React.FC = () => {
]}
onPress={() => {
navigation.goBack()
navigation.push('Screen-Shared-Account', {
id: item.id
})
navigation.push('Screen-Shared-Account', { account: item })
}}
>
<Image