From 33b0b6b8fffa973aa7581169d27ad7e859570ce4 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Mon, 21 Dec 2020 21:47:15 +0100 Subject: [PATCH] Add account actions --- src/components/Button/ButtonRow.tsx | 15 +- src/components/ParseContent.tsx | 2 +- .../Timelines/Timeline/Shared/Actions.tsx | 5 +- .../Timelines/Timeline/Shared/Avatar.tsx | 5 +- .../Timeline/Shared/HeaderDefault.tsx | 3 +- .../Shared/HeaderDefault/ActionsAccount.tsx | 20 ++- .../Timeline/Shared/HeaderNotification.tsx | 2 - src/screens/Shared/Account.tsx | 53 ++++++- src/screens/Shared/Account/Information.tsx | 149 ++++++++++++++++-- src/screens/Shared/Account/Toots.tsx | 24 +-- src/screens/Shared/Compose.tsx | 42 +++-- src/screens/Shared/Compose/Reply.tsx | 2 +- src/screens/Shared/Compose/Root.tsx | 2 +- src/screens/Shared/Compose/Root/Footer.tsx | 12 +- src/screens/Shared/Search.tsx | 4 +- 15 files changed, 257 insertions(+), 83 deletions(-) diff --git a/src/components/Button/ButtonRow.tsx b/src/components/Button/ButtonRow.tsx index 2b08ca29..5fc34925 100644 --- a/src/components/Button/ButtonRow.tsx +++ b/src/components/Button/ButtonRow.tsx @@ -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 } export interface PropsText extends PropsBase { @@ -27,7 +28,8 @@ const ButtonRow: React.FC = ({ buttonSize = 'M', text, icon, - size = 'M' + size = 'M', + style: customStyle }) => { const { theme } = useTheme() @@ -35,8 +37,15 @@ const ButtonRow: React.FC = ({ = ({ const styles = StyleSheet.create({ button: { - paddingLeft: StyleConstants.Spacing.M, - paddingRight: StyleConstants.Spacing.M, borderWidth: 1.25, borderRadius: 100, alignItems: 'center' diff --git a/src/components/ParseContent.tsx b/src/components/ParseContent.tsx index 159d1002..8b3fbe82 100644 --- a/src/components/ParseContent.tsx +++ b/src/components/ParseContent.tsx @@ -64,7 +64,7 @@ const renderNode = ({ m => m.username === username[1] ) navigation.push('Screen-Shared-Account', { - id: mentions[usernameIndex].id + account: mentions[usernameIndex] }) }} > diff --git a/src/components/Timelines/Timeline/Shared/Actions.tsx b/src/components/Timelines/Timeline/Shared/Actions.tsx index 6fed1270..541366eb 100644 --- a/src/components/Timelines/Timeline/Shared/Actions.tsx +++ b/src/components/Timelines/Timeline/Shared/Actions.tsx @@ -137,8 +137,9 @@ const TimelineActions: React.FC = ({ 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' } }) }, []) diff --git a/src/components/Timelines/Timeline/Shared/Avatar.tsx b/src/components/Timelines/Timeline/Shared/Avatar.tsx index 3775e742..39c53753 100644 --- a/src/components/Timelines/Timeline/Shared/Avatar.tsx +++ b/src/components/Timelines/Timeline/Shared/Avatar.tsx @@ -12,10 +12,7 @@ const TimelineAvatar: React.FC = ({ 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 ( diff --git a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx index 64e0bc64..d78d3b55 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx @@ -134,8 +134,7 @@ const TimelineHeaderDefault: React.FC = ({ {!sameAccount && ( )} diff --git a/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount.tsx b/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount.tsx index b7eee216..b0c85413 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount.tsx @@ -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> } const HeaderDefaultActionsAccount: React.FC = ({ 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 = ({ setBottomSheetVisible(false) mutate({ type: 'mute', - id: accountId, + id: account.id, stateKey: 'muting' }) }} iconFront='eye-off' - title={`隐藏 @${account} 的嘟嘟`} + title={`隐藏 @${account.acct} 的嘟嘟`} /> { setBottomSheetVisible(false) mutate({ type: 'block', - id: accountId, + id: account.id, stateKey: 'blocking' }) }} iconFront='x-circle' - title={`屏蔽用户 @${account}`} + title={`屏蔽用户 @${account.acct}`} /> { setBottomSheetVisible(false) mutate({ type: 'reports', - id: accountId + id: account.id }) }} iconFront='flag' - title={`举报 @${account}`} + title={`举报 @${account.acct}`} /> ) diff --git a/src/components/Timelines/Timeline/Shared/HeaderNotification.tsx b/src/components/Timelines/Timeline/Shared/HeaderNotification.tsx index bc7e5f1d..ae0edcc3 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderNotification.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderNotification.tsx @@ -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 = ({ 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( diff --git a/src/screens/Shared/Account.tsx b/src/screens/Shared/Account.tsx index 07fa0d80..b4154d5e 100644 --- a/src/screens/Shared/Account.tsx +++ b/src/screens/Shared/Account.tsx @@ -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 = ({ 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 = ({ AccountInitialState ) + const [modalVisible, setBottomSheetVisible] = useState(false) + useEffect(() => { + const updateHeaderRight = () => + navigation.setOptions({ + headerRight: () => ( + setBottomSheetVisible(true)} + /> + ) + }) + return updateHeaderRight() + }, []) + return ( <> = ({ + + setBottomSheetVisible(false)} + > + {/* 添加到列表 */} + {localAccountId !== account.id && ( + + )} + ) } diff --git a/src/screens/Shared/Account/Information.tsx b/src/screens/Shared/Account/Information.tsx index 784cbd33..0bd496cf 100644 --- a/src/screens/Shared/Account/Information.tsx +++ b/src/screens/Shared/Account/Information.tsx @@ -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 @@ -17,10 +52,42 @@ export interface Props { } const AccountInformation: React.FC = ({ 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() const shimmerNameRef = createRef() @@ -44,9 +111,35 @@ const AccountInformation: React.FC = ({ accountDispatch, account }) => { Animated.loop(informationAnimated).start() }, []) + const followingButton = useMemo( + () => ( + + mutate({ + type: 'follow', + id: account!.id, + stateKey: 'following', + prevState: data!.following + }) + } + disabled={ + status !== 'success' || + (mutateStatus !== 'success' && mutateStatus !== 'idle') + } + /> + ), + [data, status, mutateStatus] + ) + return ( accountDispatch && accountDispatch({ @@ -59,18 +152,36 @@ const AccountInformation: React.FC = ({ accountDispatch, account }) => { } > {/* Moved or not: {account.moved} */} - - setAvatarLoaded(true)} - /> - + + + setAvatarLoaded(true)} + /> + + + + navigation.navigate(getCurrentTab(navigation), { + screen: 'Screen-Shared-Compose', + params: { + type: 'conversation', + incomingStatus: { account } + } + }) + } + style={{ marginRight: StyleConstants.Spacing.S }} + /> + {followingButton} + + = ({ 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, diff --git a/src/screens/Shared/Account/Toots.tsx b/src/screens/Shared/Account/Toots.tsx index c6d73fbb..255bf08f 100644 --- a/src/screens/Shared/Account/Toots.tsx +++ b/src/screens/Shared/Account/Toots.tsx @@ -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 = ({ { key: 'Account_Media' } ] - const renderScene = ({ - route - }: { - route: { - key: App.Pages - } - }) => { - console.log(route) - return - } + const renderScene = useCallback( + ({ + route + }: { + route: { + key: App.Pages + } + }) => { + return + }, + [] + ) return ( { 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 = ({ 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 = ({ 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 = ({ route: { params }, navigation }) => { disableDebounce: true }) break + case 'conversation': + formatText({ + textInput: 'text', + composeDispatch, + content: `@${params.incomingStatus.account.acct} `, + disableDebounce: true + }) + break } }, [params?.type]) diff --git a/src/screens/Shared/Compose/Reply.tsx b/src/screens/Shared/Compose/Reply.tsx index 1b6afcb7..850e7f25 100644 --- a/src/screens/Shared/Compose/Reply.tsx +++ b/src/screens/Shared/Compose/Reply.tsx @@ -26,7 +26,7 @@ const ComposeReply: React.FC = () => { - + {replyToStatus!.content.length > 0 && ( )} diff --git a/src/screens/Shared/Compose/Root.tsx b/src/screens/Shared/Compose/Root.tsx index fe8faac6..90c317c3 100644 --- a/src/screens/Shared/Compose/Root.tsx +++ b/src/screens/Shared/Compose/Root.tsx @@ -194,7 +194,7 @@ const ComposeRoot: React.FC = () => { } - ListFooterComponent={} + ListFooterComponent={} ListEmptyComponent={listEmpty} data={data as Mastodon.Account[] & Mastodon.Tag[]} keyExtractor={listKey} diff --git a/src/screens/Shared/Compose/Root/Footer.tsx b/src/screens/Shared/Compose/Root/Footer.tsx index 24e6768f..641011fb 100644 --- a/src/screens/Shared/Compose/Root/Footer.tsx +++ b/src/screens/Shared/Compose/Root/Footer.tsx @@ -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 -} - -const ComposeRootFooter: React.FC = ({ textInputRef }) => { +const ComposeRootFooter: React.FC = () => { const { composeState } = useContext(ComposeContext) - + console.log(composeState) return ( <> {composeState.emoji.active && ( - + )} diff --git a/src/screens/Shared/Search.tsx b/src/screens/Shared/Search.tsx index ccc6d7b0..50cada3b 100644 --- a/src/screens/Shared/Search.tsx +++ b/src/screens/Shared/Search.tsx @@ -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 }) }} >