mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Updates
This commit is contained in:
		| @@ -19,7 +19,8 @@ export default (): ExpoConfig => ({ | ||||
|   scheme: 'tooot', | ||||
|   assetBundlePatterns: ['assets/*'], | ||||
|   extra: { | ||||
|     sentryDSN: process.env.SENTRY_DSN | ||||
|     sentryDSN: process.env.SENTRY_DSN, | ||||
|     sentryEnv: process.env.SENTRY_DEPLOY_ENV | ||||
|   }, | ||||
|   hooks: { | ||||
|     postPublish: [ | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|   "pages": [ | ||||
|     [ | ||||
|       { | ||||
|         "id": "1", | ||||
|         "id": "999", | ||||
|         "created_at": "2021-01-22T03:48:33.901Z", | ||||
|         "sensitive": false, | ||||
|         "visibility": "public", | ||||
| @@ -21,17 +21,38 @@ | ||||
|           "website": "https://tooot.app" | ||||
|         }, | ||||
|         "account": { | ||||
|           "id": "1", | ||||
|           "id": "999", | ||||
|           "username": "tooot📱", | ||||
|           "acct": "tooot@xmflsct.com", | ||||
|           "display_name": "tooot📱", | ||||
|           "avatar_static": "https://avatars.githubusercontent.com/u/77554750?s=200&v=4" | ||||
|         }, | ||||
|         "media_attachments": [], | ||||
|         "poll": null | ||||
|         "poll": { | ||||
|           "id": "1", | ||||
|           "expires_at": "2021-02-22T03:48:33.901Z", | ||||
|           "expired": false, | ||||
|           "multiple": false, | ||||
|           "votes_count": 10, | ||||
|           "voters_count": null, | ||||
|           "voted": false, | ||||
|           "own_votes": null, | ||||
|           "options": [ | ||||
|             { | ||||
|               "title": "I would love to!", | ||||
|               "votes_count": 6 | ||||
|             }, | ||||
|             { | ||||
|               "title": "Why not give it a go?", | ||||
|               "votes_count": 4 | ||||
|             } | ||||
|           ], | ||||
|           "emojis": [] | ||||
|         }, | ||||
|         "mentions": [] | ||||
|       }, | ||||
|       { | ||||
|         "id": "2", | ||||
|         "id": "1000", | ||||
|         "created_at": "2021-01-22T03:48:33.901Z", | ||||
|         "sensitive": false, | ||||
|         "spoiler_text": "", | ||||
| @@ -50,7 +71,7 @@ | ||||
|           "website": null | ||||
|         }, | ||||
|         "account": { | ||||
|           "id": "2", | ||||
|           "id": "1000", | ||||
|           "username": "Mastodon", | ||||
|           "acct": "mastodon", | ||||
|           "display_name": "Mastodon", | ||||
| @@ -63,10 +84,11 @@ | ||||
|           "description": "Mastodon is an open source decentralized social network - by the people for the people. Join the federation and take back control of your social media!", | ||||
|           "type": "link", | ||||
|           "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Mastodon_Logotype_%28Simple%29.svg/1200px-Mastodon_Logotype_%28Simple%29.svg.png" | ||||
|         } | ||||
|         }, | ||||
|         "mentions": [] | ||||
|       }, | ||||
|       { | ||||
|         "id": "3", | ||||
|         "id": "1001", | ||||
|         "created_at": "2021-01-22T03:48:33.901Z", | ||||
|         "spoiler_text": "", | ||||
|         "visibility": "public", | ||||
| @@ -84,13 +106,70 @@ | ||||
|           "website": null | ||||
|         }, | ||||
|         "account": { | ||||
|           "id": "3", | ||||
|           "id": "1001", | ||||
|           "username": "Fediverse", | ||||
|           "acct": "fediverse", | ||||
|           "display_name": "Fediverse", | ||||
|           "avatar_static": "https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png" | ||||
|         }, | ||||
|         "media_attachments": [] | ||||
|         "media_attachments": [], | ||||
|         "mentions": [] | ||||
|       }, | ||||
|       { | ||||
|         "id": "1002", | ||||
|         "created_at": "2021-01-22T03:48:33.901Z", | ||||
|         "sensitive": false, | ||||
|         "visibility": "public", | ||||
|         "replies_count": 0, | ||||
|         "reblogs_count": 0, | ||||
|         "favourites_count": 0, | ||||
|         "favourited": true, | ||||
|         "reblogged": false, | ||||
|         "muted": false, | ||||
|         "bookmarked": false, | ||||
|         "content": "<p>tooot is an open source, simple mobile client for Mastodon. Focusing on your connections while being able to explore the Fediverse.</p>", | ||||
|         "reblog": null, | ||||
|         "application": { | ||||
|           "name": "tooot", | ||||
|           "website": "https://tooot.app" | ||||
|         }, | ||||
|         "account": { | ||||
|           "id": "1002", | ||||
|           "username": "tooot📱", | ||||
|           "acct": "tooot@xmflsct.com", | ||||
|           "display_name": "tooot📱", | ||||
|           "avatar_static": "https://avatars.githubusercontent.com/u/77554750?s=200&v=4" | ||||
|         }, | ||||
|         "media_attachments": [], | ||||
|         "mentions": [] | ||||
|       }, | ||||
|       { | ||||
|         "id": "1003", | ||||
|         "created_at": "2021-01-22T03:48:33.901Z", | ||||
|         "sensitive": false, | ||||
|         "visibility": "public", | ||||
|         "replies_count": 0, | ||||
|         "reblogs_count": 0, | ||||
|         "favourites_count": 0, | ||||
|         "favourited": true, | ||||
|         "reblogged": false, | ||||
|         "muted": false, | ||||
|         "bookmarked": false, | ||||
|         "content": "<p>- tooot supports multiple accounts<br />- tooot supports browsing external instance<br />- tooot aims to support multiple languages</p>", | ||||
|         "reblog": null, | ||||
|         "application": { | ||||
|           "name": "tooot", | ||||
|           "website": "https://tooot.app" | ||||
|         }, | ||||
|         "account": { | ||||
|           "id": "1003", | ||||
|           "username": "tooot📱", | ||||
|           "acct": "tooot@xmflsct.com", | ||||
|           "display_name": "tooot📱", | ||||
|           "avatar_static": "https://avatars.githubusercontent.com/u/77554750?s=200&v=4" | ||||
|         }, | ||||
|         "media_attachments": [], | ||||
|         "mentions": [] | ||||
|       } | ||||
|     ] | ||||
|   ] | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -9,13 +9,13 @@ declare namespace Nav { | ||||
|  | ||||
|   type SharedStackParamList = { | ||||
|     'Screen-Shared-Account': { | ||||
|       account: Pick<Mastodon.Account, 'id' | 'username' | 'acct' | 'url'> | ||||
|       account: Mastodon.Account | Mastodon.Mention | ||||
|     } | ||||
|     'Screen-Shared-Announcements': { showAll?: boolean } | ||||
|     'Screen-Shared-Attachments': { account: Mastodon.Account } | ||||
|     'Screen-Shared-Compose': | ||||
|       | { | ||||
|           type: 'reply' | 'conversation' | 'edit' | ||||
|           type: 'edit' | ||||
|           incomingStatus: Mastodon.Status | ||||
|           queryKey?: [ | ||||
|             'Timeline', | ||||
| @@ -28,6 +28,25 @@ declare namespace Nav { | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       | { | ||||
|           type: 'reply' | ||||
|           incomingStatus: Mastodon.Status | ||||
|           accts: Mastodon.Account['acct'][] | ||||
|           queryKey?: [ | ||||
|             'Timeline', | ||||
|             { | ||||
|               page: App.Pages | ||||
|               hashtag?: Mastodon.Tag['name'] | ||||
|               list?: Mastodon.List['id'] | ||||
|               toot?: Mastodon.Status['id'] | ||||
|               account?: Mastodon.Account['id'] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       | { | ||||
|           type: 'conversation' | ||||
|           accts: Mastodon.Account['acct'][] | ||||
|         } | ||||
|       | undefined | ||||
|     'Screen-Shared-Hashtag': { | ||||
|       hashtag: Mastodon.Tag['name'] | ||||
|   | ||||
| @@ -33,6 +33,7 @@ import React, { | ||||
|   useMemo, | ||||
|   useRef | ||||
| } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Image, Platform, StatusBar } from 'react-native' | ||||
| import Toast from 'react-native-toast-message' | ||||
| import { useDispatch, useSelector } from 'react-redux' | ||||
| @@ -70,11 +71,12 @@ const Index: React.FC<Props> = ({ localCorrupt }) => { | ||||
|   // }, [isConnected, firstRender]) | ||||
|  | ||||
|   // On launch display login credentials corrupt information | ||||
|   const { t } = useTranslation('common') | ||||
|   useEffect(() => { | ||||
|     const showLocalCorrect = localCorrupt | ||||
|       ? toast({ | ||||
|           type: 'error', | ||||
|           message: '登录已过期', | ||||
|           message: t('index.localCorrupt'), | ||||
|           description: localCorrupt.length ? localCorrupt : undefined, | ||||
|           autoHide: false | ||||
|         }) | ||||
| @@ -193,8 +195,8 @@ const Index: React.FC<Props> = ({ localCorrupt }) => { | ||||
|               <Image | ||||
|                 source={{ uri: localAccount?.avatarStatic }} | ||||
|                 style={{ | ||||
|                   width: size + 2, | ||||
|                   height: size + 2, | ||||
|                   width: size, | ||||
|                   height: size, | ||||
|                   borderRadius: size, | ||||
|                   borderWidth: focused ? 2 : 0, | ||||
|                   borderColor: focused ? theme.secondary : color | ||||
|   | ||||
| @@ -1,22 +1,38 @@ | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import React, { useCallback } from 'react' | ||||
| import { Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import analytics from './analytics' | ||||
| import GracefullyImage from './GracefullyImage' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | ||||
|   onPress: () => void | ||||
|   onPress?: () => void | ||||
|   origin?: string | ||||
| } | ||||
|  | ||||
| const ComponentAccount: React.FC<Props> = ({ account, onPress }) => { | ||||
| const ComponentAccount: React.FC<Props> = ({ | ||||
|   account, | ||||
|   onPress: customOnPress, | ||||
|   origin | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('search_account_press', { page: origin }) | ||||
|     navigation.push('Screen-Shared-Account', { account }) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
|       style={[styles.itemDefault, styles.itemAccount]} | ||||
|       onPress={onPress} | ||||
|       onPress={customOnPress || onPress} | ||||
|     > | ||||
|       <GracefullyImage | ||||
|         uri={{ original: account.avatar_static }} | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import Animated, { | ||||
|   useSharedValue, | ||||
|   withTiming | ||||
| } from 'react-native-reanimated' | ||||
| import analytics from './analytics' | ||||
|  | ||||
| export interface Props { | ||||
|   children: React.ReactNode | ||||
| @@ -42,6 +43,7 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => { | ||||
|     } | ||||
|   }) | ||||
|   const callDismiss = () => { | ||||
|     analytics('bottomsheet_swipe_close') | ||||
|     handleDismiss() | ||||
|   } | ||||
|   const onGestureEvent = useAnimatedGestureHandler({ | ||||
| @@ -90,7 +92,10 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => { | ||||
|               <Button | ||||
|                 type='text' | ||||
|                 content='取消' | ||||
|                 onPress={() => handleDismiss()} | ||||
|                 onPress={() => { | ||||
|                   analytics('bottomsheet_cancel') | ||||
|                   handleDismiss() | ||||
|                 }} | ||||
|                 style={styles.button} | ||||
|               /> | ||||
|             </Animated.View> | ||||
|   | ||||
| @@ -1,23 +1,39 @@ | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import React, { useCallback } from 'react' | ||||
| import { Pressable, StyleSheet, Text } from 'react-native' | ||||
| import analytics from './analytics' | ||||
|  | ||||
| export interface Props { | ||||
|   tag: Mastodon.Tag | ||||
|   onPress: () => void | ||||
|   hashtag: Mastodon.Tag | ||||
|   onPress?: () => void | ||||
|   origin?: string | ||||
| } | ||||
|  | ||||
| const ComponentHashtag: React.FC<Props> = ({ tag, onPress }) => { | ||||
| const ComponentHashtag: React.FC<Props> = ({ | ||||
|   hashtag, | ||||
|   onPress: customOnPress, | ||||
|   origin | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('search_account_press', { page: origin }) | ||||
|     navigation.push('Screen-Shared-Hashtag', { hashtag: hashtag.name }) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
|       style={[styles.itemDefault, { borderBottomColor: theme.border }]} | ||||
|       onPress={onPress} | ||||
|       onPress={customOnPress || onPress} | ||||
|     > | ||||
|       <Text style={[styles.itemHashtag, { color: theme.primary }]}> | ||||
|         #{tag.name} | ||||
|         #{hashtag.name} | ||||
|       </Text> | ||||
|     </Pressable> | ||||
|   ) | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import { Alert, Image, StyleSheet, Text, TextInput, View } from 'react-native' | ||||
| import { useQueryClient } from 'react-query' | ||||
| import { useDispatch, useSelector } from 'react-redux' | ||||
| import { Placeholder, Fade } from 'rn-placeholder' | ||||
| import analytics from './analytics' | ||||
| import InstanceAuth from './Instance/Auth' | ||||
| import InstanceInfo from './Instance/Info' | ||||
| import { toast } from './toast' | ||||
| @@ -70,6 +71,7 @@ const ComponentInstance: React.FC<Props> = ({ | ||||
|     if (instanceDomain) { | ||||
|       switch (type) { | ||||
|         case 'local': | ||||
|           analytics('instance_local_login') | ||||
|           if ( | ||||
|             localInstances && | ||||
|             localInstances.filter(instance => instance.url === instanceDomain) | ||||
| @@ -96,6 +98,7 @@ const ComponentInstance: React.FC<Props> = ({ | ||||
|           } | ||||
|           break | ||||
|         case 'remote': | ||||
|           analytics('instance_remote_register') | ||||
|           haptics('Success') | ||||
|           const queryKey: QueryKeyTimeline = [ | ||||
|             'Timeline', | ||||
| @@ -112,6 +115,7 @@ const ComponentInstance: React.FC<Props> = ({ | ||||
|  | ||||
|   const onSubmitEditing = useCallback( | ||||
|     ({ nativeEvent: { text } }) => { | ||||
|       analytics('instance_textinput_submit', { match: text === instanceDomain }) | ||||
|       if ( | ||||
|         text === instanceDomain && | ||||
|         instanceQuery.isSuccess && | ||||
| @@ -276,7 +280,10 @@ const ComponentInstance: React.FC<Props> = ({ | ||||
|                 {t('server.disclaimer')} | ||||
|                 <Text | ||||
|                   style={{ color: theme.blue }} | ||||
|                   onPress={() => Linking.openURL('https://tooot.app/privacy')} | ||||
|                   onPress={() => { | ||||
|                     analytics('view_privacy') | ||||
|                     Linking.openURL('https://tooot.app/privacy') | ||||
|                   }} | ||||
|                 > | ||||
|                   https://tooot.app/privacy | ||||
|                 </Text> | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Icon from '@components/Icon' | ||||
| import openLink from '@components/openLink' | ||||
| import ParseEmojis from '@components/Parse/Emojis' | ||||
| import { useNavigation, useRoute } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| @@ -29,7 +31,7 @@ const renderNode = ({ | ||||
|   node: any | ||||
|   index: number | ||||
|   size: 'M' | 'L' | ||||
|   navigation: any | ||||
|   navigation: StackNavigationProp<Nav.LocalStackParamList> | ||||
|   mentions?: Mastodon.Mention[] | ||||
|   tags?: Mastodon.Tag[] | ||||
|   showFullLink: boolean | ||||
| @@ -53,6 +55,7 @@ const renderNode = ({ | ||||
|                 ...StyleConstants.FontStyle[size] | ||||
|               }} | ||||
|               onPress={() => { | ||||
|                 analytics('status_hashtag_press') | ||||
|                 !disableDetails && | ||||
|                   differentTag && | ||||
|                   navigation.push('Screen-Shared-Hashtag', { | ||||
| @@ -79,6 +82,7 @@ const renderNode = ({ | ||||
|                 ...StyleConstants.FontStyle[size] | ||||
|               }} | ||||
|               onPress={() => { | ||||
|                 analytics('status_mention_press') | ||||
|                 accountIndex !== -1 && | ||||
|                   !disableDetails && | ||||
|                   differentAccount && | ||||
| @@ -107,13 +111,14 @@ const renderNode = ({ | ||||
|               ...StyleConstants.FontStyle[size], | ||||
|               alignItems: 'center' | ||||
|             }} | ||||
|             onPress={async () => | ||||
|             onPress={async () => { | ||||
|               analytics('status_link_press') | ||||
|               !disableDetails && !shouldBeTag | ||||
|                 ? await openLink(href) | ||||
|                 : navigation.push('Screen-Shared-Hashtag', { | ||||
|                     hashtag: content.substring(1) | ||||
|                   }) | ||||
|             } | ||||
|             }} | ||||
|           > | ||||
|             {content || (showFullLink ? href : domain[1])} | ||||
|             {!shouldBeTag ? ( | ||||
| @@ -161,7 +166,9 @@ const ParseHTML: React.FC<Props> = ({ | ||||
|   expandHint, | ||||
|   disableDetails = false | ||||
| }) => { | ||||
|   const navigation = useNavigation() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   const route = useRoute() | ||||
|   const { theme } = useTheme() | ||||
|   const { t, i18n } = useTranslation('componentParse') | ||||
| @@ -229,6 +236,7 @@ const ParseHTML: React.FC<Props> = ({ | ||||
|           {expandAllow ? ( | ||||
|             <Pressable | ||||
|               onPress={() => { | ||||
|                 analytics('status_readmore', { allow: expandAllow, expanded }) | ||||
|                 layoutAnimation() | ||||
|                 setExpanded(!expanded) | ||||
|               }} | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import { toast } from '@components/toast' | ||||
| @@ -58,26 +59,28 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => { | ||||
|         type='icon' | ||||
|         content='X' | ||||
|         loading={mutation.isLoading} | ||||
|         onPress={() => | ||||
|         onPress={() => { | ||||
|           analytics('relationship_incoming_press_reject') | ||||
|           mutation.mutate({ | ||||
|             id, | ||||
|             type: 'incoming', | ||||
|             payload: { action: 'reject' } | ||||
|           }) | ||||
|         } | ||||
|         }} | ||||
|       /> | ||||
|       <Button | ||||
|         round | ||||
|         type='icon' | ||||
|         content='Check' | ||||
|         loading={mutation.isLoading} | ||||
|         onPress={() => | ||||
|         onPress={() => { | ||||
|           analytics('relationship_incoming_press_authorize') | ||||
|           mutation.mutate({ | ||||
|             id, | ||||
|             type: 'incoming', | ||||
|             payload: { action: 'authorize' } | ||||
|           }) | ||||
|         } | ||||
|         }} | ||||
|         style={styles.approve} | ||||
|       /> | ||||
|     </View> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import { toast } from '@components/toast' | ||||
| @@ -40,7 +41,7 @@ const RelationshipOutgoing = React.memo( | ||||
|         toast({ | ||||
|           type: 'error', | ||||
|           message: t('common:toastMessage.error.message', { | ||||
|             function: t(`button.${action}.function`) | ||||
|             function: t(`${action}.function`) | ||||
|           }), | ||||
|           ...(err.status && | ||||
|             typeof err.status === 'number' && | ||||
| @@ -61,12 +62,17 @@ const RelationshipOutgoing = React.memo( | ||||
|       onPress = () => {} | ||||
|     } else { | ||||
|       if (query.data?.blocked_by) { | ||||
|         analytics('relationship_outgoing_blocked_by') | ||||
|         content = t('button.blocked_by') | ||||
|         onPress = () => null | ||||
|         onPress = () => { | ||||
|           analytics('relationship_outgoing_blocked_by_press') | ||||
|         } | ||||
|       } else { | ||||
|         if (query.data?.blocking) { | ||||
|           analytics('relationship_outgoing_blocking') | ||||
|           content = t('button.blocking') | ||||
|           onPress = () => | ||||
|           onPress = () => { | ||||
|             analytics('relationship_outgoing_blocking_press') | ||||
|             mutation.mutate({ | ||||
|               id, | ||||
|               type: 'outgoing', | ||||
| @@ -75,10 +81,13 @@ const RelationshipOutgoing = React.memo( | ||||
|                 state: query.data?.blocking | ||||
|               } | ||||
|             }) | ||||
|           } | ||||
|         } else { | ||||
|           if (query.data?.following) { | ||||
|             analytics('relationship_outgoing_following') | ||||
|             content = t('button.following') | ||||
|             onPress = () => | ||||
|             onPress = () => { | ||||
|               analytics('relationship_outgoing_following_press') | ||||
|               mutation.mutate({ | ||||
|                 id, | ||||
|                 type: 'outgoing', | ||||
| @@ -87,10 +96,13 @@ const RelationshipOutgoing = React.memo( | ||||
|                   state: query.data?.following | ||||
|                 } | ||||
|               }) | ||||
|             } | ||||
|           } else { | ||||
|             if (query.data?.requested) { | ||||
|               analytics('relationship_outgoing_requested') | ||||
|               content = t('button.requested') | ||||
|               onPress = () => | ||||
|               onPress = () => { | ||||
|                 analytics('relationship_outgoing_requested_press') | ||||
|                 mutation.mutate({ | ||||
|                   id, | ||||
|                   type: 'outgoing', | ||||
| @@ -99,9 +111,12 @@ const RelationshipOutgoing = React.memo( | ||||
|                     state: query.data?.requested | ||||
|                   } | ||||
|                 }) | ||||
|               } | ||||
|             } else { | ||||
|               analytics('relationship_outgoing_default') | ||||
|               content = t('button.default') | ||||
|               onPress = () => | ||||
|               onPress = () => { | ||||
|                 analytics('relationship_outgoing_default_press') | ||||
|                 mutation.mutate({ | ||||
|                   id, | ||||
|                   type: 'outgoing', | ||||
| @@ -110,6 +125,7 @@ const RelationshipOutgoing = React.memo( | ||||
|                     state: false | ||||
|                   } | ||||
|                 }) | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { createNativeStackNavigator } from 'react-native-screens/native-stack' | ||||
| import { TabView } from 'react-native-tab-view' | ||||
| import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter' | ||||
| import { useSelector } from 'react-redux' | ||||
| import analytics from './analytics' | ||||
|  | ||||
| const Stack = createNativeStackNavigator< | ||||
|   Nav.LocalStackParamList | Nav.RemoteStackParamList | ||||
| @@ -41,6 +42,7 @@ const Timelines: React.FC<Props> = ({ name }) => { | ||||
|   const localActiveIndex = useSelector(getLocalActiveIndex) | ||||
|  | ||||
|   const onPressSearch = useCallback(() => { | ||||
|     analytics('search_tap', { page: mapNameToContent[name][segment].page }) | ||||
|     navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' }) | ||||
|   }, []) | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| import ComponentSeparator from '@components/Separator' | ||||
| import TimelineConversation from '@components/Timelines/Timeline/Conversation' | ||||
| import TimelineDefault from '@components/Timelines/Timeline/Default' | ||||
| import TimelineEmpty from '@components/Timelines/Timeline/Empty' | ||||
| import TimelineEnd from '@root/components/Timelines/Timeline/End' | ||||
| import TimelineHeader from '@components/Timelines/Timeline/Header' | ||||
| import TimelineNotifications from '@components/Timelines/Timeline/Notifications' | ||||
| import { useNavigation, useScrollToTop } from '@react-navigation/native' | ||||
| import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { getPublicRemoteNotice } from '@utils/slices/contextsSlice' | ||||
| import { localUpdateNotification } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { findIndex } from 'lodash' | ||||
| import React, { useCallback, useEffect, useMemo, useRef } from 'react' | ||||
| import { | ||||
|   FlatListProps, | ||||
| @@ -17,9 +14,12 @@ import { | ||||
| } from 'react-native' | ||||
| import { FlatList } from 'react-native-gesture-handler' | ||||
| import { useDispatch, useSelector } from 'react-redux' | ||||
| import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { findIndex } from 'lodash' | ||||
| import { getPublicRemoteNotice } from '@utils/slices/contextsSlice' | ||||
| import TimelineConversation from './Timeline/Conversation' | ||||
| import TimelineDefault from './Timeline/Default' | ||||
| import TimelineEmpty from './Timeline/Empty' | ||||
| import TimelineEnd from './Timeline/End' | ||||
| import TimelineHeader from './Timeline/Header' | ||||
| import TimelineNotifications from './Timeline/Notifications' | ||||
|  | ||||
| export interface Props { | ||||
|   page: App.Pages | ||||
| @@ -99,8 +99,10 @@ const Timeline: React.FC<Props> = ({ | ||||
|   }, [navigation, flattenData]) | ||||
|  | ||||
|   const flRef = useRef<FlatList<any>>(null) | ||||
|   const scrolled = useRef(false) | ||||
|   useEffect(() => { | ||||
|     if (toot && isSuccess) { | ||||
|     if (toot && isSuccess && !scrolled.current) { | ||||
|       scrolled.current = true | ||||
|       const pointer = findIndex(flattenData, ['id', toot]) | ||||
|       setTimeout(() => { | ||||
|         flRef.current?.scrollToIndex({ | ||||
| @@ -109,7 +111,7 @@ const Timeline: React.FC<Props> = ({ | ||||
|         }) | ||||
|       }, 500) | ||||
|     } | ||||
|   }, [isSuccess, flattenData]) | ||||
|   }, [isSuccess, flattenData.length, scrolled]) | ||||
|  | ||||
|   const keyExtractor = useCallback(({ id }) => id, []) | ||||
|   const renderItem = useCallback(({ item }) => { | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| import client from '@api/client' | ||||
| import analytics from '@components/analytics' | ||||
| import GracefullyImage from '@components/GracefullyImage' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { getLocalAccount } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -9,11 +12,42 @@ import { Pressable, StyleSheet, View } from 'react-native' | ||||
| import { useMutation, useQueryClient } from 'react-query' | ||||
| import { useSelector } from 'react-redux' | ||||
| import TimelineActions from './Shared/Actions' | ||||
| import TimelineAvatar from './Shared/Avatar' | ||||
| import TimelineContent from './Shared/Content' | ||||
| import TimelineHeaderConversation from './Shared/HeaderConversation' | ||||
| import TimelinePoll from './Shared/Poll' | ||||
|  | ||||
| const Avatars: React.FC<{ accounts: Mastodon.Account[] }> = ({ accounts }) => { | ||||
|   return ( | ||||
|     <View | ||||
|       style={{ | ||||
|         borderRadius: 4, | ||||
|         overflow: 'hidden', | ||||
|         marginRight: StyleConstants.Spacing.S, | ||||
|         width: StyleConstants.Avatar.M, | ||||
|         height: StyleConstants.Avatar.M, | ||||
|         flexDirection: 'row', | ||||
|         flexWrap: 'wrap' | ||||
|       }} | ||||
|     > | ||||
|       {accounts.slice(0, 4).map(account => ( | ||||
|         <GracefullyImage | ||||
|           key={account.id} | ||||
|           cache | ||||
|           uri={{ original: account.avatar_static }} | ||||
|           dimension={{ | ||||
|             width: StyleConstants.Avatar.M, | ||||
|             height: | ||||
|               accounts.length > 2 | ||||
|                 ? StyleConstants.Avatar.M / 2 | ||||
|                 : StyleConstants.Avatar.M | ||||
|           }} | ||||
|           style={{ flex: 1, flexBasis: '50%' }} | ||||
|         /> | ||||
|       ))} | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export interface Props { | ||||
|   conversation: Mastodon.Conversation | ||||
|   queryKey: QueryKeyTimeline | ||||
| @@ -42,9 +76,11 @@ const TimelineConversation: React.FC<Props> = ({ | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   const navigation = useNavigation() | ||||
|  | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('timeline_conversation_press') | ||||
|     if (conversation.last_status) { | ||||
|       conversation.unread && mutate() | ||||
|       navigation.push('Screen-Shared-Toot', { | ||||
| @@ -68,10 +104,7 @@ const TimelineConversation: React.FC<Props> = ({ | ||||
|       onPress={onPress} | ||||
|     > | ||||
|       <View style={styles.header}> | ||||
|         <TimelineAvatar | ||||
|           queryKey={queryKey} | ||||
|           account={conversation.accounts[0]} | ||||
|         /> | ||||
|         <Avatars accounts={conversation.accounts} /> | ||||
|         <TimelineHeaderConversation | ||||
|           queryKey={queryKey} | ||||
|           conversation={conversation} | ||||
| @@ -112,6 +145,7 @@ const TimelineConversation: React.FC<Props> = ({ | ||||
|             <TimelineActions | ||||
|               queryKey={queryKey} | ||||
|               status={conversation.last_status} | ||||
|               accts={conversation.accounts.map(account => account.acct)} | ||||
|               reblog={false} | ||||
|             /> | ||||
|           </View> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned' | ||||
| import TimelineActions from '@components/Timelines/Timeline/Shared/Actions' | ||||
| import TimelineAttachment from '@components/Timelines/Timeline/Shared/Attachment' | ||||
| @@ -7,6 +8,7 @@ import TimelineContent from '@components/Timelines/Timeline/Shared/Content' | ||||
| import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderDefault' | ||||
| import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { getLocalAccount } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -17,6 +19,7 @@ import { useSelector } from 'react-redux' | ||||
| export interface Props { | ||||
|   item: Mastodon.Status & { isPinned?: boolean } | ||||
|   queryKey?: QueryKeyTimeline | ||||
|   origin?: string | ||||
|   highlighted?: boolean | ||||
|   disableDetails?: boolean | ||||
|   disableOnPress?: boolean | ||||
| @@ -26,27 +29,42 @@ export interface Props { | ||||
| const TimelineDefault: React.FC<Props> = ({ | ||||
|   item, | ||||
|   queryKey, | ||||
|   origin, | ||||
|   highlighted = false, | ||||
|   disableDetails = false, | ||||
|   disableOnPress = false | ||||
| }) => { | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const navigation = useNavigation() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|  | ||||
|   let actualStatus = item.reblog ? item.reblog : item | ||||
|  | ||||
|   const onPress = useCallback( | ||||
|     () => | ||||
|       !disableOnPress && | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('timeline_default_press', { | ||||
|       page: queryKey ? queryKey[1].page : origin | ||||
|     }) | ||||
|     !disableOnPress && | ||||
|       !highlighted && | ||||
|       navigation.push('Screen-Shared-Toot', { | ||||
|         toot: actualStatus | ||||
|       }), | ||||
|     [] | ||||
|   ) | ||||
|       }) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <Pressable style={styles.statusView} onPress={onPress}> | ||||
|     <Pressable | ||||
|       style={[ | ||||
|         styles.statusView, | ||||
|         { | ||||
|           paddingBottom: | ||||
|             disableDetails && disableOnPress | ||||
|               ? StyleConstants.Spacing.Global.PagePadding | ||||
|               : 0 | ||||
|         } | ||||
|       ]} | ||||
|       onPress={onPress} | ||||
|     > | ||||
|       {item.reblog ? ( | ||||
|         <TimelineActioned action='reblog' account={item.account} /> | ||||
|       ) : item.isPinned ? ( | ||||
| @@ -107,6 +125,11 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
|           <TimelineActions | ||||
|             queryKey={queryKey} | ||||
|             status={actualStatus} | ||||
|             accts={([actualStatus.account] as Mastodon.Account[] & | ||||
|               Mastodon.Mention[]) | ||||
|               .concat(actualStatus.mentions) | ||||
|               .filter(d => d.id !== localAccount?.id) | ||||
|               .map(d => d.acct)} | ||||
|             reblog={item.reblog ? true : false} | ||||
|           /> | ||||
|         </View> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import Icon from '@components/Icon' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -37,7 +38,10 @@ const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => { | ||||
|             <Button | ||||
|               type='text' | ||||
|               content={t('empty.error.button')} | ||||
|               onPress={() => refetch()} | ||||
|               onPress={() => { | ||||
|                 analytics('timeline_error_press_refetch') | ||||
|                 refetch() | ||||
|               }} | ||||
|             /> | ||||
|           </> | ||||
|         ) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import Icon from '@root/components/Icon' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| @@ -22,6 +23,7 @@ const TimelineHeader = React.memo( | ||||
|           <Text | ||||
|             style={{ color: theme.blue }} | ||||
|             onPress={() => { | ||||
|               analytics('timeline_remote_header_press') | ||||
|               dispatch(updatePublicRemoteNotice(1)) | ||||
|               navigation.navigate('Screen-Me', { | ||||
|                 screen: 'Screen-Me-Root', | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned' | ||||
| import TimelineActions from '@components/Timelines/Timeline/Shared/Actions' | ||||
| import TimelineAttachment from '@components/Timelines/Timeline/Shared/Attachment' | ||||
| @@ -7,6 +8,7 @@ import TimelineContent from '@components/Timelines/Timeline/Shared/Content' | ||||
| import TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/HeaderNotification' | ||||
| import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { getLocalAccount } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -26,19 +28,20 @@ const TimelineNotifications: React.FC<Props> = ({ | ||||
|   highlighted = false | ||||
| }) => { | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const navigation = useNavigation() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   const actualAccount = notification.status | ||||
|     ? notification.status.account | ||||
|     : notification.account | ||||
|  | ||||
|   const onPress = useCallback( | ||||
|     () => | ||||
|       notification.status && | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('timeline_notification_press') | ||||
|     notification.status && | ||||
|       navigation.push('Screen-Shared-Toot', { | ||||
|         toot: notification.status | ||||
|       }), | ||||
|     [] | ||||
|   ) | ||||
|       }) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <Pressable style={styles.notificationView} onPress={onPress}> | ||||
| @@ -112,6 +115,11 @@ const TimelineNotifications: React.FC<Props> = ({ | ||||
|           <TimelineActions | ||||
|             queryKey={queryKey} | ||||
|             status={notification.status} | ||||
|             accts={([notification.status.account] as Mastodon.Account[] & | ||||
|               Mastodon.Mention[]) | ||||
|               .concat(notification.status.mentions) | ||||
|               .filter(d => d.id !== localAccount?.id) | ||||
|               .map(d => d.acct)} | ||||
|             reblog={false} | ||||
|           /> | ||||
|         </View> | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Icon from '@components/Icon' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useMemo } from 'react' | ||||
| @@ -20,7 +22,9 @@ const TimelineActioned: React.FC<Props> = ({ | ||||
| }) => { | ||||
|   const { t } = useTranslation('componentTimeline') | ||||
|   const { theme } = useTheme() | ||||
|   const navigation = useNavigation() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   const name = account.display_name || account.username | ||||
|   const iconColor = theme.primary | ||||
|  | ||||
| @@ -29,6 +33,7 @@ const TimelineActioned: React.FC<Props> = ({ | ||||
|   ) | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('timeline_shared_actioned_press', { action }) | ||||
|     navigation.push('Screen-Shared-Account', { account }) | ||||
|   }, []) | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| import { toast } from '@components/toast' | ||||
| @@ -17,10 +18,16 @@ import { useQueryClient } from 'react-query' | ||||
| export interface Props { | ||||
|   queryKey: QueryKeyTimeline | ||||
|   status: Mastodon.Status | ||||
|   accts: Mastodon.Account['acct'][] // When replying to conversations | ||||
|   reblog: boolean | ||||
| } | ||||
|  | ||||
| const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => { | ||||
| const TimelineActions: React.FC<Props> = ({ | ||||
|   queryKey, | ||||
|   status, | ||||
|   accts, | ||||
|   reblog | ||||
| }) => { | ||||
|   const navigation = useNavigation() | ||||
|   const { t } = useTranslation('componentTimeline') | ||||
|   const { theme } = useTheme() | ||||
| @@ -92,63 +99,74 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => { | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   const onPressReply = useCallback( | ||||
|     () => | ||||
|       navigation.navigate('Screen-Shared-Compose', { | ||||
|         type: 'reply', | ||||
|         incomingStatus: status, | ||||
|         queryKey | ||||
|       }), | ||||
|     [] | ||||
|   ) | ||||
|   const onPressReblog = useCallback( | ||||
|     () => | ||||
|       mutation.mutate({ | ||||
|         type: 'updateStatusProperty', | ||||
|         queryKey, | ||||
|         id: status.id, | ||||
|         reblog, | ||||
|         payload: { | ||||
|           property: 'reblogged', | ||||
|           currentValue: status.reblogged, | ||||
|           propertyCount: 'reblogs_count', | ||||
|           countValue: status.reblogs_count | ||||
|         } | ||||
|       }), | ||||
|     [status.reblogged] | ||||
|   ) | ||||
|   const onPressFavourite = useCallback( | ||||
|     () => | ||||
|       mutation.mutate({ | ||||
|         type: 'updateStatusProperty', | ||||
|         queryKey, | ||||
|         id: status.id, | ||||
|         reblog, | ||||
|         payload: { | ||||
|           property: 'favourited', | ||||
|           currentValue: status.favourited, | ||||
|           propertyCount: 'favourites_count', | ||||
|           countValue: status.favourites_count | ||||
|         } | ||||
|       }), | ||||
|     [status.favourited] | ||||
|   ) | ||||
|   const onPressBookmark = useCallback( | ||||
|     () => | ||||
|       mutation.mutate({ | ||||
|         type: 'updateStatusProperty', | ||||
|         queryKey, | ||||
|         id: status.id, | ||||
|         reblog, | ||||
|         payload: { | ||||
|           property: 'bookmarked', | ||||
|           currentValue: status.bookmarked, | ||||
|           propertyCount: undefined, | ||||
|           countValue: undefined | ||||
|         } | ||||
|       }), | ||||
|     [status.bookmarked] | ||||
|   ) | ||||
|   const onPressReply = useCallback(() => { | ||||
|     analytics('timeline_shared_actions_reply_press', { | ||||
|       page: queryKey[1].page, | ||||
|       count: status.replies_count | ||||
|     }) | ||||
|     navigation.navigate('Screen-Shared-Compose', { | ||||
|       type: 'reply', | ||||
|       incomingStatus: status, | ||||
|       accts, | ||||
|       queryKey | ||||
|     }) | ||||
|   }, [status.replies_count]) | ||||
|   const onPressReblog = useCallback(() => { | ||||
|     analytics('timeline_shared_actions_reblog_press', { | ||||
|       page: queryKey[1].page, | ||||
|       count: status.reblogs_count, | ||||
|       current: status.reblogged | ||||
|     }) | ||||
|     mutation.mutate({ | ||||
|       type: 'updateStatusProperty', | ||||
|       queryKey, | ||||
|       id: status.id, | ||||
|       reblog, | ||||
|       payload: { | ||||
|         property: 'reblogged', | ||||
|         currentValue: status.reblogged, | ||||
|         propertyCount: 'reblogs_count', | ||||
|         countValue: status.reblogs_count | ||||
|       } | ||||
|     }) | ||||
|   }, [status.reblogged, status.reblogs_count]) | ||||
|   const onPressFavourite = useCallback(() => { | ||||
|     analytics('timeline_shared_actions_favourite_press', { | ||||
|       page: queryKey[1].page, | ||||
|       count: status.favourites_count, | ||||
|       current: status.favourited | ||||
|     }) | ||||
|     mutation.mutate({ | ||||
|       type: 'updateStatusProperty', | ||||
|       queryKey, | ||||
|       id: status.id, | ||||
|       reblog, | ||||
|       payload: { | ||||
|         property: 'favourited', | ||||
|         currentValue: status.favourited, | ||||
|         propertyCount: 'favourites_count', | ||||
|         countValue: status.favourites_count | ||||
|       } | ||||
|     }) | ||||
|   }, [status.favourited, status.favourites_count]) | ||||
|   const onPressBookmark = useCallback(() => { | ||||
|     analytics('timeline_shared_actions_bookmark_press', { | ||||
|       page: queryKey[1].page, | ||||
|       current: status.bookmarked | ||||
|     }) | ||||
|     mutation.mutate({ | ||||
|       type: 'updateStatusProperty', | ||||
|       queryKey, | ||||
|       id: status.id, | ||||
|       reblog, | ||||
|       payload: { | ||||
|         property: 'bookmarked', | ||||
|         currentValue: status.bookmarked, | ||||
|         propertyCount: undefined, | ||||
|         countValue: undefined | ||||
|       } | ||||
|     }) | ||||
|   }, [status.bookmarked]) | ||||
|  | ||||
|   const childrenReply = useMemo( | ||||
|     () => ( | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import AttachmentAudio from '@components/Timelines/Timeline/Shared/Attachment/Audio' | ||||
| @@ -21,13 +22,15 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => { | ||||
|  | ||||
|   const [sensitiveShown, setSensitiveShown] = useState(status.sensitive) | ||||
|   const onPressBlurView = useCallback(() => { | ||||
|     analytics('timeline_shared_attachment_blurview_press_show') | ||||
|     layoutAnimation() | ||||
|     setSensitiveShown(false) | ||||
|     haptics('Medium') | ||||
|     haptics('Light') | ||||
|   }, []) | ||||
|   const onPressShow = useCallback(() => { | ||||
|     analytics('timeline_shared_attachment_blurview_press_hide') | ||||
|     setSensitiveShown(true) | ||||
|     haptics('Medium') | ||||
|     haptics('Light') | ||||
|   }, []) | ||||
|  | ||||
|   let imageUrls: (IImageInfo & { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import { Blurhash } from 'gl-react-blurhash' | ||||
| import React, { useCallback, useState } from 'react' | ||||
| import { StyleSheet, View } from 'react-native' | ||||
| import attachmentAspectRatio from './aspectRatio' | ||||
| import analytics from '@components/analytics' | ||||
|  | ||||
| export interface Props { | ||||
|   total: number | ||||
| @@ -29,6 +30,7 @@ const AttachmentAudio: React.FC<Props> = ({ | ||||
|   const [audioPlaying, setAudioPlaying] = useState(false) | ||||
|   const [audioPosition, setAudioPosition] = useState(0) | ||||
|   const playAudio = useCallback(async () => { | ||||
|     analytics('timeline_shared_attachment_audio_play_press', { id: audio.id }) | ||||
|     if (!audioPlayer) { | ||||
|       const { sound } = await Audio.Sound.createAsync( | ||||
|         { uri: audio.url }, | ||||
| @@ -44,6 +46,7 @@ const AttachmentAudio: React.FC<Props> = ({ | ||||
|     } | ||||
|   }, [audioPlayer, audioPosition]) | ||||
|   const pauseAudio = useCallback(async () => { | ||||
|     analytics('timeline_shared_attachment_audio_pause_press', { id: audio.id }) | ||||
|     audioPlayer!.pauseAsync() | ||||
|     setAudioPlaying(false) | ||||
|   }, [audioPlayer]) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import GracefullyImage from '@components/GracefullyImage' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import React, { useCallback } from 'react' | ||||
| @@ -19,7 +20,10 @@ const AttachmentImage: React.FC<Props> = ({ | ||||
|   image, | ||||
|   navigateToImagesViewer | ||||
| }) => { | ||||
|   const onPress = useCallback(() => navigateToImagesViewer(index), []) | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('timeline_shared_attachment_image_press', { id: image.id }) | ||||
|     navigateToImagesViewer(index) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <GracefullyImage | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import openLink from '@components/openLink' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -59,9 +60,10 @@ const AttachmentUnsupported: React.FC<Props> = ({ | ||||
|               content={t('shared.attachment.unsupported.button')} | ||||
|               size='S' | ||||
|               overlay | ||||
|               onPress={async () => | ||||
|               onPress={async () => { | ||||
|                 analytics('timeline_shared_attachment_unsupported_press') | ||||
|                 attachment.remote_url && (await openLink(attachment.remote_url)) | ||||
|               } | ||||
|               }} | ||||
|             /> | ||||
|           ) : null} | ||||
|         </> | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { Blurhash } from 'gl-react-blurhash' | ||||
| import React, { useCallback, useRef, useState } from 'react' | ||||
| import { Pressable, StyleSheet, View } from 'react-native' | ||||
| import attachmentAspectRatio from './aspectRatio' | ||||
| import analytics from '@components/analytics' | ||||
|  | ||||
| export interface Props { | ||||
|   total: number | ||||
| @@ -25,6 +26,13 @@ const AttachmentVideo: React.FC<Props> = ({ | ||||
|   const [videoLoaded, setVideoLoaded] = useState(false) | ||||
|   const [videoPosition, setVideoPosition] = useState<number>(0) | ||||
|   const playOnPress = useCallback(async () => { | ||||
|     analytics('timeline_shared_attachment_video_length', { | ||||
|       length: video.meta?.length | ||||
|     }) | ||||
|     analytics('timeline_shared_attachment_vide_play_press', { | ||||
|       id: video.id, | ||||
|       timestamp: Date.now() | ||||
|     }) | ||||
|     setVideoLoading(true) | ||||
|     if (!videoLoaded) { | ||||
|       await videoPlayer.current?.loadAsync({ uri: video.url }) | ||||
| @@ -66,6 +74,10 @@ const AttachmentVideo: React.FC<Props> = ({ | ||||
|         useNativeControls={false} | ||||
|         onFullscreenUpdate={event => { | ||||
|           if (event.fullscreenUpdate === 3) { | ||||
|             analytics('timeline_shared_attachment_video_pause_press', { | ||||
|               id: video.id, | ||||
|               timestamp: Date.now() | ||||
|             }) | ||||
|             videoPlayer.current?.pauseAsync() | ||||
|           } | ||||
|         }} | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import GracefullyImage from '@components/GracefullyImage' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import analytics from '@components/analytics' | ||||
|  | ||||
| export interface Props { | ||||
|   queryKey?: QueryKeyTimeline | ||||
| @@ -10,9 +12,12 @@ export interface Props { | ||||
| } | ||||
|  | ||||
| const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => { | ||||
|   const navigation = useNavigation() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   // Need to fix go back root | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('timeline_shared_avatar_press', { page: queryKey[1].page }) | ||||
|     queryKey && navigation.push('Screen-Shared-Account', { account }) | ||||
|   }, []) | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import GracefullyImage from '@components/GracefullyImage' | ||||
| import openLink from '@components/openLink' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -15,7 +16,10 @@ const TimelineCard: React.FC<Props> = ({ card }) => { | ||||
|   return ( | ||||
|     <Pressable | ||||
|       style={[styles.card, { borderColor: theme.border }]} | ||||
|       onPress={async () => await openLink(card.url)} | ||||
|       onPress={async () => { | ||||
|         analytics('timeline_shared_card_press') | ||||
|         await openLink(card.url) | ||||
|       }} | ||||
|       testID='base' | ||||
|     > | ||||
|       {card.image && ( | ||||
| @@ -42,7 +46,10 @@ const TimelineCard: React.FC<Props> = ({ card }) => { | ||||
|             {card.description} | ||||
|           </Text> | ||||
|         ) : null} | ||||
|         <Text numberOfLines={1} style={{ color: theme.secondary }}> | ||||
|         <Text | ||||
|           numberOfLines={1} | ||||
|           style={[styles.rightLink, { color: theme.secondary }]} | ||||
|         > | ||||
|           {card.url} | ||||
|         </Text> | ||||
|       </View> | ||||
| @@ -54,14 +61,14 @@ const styles = StyleSheet.create({ | ||||
|   card: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     height: StyleConstants.Font.LineHeight.M * 4.5, | ||||
|     height: StyleConstants.Font.LineHeight.M * 5, | ||||
|     marginTop: StyleConstants.Spacing.M, | ||||
|     borderWidth: StyleSheet.hairlineWidth, | ||||
|     borderRadius: 6 | ||||
|   }, | ||||
|   left: { | ||||
|     width: StyleConstants.Font.LineHeight.M * 4.5, | ||||
|     height: StyleConstants.Font.LineHeight.M * 4.5 | ||||
|     width: StyleConstants.Font.LineHeight.M * 5, | ||||
|     height: StyleConstants.Font.LineHeight.M * 5 | ||||
|   }, | ||||
|   image: { | ||||
|     width: '100%', | ||||
| @@ -74,11 +81,16 @@ const styles = StyleSheet.create({ | ||||
|     padding: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   rightTitle: { | ||||
|     ...StyleConstants.FontStyle.S, | ||||
|     marginBottom: StyleConstants.Spacing.XS, | ||||
|     fontWeight: StyleConstants.Font.Weight.Bold | ||||
|   }, | ||||
|   rightDescription: { | ||||
|     ...StyleConstants.FontStyle.S, | ||||
|     marginBottom: StyleConstants.Spacing.XS | ||||
|   }, | ||||
|   rightLink: { | ||||
|     ...StyleConstants.FontStyle.S | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import haptics from '@components/haptics' | ||||
| import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu' | ||||
| import { toast } from '@components/toast' | ||||
| import { | ||||
|   MutationVarsTimelineUpdateAccountProperty, | ||||
|   QueryKeyTimeline, | ||||
|   useTimelineMutation | ||||
| } from '@utils/queryHooks/timeline' | ||||
| @@ -11,7 +13,7 @@ import { useQueryClient } from 'react-query' | ||||
|  | ||||
| export interface Props { | ||||
|   queryKey?: QueryKeyTimeline | ||||
|   account: Pick<Mastodon.Account, 'id' | 'acct'> | ||||
|   account: Mastodon.Account | ||||
|   setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>> | ||||
| } | ||||
|  | ||||
| @@ -25,23 +27,30 @@ const HeaderActionsAccount: React.FC<Props> = ({ | ||||
|   const queryClient = useQueryClient() | ||||
|   const mutateion = useTimelineMutation({ | ||||
|     queryClient, | ||||
|     onSuccess: (_, { payload: { property } }) => { | ||||
|     onSuccess: (_, params) => { | ||||
|       const theParams = params as MutationVarsTimelineUpdateAccountProperty | ||||
|       haptics('Success') | ||||
|       toast({ | ||||
|         type: 'success', | ||||
|         message: t('common:toastMessage.success.message', { | ||||
|           function: t(`shared.header.actions.account.${property}.function`, { | ||||
|             acct: account.acct | ||||
|           }) | ||||
|           function: t( | ||||
|             `shared.header.actions.account.${theParams.payload.property}.function`, | ||||
|             { | ||||
|               acct: account.acct | ||||
|             } | ||||
|           ) | ||||
|         }) | ||||
|       }) | ||||
|     }, | ||||
|     onError: (err: any, { payload: { property } }) => { | ||||
|     onError: (err: any, params) => { | ||||
|       const theParams = params as MutationVarsTimelineUpdateAccountProperty | ||||
|       haptics('Error') | ||||
|       toast({ | ||||
|         type: 'error', | ||||
|         message: t('common:toastMessage.error.message', { | ||||
|           function: t(`shared.header.actions.account.${property}.function`) | ||||
|           function: t( | ||||
|             `shared.header.actions.account.${theParams.payload.property}.function` | ||||
|           ) | ||||
|         }), | ||||
|         ...(err.status && | ||||
|           typeof err.status === 'number' && | ||||
| @@ -62,6 +71,9 @@ const HeaderActionsAccount: React.FC<Props> = ({ | ||||
|       <MenuHeader heading={t('shared.header.actions.account.heading')} /> | ||||
|       <MenuRow | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_headeractions_account_mute_press', { | ||||
|             page: queryKey && queryKey[1].page | ||||
|           }) | ||||
|           setBottomSheetVisible(false) | ||||
|           mutateion.mutate({ | ||||
|             type: 'updateAccountProperty', | ||||
| @@ -77,6 +89,9 @@ const HeaderActionsAccount: React.FC<Props> = ({ | ||||
|       /> | ||||
|       <MenuRow | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_headeractions_account_block_press', { | ||||
|             page: queryKey && queryKey[1].page | ||||
|           }) | ||||
|           setBottomSheetVisible(false) | ||||
|           mutateion.mutate({ | ||||
|             type: 'updateAccountProperty', | ||||
| @@ -92,6 +107,9 @@ const HeaderActionsAccount: React.FC<Props> = ({ | ||||
|       /> | ||||
|       <MenuRow | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_headeractions_account_reports_press', { | ||||
|             page: queryKey && queryKey[1].page | ||||
|           }) | ||||
|           setBottomSheetVisible(false) | ||||
|           mutateion.mutate({ | ||||
|             type: 'updateAccountProperty', | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import MenuContainer from '@components/Menu/Container' | ||||
| import MenuHeader from '@components/Menu/Header' | ||||
| import MenuRow from '@components/Menu/Row' | ||||
| @@ -42,6 +43,9 @@ const HeaderActionsDomain: React.FC<Props> = ({ | ||||
|       <MenuHeader heading={t(`shared.header.actions.domain.heading`)} /> | ||||
|       <MenuRow | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_headeractions_domain_block_press', { | ||||
|             page: queryKey[1].page | ||||
|           }) | ||||
|           Alert.alert( | ||||
|             t('shared.header.actions.domain.alert.title', { domain }), | ||||
|             t('shared.header.actions.domain.alert.message'), | ||||
| @@ -54,6 +58,12 @@ const HeaderActionsDomain: React.FC<Props> = ({ | ||||
|                 text: t('shared.header.actions.domain.alert.buttons.confirm'), | ||||
|                 style: 'destructive', | ||||
|                 onPress: () => { | ||||
|                   analytics( | ||||
|                     'timeline_shared_headeractions_domain_block_confirm', | ||||
|                     { | ||||
|                       page: queryKey && queryKey[1].page | ||||
|                     } | ||||
|                   ) | ||||
|                   setBottomSheetVisible(false) | ||||
|                   mutation.mutate({ | ||||
|                     type: 'domainBlock', | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import BottomSheet from '@components/BottomSheet' | ||||
| import Icon from '@components/Icon' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| @@ -33,7 +34,12 @@ const HeaderActions = React.memo( | ||||
|     const sameDomain = localDomain === statusDomain | ||||
|  | ||||
|     const [modalVisible, setBottomSheetVisible] = useState(false) | ||||
|     const onPress = useCallback(() => setBottomSheetVisible(true), []) | ||||
|     const onPress = useCallback(() => { | ||||
|       analytics('bottomsheet_open_press', { | ||||
|         page: queryKey[1].page | ||||
|       }) | ||||
|       setBottomSheetVisible(true) | ||||
|     }, []) | ||||
|     const children = useMemo( | ||||
|       () => ( | ||||
|         <Icon | ||||
|   | ||||
| @@ -1,11 +1,7 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import MenuContainer from '@components/Menu/Container' | ||||
| import MenuHeader from '@components/Menu/Header' | ||||
| import MenuRow from '@components/Menu/Row' | ||||
| import { toast } from '@components/toast' | ||||
| import { | ||||
|   QueryKeyTimeline, | ||||
|   useTimelineMutation | ||||
| } from '@utils/queryHooks/timeline' | ||||
| import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Platform, Share } from 'react-native' | ||||
| @@ -30,6 +26,7 @@ const HeaderActionsShare: React.FC<Props> = ({ | ||||
|         iconFront='Share2' | ||||
|         title={t(`shared.header.actions.share.${type}.button`)} | ||||
|         onPress={async () => { | ||||
|           analytics('timeline_shared_headeractions_share_press') | ||||
|           switch (Platform.OS) { | ||||
|             case 'ios': | ||||
|               await Share.share({ | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import { | ||||
|   QueryKeyTimeline, | ||||
|   useTimelineMutation | ||||
| } from '@utils/queryHooks/timeline' | ||||
| import analytics from '@components/analytics' | ||||
|  | ||||
| export interface Props { | ||||
|   queryKey: QueryKeyTimeline | ||||
| @@ -37,9 +38,7 @@ const HeaderActionsStatus: React.FC<Props> = ({ | ||||
|       toast({ | ||||
|         type: 'error', | ||||
|         message: t('common:toastMessage.error.message', { | ||||
|           function: t( | ||||
|             `shared.header.actions.status.${theFunction}.function` | ||||
|           ) | ||||
|           function: t(`shared.header.actions.status.${theFunction}.function`) | ||||
|         }), | ||||
|         ...(err.status && | ||||
|           typeof err.status === 'number' && | ||||
| @@ -55,11 +54,12 @@ const HeaderActionsStatus: React.FC<Props> = ({ | ||||
|  | ||||
|   return ( | ||||
|     <MenuContainer> | ||||
|       <MenuHeader | ||||
|         heading={t('shared.header.actions.status.heading')} | ||||
|       /> | ||||
|       <MenuHeader heading={t('shared.header.actions.status.heading')} /> | ||||
|       <MenuRow | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_headeractions_status_delete_press', { | ||||
|             page: queryKey && queryKey[1].page | ||||
|           }) | ||||
|           setBottomSheetVisible(false) | ||||
|           mutation.mutate({ | ||||
|             type: 'deleteItem', | ||||
| @@ -73,19 +73,31 @@ const HeaderActionsStatus: React.FC<Props> = ({ | ||||
|       /> | ||||
|       <MenuRow | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_headeractions_status_deleteedit_press', { | ||||
|             page: queryKey && queryKey[1].page | ||||
|           }) | ||||
|           Alert.alert( | ||||
|             t('shared.header.actions.status.edit.alert.title'), | ||||
|             t( | ||||
|               'shared.header.actions.status.edit.alert.message' | ||||
|             ), | ||||
|             t('shared.header.actions.status.edit.alert.message'), | ||||
|             [ | ||||
|               { text: t('shared.header.actions.status.edit.alert.buttons.cancel'), style: 'cancel' }, | ||||
|               { | ||||
|                 text: t( | ||||
|                   'shared.header.actions.status.edit.alert.buttons.cancel' | ||||
|                 ), | ||||
|                 style: 'cancel' | ||||
|               }, | ||||
|               { | ||||
|                 text: t( | ||||
|                   'shared.header.actions.status.edit.alert.buttons.confirm' | ||||
|                 ), | ||||
|                 style: 'destructive', | ||||
|                 onPress: async () => { | ||||
|                   analytics( | ||||
|                     'timeline_shared_headeractions_status_deleteedit_confirm', | ||||
|                     { | ||||
|                       page: queryKey && queryKey[1].page | ||||
|                     } | ||||
|                   ) | ||||
|                   setBottomSheetVisible(false) | ||||
|                   const res = await mutation.mutateAsync({ | ||||
|                     type: 'deleteItem', | ||||
| @@ -110,46 +122,54 @@ const HeaderActionsStatus: React.FC<Props> = ({ | ||||
|       /> | ||||
|       <MenuRow | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_headeractions_status_mute_press', { | ||||
|             page: queryKey && queryKey[1].page | ||||
|           }) | ||||
|           setBottomSheetVisible(false) | ||||
|           mutation.mutate({ | ||||
|             type: 'updateStatusProperty', | ||||
|             queryKey, | ||||
|             id: status.id, | ||||
|             payload: { property: 'muted', currentValue: status.muted } | ||||
|             payload: { | ||||
|               property: 'muted', | ||||
|               currentValue: status.muted, | ||||
|               propertyCount: undefined, | ||||
|               countValue: undefined | ||||
|             } | ||||
|           }) | ||||
|         }} | ||||
|         iconFront='VolumeX' | ||||
|         title={ | ||||
|           status.muted | ||||
|             ? t( | ||||
|                 'shared.header.actions.status.mute.button.negative' | ||||
|               ) | ||||
|             : t( | ||||
|                 'shared.header.actions.status.mute.button.positive' | ||||
|               ) | ||||
|             ? t('shared.header.actions.status.mute.button.negative') | ||||
|             : t('shared.header.actions.status.mute.button.positive') | ||||
|         } | ||||
|       /> | ||||
|       {/* Also note that reblogs cannot be pinned. */} | ||||
|       {(status.visibility === 'public' || status.visibility === 'unlisted') && ( | ||||
|         <MenuRow | ||||
|           onPress={() => { | ||||
|             analytics('timeline_shared_headeractions_status_pin_press', { | ||||
|               page: queryKey && queryKey[1].page | ||||
|             }) | ||||
|             setBottomSheetVisible(false) | ||||
|             mutation.mutate({ | ||||
|               type: 'updateStatusProperty', | ||||
|               queryKey, | ||||
|               id: status.id, | ||||
|               payload: { property: 'pinned', currentValue: status.pinned } | ||||
|               payload: { | ||||
|                 property: 'pinned', | ||||
|                 currentValue: status.pinned, | ||||
|                 propertyCount: undefined, | ||||
|                 countValue: undefined | ||||
|               } | ||||
|             }) | ||||
|           }} | ||||
|           iconFront='Anchor' | ||||
|           title={ | ||||
|             status.pinned | ||||
|               ? t( | ||||
|                   'shared.header.actions.status.pin.button.negative' | ||||
|                 ) | ||||
|               : t( | ||||
|                   'shared.header.actions.status.pin.button.positive' | ||||
|                 ) | ||||
|               ? t('shared.header.actions.status.pin.button.negative') | ||||
|               : t('shared.header.actions.status.pin.button.positive') | ||||
|           } | ||||
|         /> | ||||
|       )} | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import { toast } from '@components/toast' | ||||
| import { | ||||
|   QueryKeyTimeline, | ||||
| @@ -9,12 +11,34 @@ import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useMemo } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Pressable, StyleSheet, View } from 'react-native' | ||||
| import { Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import { useQueryClient } from 'react-query' | ||||
| import HeaderSharedAccount from './HeaderShared/Account' | ||||
| import HeaderSharedCreated from './HeaderShared/Created' | ||||
| import HeaderSharedMuted from './HeaderShared/Muted' | ||||
|  | ||||
| const Names: React.FC<{ accounts: Mastodon.Account[] }> = ({ accounts }) => { | ||||
|   const { t } = useTranslation('componentTimeline') | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   return ( | ||||
|     <Text numberOfLines={1}> | ||||
|       <Text style={[styles.namesLeading, { color: theme.secondary }]}> | ||||
|         {t('shared.header.conversation.withAccounts')}{' '} | ||||
|       </Text> | ||||
|       {accounts.map((account, index) => ( | ||||
|         <Text key={account.id} numberOfLines={1}> | ||||
|           {index !== 0 ? ', ' : undefined} | ||||
|           <ParseEmojis | ||||
|             content={account.display_name || account.username} | ||||
|             emojis={account.emojis} | ||||
|             fontBold | ||||
|           /> | ||||
|         </Text> | ||||
|       ))} | ||||
|     </Text> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export interface Props { | ||||
|   queryKey: QueryKeyTimeline | ||||
|   conversation: Mastodon.Conversation | ||||
| @@ -49,16 +73,15 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => { | ||||
|  | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const actionOnPress = useCallback( | ||||
|     () => | ||||
|       mutation.mutate({ | ||||
|         type: 'deleteItem', | ||||
|         source: 'conversations', | ||||
|         queryKey, | ||||
|         id: conversation.id | ||||
|       }), | ||||
|     [] | ||||
|   ) | ||||
|   const actionOnPress = useCallback(() => { | ||||
|     analytics('timeline_conversation_delete_press') | ||||
|     mutation.mutate({ | ||||
|       type: 'deleteItem', | ||||
|       source: 'conversations', | ||||
|       queryKey, | ||||
|       id: conversation.id | ||||
|     }) | ||||
|   }, []) | ||||
|  | ||||
|   const actionChildren = useMemo( | ||||
|     () => ( | ||||
| @@ -74,7 +97,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => { | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <View style={styles.nameAndMeta}> | ||||
|         <HeaderSharedAccount account={conversation.accounts[0]} /> | ||||
|         <Names accounts={conversation.accounts} /> | ||||
|         <View style={styles.meta}> | ||||
|           {conversation.last_status?.created_at ? ( | ||||
|             <HeaderSharedCreated | ||||
| @@ -100,7 +123,7 @@ const styles = StyleSheet.create({ | ||||
|     flexDirection: 'row' | ||||
|   }, | ||||
|   nameAndMeta: { | ||||
|     flex: 4 | ||||
|     flex: 3 | ||||
|   }, | ||||
|   meta: { | ||||
|     flexDirection: 'row', | ||||
| @@ -115,6 +138,9 @@ const styles = StyleSheet.create({ | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'center' | ||||
|   }, | ||||
|   namesLeading: { | ||||
|     ...StyleConstants.FontStyle.M | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -45,7 +45,7 @@ const styles = StyleSheet.create({ | ||||
|     flexDirection: 'row' | ||||
|   }, | ||||
|   accountAndMeta: { | ||||
|     flex: 4 | ||||
|     flex: 5 | ||||
|   }, | ||||
|   meta: { | ||||
|     flexDirection: 'row', | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { StyleSheet, Text, View } from 'react-native' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | ||||
|   withoutName?: boolean | ||||
|   withoutName?: boolean // For notification follow request etc. | ||||
| } | ||||
|  | ||||
| const HeaderSharedAccount: React.FC<Props> = ({ | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import openLink from '@components/openLink' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| @@ -15,9 +16,12 @@ const HeaderSharedApplication: React.FC<Props> = ({ application }) => { | ||||
|  | ||||
|   return application && application.name !== 'Web' ? ( | ||||
|     <Text | ||||
|       onPress={async () => | ||||
|       onPress={async () => { | ||||
|         analytics('timeline_shared_header_application_press', { | ||||
|           application | ||||
|         }) | ||||
|         application.website && (await openLink(application.website)) | ||||
|       } | ||||
|       }} | ||||
|       style={[styles.application, { color: theme.secondary }]} | ||||
|     > | ||||
|       {t('shared.header.shared.application', { application: application.name })} | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| @@ -49,6 +50,7 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|       toast({ | ||||
|         type: 'error', | ||||
|         message: t('common:toastMessage.error.message', { | ||||
|           // @ts-ignore | ||||
|           function: t(`shared.poll.meta.button.${theParams.payload.type}`) | ||||
|         }), | ||||
|         ...(err.status && | ||||
| @@ -69,7 +71,8 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|         return ( | ||||
|           <View style={styles.button}> | ||||
|             <Button | ||||
|               onPress={() => | ||||
|               onPress={() => { | ||||
|                 analytics('timeline_shared_vote_vote_press') | ||||
|                 mutation.mutate({ | ||||
|                   type: 'updateStatusProperty', | ||||
|                   queryKey, | ||||
| @@ -82,7 +85,7 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|                     options: allOptions | ||||
|                   } | ||||
|                 }) | ||||
|               } | ||||
|               }} | ||||
|               type='text' | ||||
|               content={t('shared.poll.meta.button.vote')} | ||||
|               loading={mutation.isLoading} | ||||
| @@ -94,7 +97,8 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|         return ( | ||||
|           <View style={styles.button}> | ||||
|             <Button | ||||
|               onPress={() => | ||||
|               onPress={() => { | ||||
|                 analytics('timeline_shared_vote_refresh_press') | ||||
|                 mutation.mutate({ | ||||
|                   type: 'updateStatusProperty', | ||||
|                   queryKey, | ||||
| @@ -106,7 +110,7 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|                     type: 'refresh' | ||||
|                   } | ||||
|                 }) | ||||
|               } | ||||
|               }} | ||||
|               type='text' | ||||
|               content={t('shared.poll.meta.button.refresh')} | ||||
|               loading={mutation.isLoading} | ||||
| @@ -199,6 +203,7 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|         key={index} | ||||
|         style={styles.optionContainer} | ||||
|         onPress={() => { | ||||
|           analytics('timeline_shared_vote_option_press') | ||||
|           haptics('Light') | ||||
|           if (poll.multiple) { | ||||
|             setAllOptions(allOptions.map((o, i) => (i === index ? !o : o))) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as Analytics from 'expo-firebase-analytics' | ||||
|  | ||||
| const analytics = (event: string, params?: { [key: string]: string }) => { | ||||
| const analytics = (event: string, params?: { [key: string]: any }) => { | ||||
|   Analytics.logEvent(event, params) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,7 @@ | ||||
| export default { | ||||
|   index: { | ||||
|     localCorrupt: 'Login expired, please login again' | ||||
|   }, | ||||
|   buttons: { | ||||
|     cancel: 'Cancel' | ||||
|   }, | ||||
|   | ||||
| @@ -57,6 +57,7 @@ export default { | ||||
|         application: 'Tooted with {{application}}' | ||||
|       }, | ||||
|       conversation: { | ||||
|         withAccounts: 'With', | ||||
|         delete: { | ||||
|           function: 'Delete direct message' | ||||
|         } | ||||
|   | ||||
| @@ -1,4 +1,7 @@ | ||||
| export default { | ||||
|   index: { | ||||
|     localCorrupt: '登录已过期,请重新登录' | ||||
|   }, | ||||
|   buttons: { | ||||
|     cancel: '取消' | ||||
|   }, | ||||
|   | ||||
| @@ -57,6 +57,7 @@ export default { | ||||
|         application: '发自于 {{application}}' | ||||
|       }, | ||||
|       conversation: { | ||||
|         withAccounts: '与', | ||||
|         delete: { | ||||
|           function: '删除私信' | ||||
|         } | ||||
| @@ -141,7 +142,7 @@ export default { | ||||
|         }, | ||||
|         count: { | ||||
|           voters: '已投{{count}}人 • ', | ||||
|           votes: '{{count}}票 • ' | ||||
|           votes: '已投{{count}}票 • ' | ||||
|         }, | ||||
|         expiration: { | ||||
|           expired: '投票已结束', | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import { useNavigation } from '@react-navigation/native' | ||||
| import TimelineEmpty from '@root/components/Timelines/Timeline/Empty' | ||||
| import { useListsQuery } from '@utils/queryHooks/lists' | ||||
| import React, { useMemo } from 'react' | ||||
| import { StyleSheet } from 'react-native' | ||||
|  | ||||
| const ScreenMeLists: React.FC = () => { | ||||
|   const navigation = useNavigation() | ||||
| @@ -32,11 +31,4 @@ const ScreenMeLists: React.FC = () => { | ||||
|   return <>{children}</> | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   loading: { | ||||
|     flex: 1, | ||||
|     alignItems: 'center' | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ScreenMeLists | ||||
|   | ||||
| @@ -22,7 +22,7 @@ const MyInfo: React.FC<Props> = ({ setData }) => { | ||||
|   return ( | ||||
|     <> | ||||
|       <AccountHeader account={data} limitHeight /> | ||||
|       <AccountInformation account={data} ownAccount /> | ||||
|       <AccountInformation account={data} myInfo /> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| @@ -129,7 +130,12 @@ const ScreenMeSettings: React.FC = () => { | ||||
|               }, | ||||
|               buttonIndex => { | ||||
|                 if (buttonIndex < options.length) { | ||||
|                   analytics('settings_language_press', { | ||||
|                     current: i18n.language, | ||||
|                     new: availableLanguages[buttonIndex] | ||||
|                   }) | ||||
|                   haptics('Success') | ||||
|                   // @ts-ignore | ||||
|                   dispatch(changeLanguage(availableLanguages[buttonIndex])) | ||||
|                   i18n.changeLanguage(availableLanguages[buttonIndex]) | ||||
|                 } | ||||
| @@ -156,15 +162,27 @@ const ScreenMeSettings: React.FC = () => { | ||||
|               buttonIndex => { | ||||
|                 switch (buttonIndex) { | ||||
|                   case 0: | ||||
|                     analytics('settings_appearance_press', { | ||||
|                       current: settingsTheme, | ||||
|                       new: 'auto' | ||||
|                     }) | ||||
|                     haptics('Success') | ||||
|                     dispatch(changeTheme('auto')) | ||||
|                     break | ||||
|                   case 1: | ||||
|                     analytics('settings_appearance_press', { | ||||
|                       current: settingsTheme, | ||||
|                       new: 'light' | ||||
|                     }) | ||||
|                     haptics('Success') | ||||
|                     dispatch(changeTheme('light')) | ||||
|                     setTheme('light') | ||||
|                     break | ||||
|                   case 2: | ||||
|                     analytics('settings_appearance_press', { | ||||
|                       current: settingsTheme, | ||||
|                       new: 'dark' | ||||
|                     }) | ||||
|                     haptics('Success') | ||||
|                     dispatch(changeTheme('dark')) | ||||
|                     setTheme('dark') | ||||
| @@ -192,10 +210,18 @@ const ScreenMeSettings: React.FC = () => { | ||||
|               buttonIndex => { | ||||
|                 switch (buttonIndex) { | ||||
|                   case 0: | ||||
|                     analytics('settings_browser_press', { | ||||
|                       current: settingsBrowser, | ||||
|                       new: 'internal' | ||||
|                     }) | ||||
|                     haptics('Success') | ||||
|                     dispatch(changeBrowser('internal')) | ||||
|                     break | ||||
|                   case 1: | ||||
|                     analytics('settings_browser_press', { | ||||
|                       current: settingsBrowser, | ||||
|                       new: 'external' | ||||
|                     }) | ||||
|                     haptics('Success') | ||||
|                     dispatch(changeBrowser('external')) | ||||
|                     break | ||||
| @@ -220,6 +246,9 @@ const ScreenMeSettings: React.FC = () => { | ||||
|           } | ||||
|           iconBack='ChevronRight' | ||||
|           onPress={async () => { | ||||
|             analytics('settings_cache_press', { | ||||
|               size: cacheSize ? prettyBytes(cacheSize) : 'empty' | ||||
|             }) | ||||
|             await CacheManager.clearCache() | ||||
|             haptics('Success') | ||||
|             setCacheSize(0) | ||||
| @@ -237,7 +266,10 @@ const ScreenMeSettings: React.FC = () => { | ||||
|             /> | ||||
|           } | ||||
|           iconBack='ChevronRight' | ||||
|           onPress={() => Linking.openURL('https://www.patreon.com/xmflsct')} | ||||
|           onPress={() => { | ||||
|             analytics('settings_support_press') | ||||
|             Linking.openURL('https://www.patreon.com/xmflsct') | ||||
|           }} | ||||
|         /> | ||||
|         <MenuRow | ||||
|           title={t('content.review.heading')} | ||||
| @@ -249,11 +281,12 @@ const ScreenMeSettings: React.FC = () => { | ||||
|             /> | ||||
|           } | ||||
|           iconBack='ChevronRight' | ||||
|           onPress={() => | ||||
|           onPress={() => { | ||||
|             analytics('settings_review_press') | ||||
|             StoreReview.isAvailableAsync().then(() => | ||||
|               StoreReview.requestReview() | ||||
|             ) | ||||
|           } | ||||
|           }} | ||||
|         /> | ||||
|       </MenuContainer> | ||||
|       <MenuContainer> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { HeaderCenter, HeaderLeft } from '@components/Header' | ||||
| import { StackScreenProps } from '@react-navigation/stack' | ||||
| import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Platform, StyleSheet } from 'react-native' | ||||
| @@ -7,7 +8,10 @@ import ScreenMeSwitchRoot from './Switch/Root' | ||||
|  | ||||
| const Stack = createNativeStackNavigator() | ||||
|  | ||||
| const ScreenMeSwitch: React.FC = ({ navigation }) => { | ||||
| const ScreenMeSwitch: React.FC<StackScreenProps< | ||||
|   Nav.MeStackParamList, | ||||
|   'Screen-Me-Switch' | ||||
| >> = ({ navigation }) => { | ||||
|   const { t } = useTranslation() | ||||
|   return ( | ||||
|     <Stack.Navigator | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import ComponentInstance from '@components/Instance' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| @@ -47,6 +48,7 @@ const AccountButton: React.FC<Props> = ({ | ||||
|         disabled ? ' ✓' : '' | ||||
|       }`} | ||||
|       onPress={() => { | ||||
|         analytics('switch_existing_press') | ||||
|         dispatch(localUpdateActiveIndex(index)) | ||||
|         queryClient.clear() | ||||
|         navigation.goBack() | ||||
|   | ||||
| @@ -34,7 +34,7 @@ const ScreenNotifications: React.FC = () => { | ||||
|         } | ||||
|       </Stack.Screen> | ||||
|  | ||||
|       {sharedScreens(Stack)} | ||||
|       {sharedScreens(Stack as any)} | ||||
|     </Stack.Navigator> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import BottomSheet from '@components/BottomSheet' | ||||
| import { HeaderRight } from '@components/Header' | ||||
| import Timeline from '@components/Timelines/Timeline' | ||||
| @@ -49,7 +50,12 @@ const ScreenSharedAccount: React.FC<SharedAccountProp> = ({ | ||||
|         headerRight: () => ( | ||||
|           <HeaderRight | ||||
|             content='MoreHorizontal' | ||||
|             onPress={() => setBottomSheetVisible(true)} | ||||
|             onPress={() => { | ||||
|               analytics('bottomsheet_open_press', { | ||||
|                 page: 'account' | ||||
|               }) | ||||
|               setBottomSheetVisible(true) | ||||
|             }} | ||||
|           /> | ||||
|         ) | ||||
|       }) | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import GracefullyImage from '@components/GracefullyImage' | ||||
| import Icon from '@components/Icon' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| @@ -22,7 +24,9 @@ export interface Props { | ||||
|  | ||||
| const AccountAttachments = React.memo( | ||||
|   ({ account }: Props) => { | ||||
|     const navigation = useNavigation() | ||||
|     const navigation = useNavigation< | ||||
|       StackNavigationProp<Nav.LocalStackParamList> | ||||
|     >() | ||||
|     const { theme } = useTheme() | ||||
|  | ||||
|     const width = | ||||
| @@ -58,9 +62,11 @@ const AccountAttachments = React.memo( | ||||
|         if (index === 3) { | ||||
|           return ( | ||||
|             <Pressable | ||||
|               onPress={() => | ||||
|                 navigation.push('Screen-Shared-Attachments', { account }) | ||||
|               } | ||||
|               onPress={() => { | ||||
|                 analytics('account_attachment_more_press') | ||||
|                 account && | ||||
|                   navigation.push('Screen-Shared-Attachments', { account }) | ||||
|               }} | ||||
|               children={ | ||||
|                 <View | ||||
|                   style={{ | ||||
| @@ -92,9 +98,10 @@ const AccountAttachments = React.memo( | ||||
|               blurhash={item.media_attachments[0].blurhash} | ||||
|               dimension={{ width: width, height: width }} | ||||
|               style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }} | ||||
|               onPress={() => | ||||
|               onPress={() => { | ||||
|                 analytics('account_attachment_item_press') | ||||
|                 navigation.push('Screen-Shared-Toot', { toot: item }) | ||||
|               } | ||||
|               }} | ||||
|             /> | ||||
|           ) | ||||
|         } | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import { getLocalAccount } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback } from 'react' | ||||
| import { StyleSheet, View } from 'react-native' | ||||
| import { useSelector } from 'react-redux' | ||||
| import { Placeholder, Fade } from 'rn-placeholder' | ||||
| import AccountInformationAccount from './Information/Account' | ||||
| import AccountInformationActions from './Information/Actions' | ||||
| @@ -15,13 +17,11 @@ import AccountInformationSwitch from './Information/Switch' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | undefined | ||||
|   ownAccount?: boolean | ||||
|   myInfo?: boolean // Showing from my info page | ||||
| } | ||||
|  | ||||
| const AccountInformation: React.FC<Props> = ({ | ||||
|   account, | ||||
|   ownAccount = false | ||||
| }) => { | ||||
| const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => { | ||||
|   const ownAccount = account?.id === useSelector(getLocalAccount)?.id | ||||
|   const { mode, theme } = useTheme() | ||||
|  | ||||
|   const animation = useCallback( | ||||
| @@ -35,21 +35,24 @@ const AccountInformation: React.FC<Props> = ({ | ||||
|     <View style={styles.base}> | ||||
|       <Placeholder Animation={animation}> | ||||
|         <View style={styles.avatarAndActions}> | ||||
|           <AccountInformationAvatar account={account} /> | ||||
|           <AccountInformationAvatar account={account} myInfo={myInfo} /> | ||||
|           <View style={styles.actions}> | ||||
|             {ownAccount ? ( | ||||
|             {myInfo ? ( | ||||
|               <AccountInformationSwitch /> | ||||
|             ) : ( | ||||
|               <AccountInformationActions account={account} /> | ||||
|               <AccountInformationActions | ||||
|                 account={account} | ||||
|                 ownAccount={ownAccount} | ||||
|               /> | ||||
|             )} | ||||
|           </View> | ||||
|         </View> | ||||
|  | ||||
|         <AccountInformationName account={account} /> | ||||
|  | ||||
|         <AccountInformationAccount account={account} ownAccount={ownAccount} /> | ||||
|         <AccountInformationAccount account={account} myInfo={myInfo} /> | ||||
|  | ||||
|         {!ownAccount ? ( | ||||
|         {!myInfo ? ( | ||||
|           <> | ||||
|             {account?.fields && account.fields.length > 0 ? ( | ||||
|               <AccountInformationFields account={account} /> | ||||
| @@ -64,7 +67,7 @@ const AccountInformation: React.FC<Props> = ({ | ||||
|           </> | ||||
|         ) : null} | ||||
|  | ||||
|         <AccountInformationStats account={account} ownAccount={ownAccount} /> | ||||
|         <AccountInformationStats account={account} myInfo={myInfo} /> | ||||
|       </Placeholder> | ||||
|     </View> | ||||
|   ) | ||||
|   | ||||
| @@ -9,13 +9,10 @@ import { PlaceholderLine } from 'rn-placeholder' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | undefined | ||||
|   ownAccount?: boolean | ||||
|   myInfo: boolean | ||||
| } | ||||
|  | ||||
| const AccountInformationAccount: React.FC<Props> = ({ | ||||
|   account, | ||||
|   ownAccount | ||||
| }) => { | ||||
| const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const localUri = useSelector(getLocalUri) | ||||
| @@ -45,7 +42,7 @@ const AccountInformationAccount: React.FC<Props> = ({ | ||||
|     } | ||||
|   }, [account?.moved]) | ||||
|  | ||||
|   if (account || (ownAccount && localAccount !== undefined)) { | ||||
|   if (account || (myInfo && localAccount !== undefined)) { | ||||
|     return ( | ||||
|       <View | ||||
|         style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]} | ||||
| @@ -60,8 +57,8 @@ const AccountInformationAccount: React.FC<Props> = ({ | ||||
|           ]} | ||||
|           selectable | ||||
|         > | ||||
|           @{ownAccount ? localAccount?.acct : account?.acct} | ||||
|           {ownAccount ? `@${localUri}` : null} | ||||
|           @{myInfo ? localAccount?.acct : account?.acct} | ||||
|           {myInfo ? `@${localUri}` : null} | ||||
|         </Text> | ||||
|         {movedContent} | ||||
|         {account?.locked ? ( | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import { RelationshipOutgoing } from '@components/Relationship' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { useRelationshipQuery } from '@utils/queryHooks/relationship' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import React from 'react' | ||||
| @@ -9,22 +11,25 @@ import { StyleSheet } from 'react-native' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | undefined | ||||
|   ownAccount: boolean | ||||
| } | ||||
|  | ||||
| const GoToMoved = ({ account }: { account: Mastodon.Account }) => { | ||||
| const GoToMoved = ({ accountMoved }: { accountMoved: Mastodon.Account }) => { | ||||
|   const { t } = useTranslation('sharedAccount') | ||||
|   const navigation = useNavigation() | ||||
|   const query = useRelationshipQuery({ id: account.id }) | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|  | ||||
|   return query.data && !query.data.blocked_by ? ( | ||||
|   return ( | ||||
|     <Button | ||||
|       type='text' | ||||
|       content={t('content.moved')} | ||||
|       onPress={() => | ||||
|         navigation.push('Screen-Shared-Account', { account: account.moved }) | ||||
|       } | ||||
|       onPress={() => { | ||||
|         analytics('account_gotomoved_press') | ||||
|         navigation.push('Screen-Shared-Account', { account: accountMoved }) | ||||
|       }} | ||||
|     /> | ||||
|   ) : null | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const Conversation = ({ account }: { account: Mastodon.Account }) => { | ||||
| @@ -37,26 +42,30 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => { | ||||
|       type='icon' | ||||
|       content='Mail' | ||||
|       style={styles.actionConversation} | ||||
|       onPress={() => | ||||
|       onPress={() => { | ||||
|         analytics('account_DM_press') | ||||
|         navigation.navigate('Screen-Shared-Compose', { | ||||
|           type: 'conversation', | ||||
|           incomingStatus: { account } | ||||
|           accts: [account.acct] | ||||
|         }) | ||||
|       } | ||||
|       }} | ||||
|     /> | ||||
|   ) : null | ||||
| } | ||||
|  | ||||
| const AccountInformationActions: React.FC<Props> = ({ account }) => { | ||||
| const AccountInformationActions: React.FC<Props> = ({ | ||||
|   account, | ||||
|   ownAccount | ||||
| }) => { | ||||
|   return account && account.id ? ( | ||||
|     account.moved ? ( | ||||
|       <GoToMoved account={account} /> | ||||
|     ) : ( | ||||
|       <GoToMoved accountMoved={account.moved} /> | ||||
|     ) : !ownAccount ? ( | ||||
|       <> | ||||
|         <Conversation account={account} /> | ||||
|         <RelationshipOutgoing id={account.id} /> | ||||
|       </> | ||||
|     ) | ||||
|     ) : null | ||||
|   ) : null | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,20 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import GracefullyImage from '@components/GracefullyImage' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import React, { useMemo } from 'react' | ||||
| import { StyleSheet } from 'react-native' | ||||
| import { Pressable, StyleSheet } from 'react-native' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | undefined | ||||
|   myInfo: boolean | ||||
| } | ||||
|  | ||||
| const AccountInformationAvatar: React.FC<Props> = ({ account }) => { | ||||
| const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => { | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   const dimension = useMemo( | ||||
|     () => ({ | ||||
|       width: StyleConstants.Avatar.L, | ||||
| @@ -17,11 +24,21 @@ const AccountInformationAvatar: React.FC<Props> = ({ account }) => { | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <GracefullyImage | ||||
|       style={styles.base} | ||||
|       uri={{ original: account?.avatar }} | ||||
|       dimension={dimension} | ||||
|     /> | ||||
|     <Pressable | ||||
|       disabled={!myInfo} | ||||
|       onPress={() => { | ||||
|         analytics('account_avatar_press') | ||||
|         myInfo && | ||||
|           account && | ||||
|           navigation.push('Screen-Shared-Account', { account }) | ||||
|       }} | ||||
|     > | ||||
|       <GracefullyImage | ||||
|         style={styles.base} | ||||
|         uri={{ original: account?.avatar }} | ||||
|         dimension={dimension} | ||||
|       /> | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { useTheme } from '@root/utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| @@ -8,11 +10,13 @@ import { PlaceholderLine } from 'rn-placeholder' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | undefined | ||||
|   ownAccount?: boolean | ||||
|   myInfo: boolean | ||||
| } | ||||
|  | ||||
| const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => { | ||||
|   const navigation = useNavigation() | ||||
| const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => { | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   const { theme } = useTheme() | ||||
|   const { t } = useTranslation('sharedAccount') | ||||
|  | ||||
| @@ -24,9 +28,12 @@ const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => { | ||||
|           children={t('content.summary.statuses_count', { | ||||
|             count: account?.statuses_count || 0 | ||||
|           })} | ||||
|           onPress={() => | ||||
|             ownAccount && navigation.push('Screen-Shared-Account', { account }) | ||||
|           } | ||||
|           onPress={() => { | ||||
|             analytics('account_stats_toots_press', { | ||||
|               count: account.statuses_count | ||||
|             }) | ||||
|             myInfo && navigation.push('Screen-Shared-Account', { account }) | ||||
|           }} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <PlaceholderLine | ||||
| @@ -43,12 +50,15 @@ const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => { | ||||
|           children={t('content.summary.following_count', { | ||||
|             count: account?.following_count || 0 | ||||
|           })} | ||||
|           onPress={() => | ||||
|           onPress={() => { | ||||
|             analytics('account_stats_following_press', { | ||||
|               count: account.following_count | ||||
|             }) | ||||
|             navigation.push('Screen-Shared-Relationships', { | ||||
|               account, | ||||
|               initialType: 'following' | ||||
|             }) | ||||
|           } | ||||
|           }} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <PlaceholderLine | ||||
| @@ -65,12 +75,15 @@ const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => { | ||||
|           children={t('content.summary.followers_count', { | ||||
|             count: account?.followers_count || 0 | ||||
|           })} | ||||
|           onPress={() => | ||||
|           onPress={() => { | ||||
|             analytics('account_stats_followers_press', { | ||||
|               count: account.followers_count | ||||
|             }) | ||||
|             navigation.push('Screen-Shared-Relationships', { | ||||
|               account, | ||||
|               initialType: 'followers' | ||||
|             }) | ||||
|           } | ||||
|           }} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <PlaceholderLine | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import { ParseHTML } from '@components/Parse' | ||||
| @@ -108,14 +109,17 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({ | ||||
|                         : theme.background | ||||
|                     } | ||||
|                   ]} | ||||
|                   onPress={() => | ||||
|                   onPress={() => { | ||||
|                     analytics('accnouncement_reaction_press', { | ||||
|                       current: reaction.me | ||||
|                     }) | ||||
|                     mutation.mutate({ | ||||
|                       id: item.id, | ||||
|                       type: 'reaction', | ||||
|                       name: reaction.name, | ||||
|                       me: reaction.me | ||||
|                     }) | ||||
|                   } | ||||
|                   }} | ||||
|                 > | ||||
|                   {reaction.url ? ( | ||||
|                     <Image | ||||
| @@ -153,13 +157,14 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({ | ||||
|             } | ||||
|             loading={mutation.isLoading} | ||||
|             disabled={item.read} | ||||
|             onPress={() => | ||||
|             onPress={() => { | ||||
|               analytics('accnouncement_read_press') | ||||
|               !item.read && | ||||
|               mutation.mutate({ | ||||
|                 id: item.id, | ||||
|                 type: 'dismiss' | ||||
|               }) | ||||
|             } | ||||
|                 mutation.mutate({ | ||||
|                   id: item.id, | ||||
|                   type: 'dismiss' | ||||
|                 }) | ||||
|             }} | ||||
|           /> | ||||
|         </View> | ||||
|       </View> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import { HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import haptics from '@root/components/haptics' | ||||
| import { store } from '@root/store' | ||||
| @@ -93,20 +94,11 @@ const Compose: React.FC<SharedComposeProp> = ({ | ||||
|         }) | ||||
|         break | ||||
|       case 'reply': | ||||
|         const actualStatus = | ||||
|           params.incomingStatus.reblog || params.incomingStatus | ||||
|         formatText({ | ||||
|           textInput: 'text', | ||||
|           composeDispatch, | ||||
|           content: `@${actualStatus.account.acct} `, | ||||
|           disableDebounce: true | ||||
|         }) | ||||
|         break | ||||
|       case 'conversation': | ||||
|         formatText({ | ||||
|           textInput: 'text', | ||||
|           composeDispatch, | ||||
|           content: `@${params.incomingStatus.account.acct} `, | ||||
|           content: params.accts.map(acct => `@${acct}`).join(' ') + ' ', | ||||
|           disableDebounce: true | ||||
|         }) | ||||
|         break | ||||
| @@ -123,23 +115,32 @@ const Compose: React.FC<SharedComposeProp> = ({ | ||||
|         type='text' | ||||
|         content={t('heading.left.button')} | ||||
|         onPress={() => { | ||||
|           analytics('compose_header_back_press') | ||||
|           if ( | ||||
|             totalTextCount === 0 && | ||||
|             composeState.attachments.uploads.length === 0 && | ||||
|             composeState.poll.active === false | ||||
|           ) { | ||||
|             analytics('compose_header_back_empty') | ||||
|             navigation.goBack() | ||||
|             return | ||||
|           } else { | ||||
|             analytics('compose_header_back_state_occupied') | ||||
|             Alert.alert(t('heading.left.alert.title'), undefined, [ | ||||
|               { | ||||
|                 text: t('heading.left.alert.buttons.exit'), | ||||
|                 style: 'destructive', | ||||
|                 onPress: () => navigation.goBack() | ||||
|                 onPress: () => { | ||||
|                   analytics('compose_header_back_occupied_confirm') | ||||
|                   navigation.goBack() | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 text: t('heading.left.alert.buttons.continue'), | ||||
|                 style: 'cancel' | ||||
|                 style: 'cancel', | ||||
|                 onPress: () => { | ||||
|                   analytics('compose_header_back_occupied_cancel') | ||||
|                 } | ||||
|               } | ||||
|             ]) | ||||
|           } | ||||
| @@ -174,6 +175,7 @@ const Compose: React.FC<SharedComposeProp> = ({ | ||||
|             : t('heading.right.button.default') | ||||
|         } | ||||
|         onPress={() => { | ||||
|           analytics('compose_header_post_press') | ||||
|           composeDispatch({ type: 'posting', payload: true }) | ||||
|  | ||||
|           composePost(params, composeState) | ||||
| @@ -186,13 +188,18 @@ const Compose: React.FC<SharedComposeProp> = ({ | ||||
|               ] | ||||
|               queryClient.invalidateQueries(queryKey) | ||||
|  | ||||
|               if (params?.queryKey && params.queryKey[1].page === 'Toot') { | ||||
|                 queryClient.invalidateQueries(params.queryKey) | ||||
|               switch (params?.type) { | ||||
|                 case 'edit': | ||||
|                 case 'reply': | ||||
|                   if (params?.queryKey && params.queryKey[1].page === 'Toot') { | ||||
|                     queryClient.invalidateQueries(params.queryKey) | ||||
|                   } | ||||
|                   break | ||||
|               } | ||||
|               navigation.goBack() | ||||
|             }) | ||||
|             .catch(error => { | ||||
|               // Sentry.Native.captureException(error) | ||||
|               Sentry.Native.captureException(error) | ||||
|               haptics('Error') | ||||
|               composeDispatch({ type: 'posting', payload: false }) | ||||
|               Alert.alert(t('heading.right.alert.title'), undefined, [ | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import client from '@api/client' | ||||
| import analytics from '@components/analytics' | ||||
| import haptics from '@components/haptics' | ||||
| import { HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import React, { | ||||
| @@ -87,6 +88,7 @@ const ComposeEditAttachment: React.FC<Props> = ({ | ||||
|         content={t('content.editAttachment.header.right.button')} | ||||
|         loading={isSubmitting} | ||||
|         onPress={() => { | ||||
|           analytics('editattachment_confirm_press') | ||||
|           if (!altText && focus.current.x === 0 && focus.current.y === 0) { | ||||
|             navigation.goBack() | ||||
|             return | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Icon from '@components/Icon' | ||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -28,6 +29,9 @@ const ComposeActions: React.FC = () => { | ||||
|     if (composeState.poll.active) return | ||||
|  | ||||
|     if (composeState.attachments.uploads.length < 4) { | ||||
|       analytics('compose_actions_attachment_press', { | ||||
|         count: composeState.attachments.uploads.length | ||||
|       }) | ||||
|       return await addAttachment({ | ||||
|         composeDispatch, | ||||
|         showActionSheetWithOptions | ||||
| @@ -46,6 +50,9 @@ const ComposeActions: React.FC = () => { | ||||
|   }, [composeState.poll.active, composeState.attachments.uploads]) | ||||
|   const pollOnPress = useCallback(() => { | ||||
|     if (!composeState.attachments.uploads.length) { | ||||
|       analytics('compose_actions_poll_press', { | ||||
|         current: composeState.poll.active | ||||
|       }) | ||||
|       layoutAnimation() | ||||
|       composeDispatch({ | ||||
|         type: 'poll', | ||||
| @@ -86,24 +93,43 @@ const ComposeActions: React.FC = () => { | ||||
|         buttonIndex => { | ||||
|           switch (buttonIndex) { | ||||
|             case 0: | ||||
|               analytics('compose_actions_visibility_press', { | ||||
|                 current: composeState.visibility, | ||||
|                 new: 'public' | ||||
|               }) | ||||
|               composeDispatch({ type: 'visibility', payload: 'public' }) | ||||
|               break | ||||
|             case 1: | ||||
|               analytics('compose_actions_visibility_press', { | ||||
|                 current: composeState.visibility, | ||||
|                 new: 'unlisted' | ||||
|               }) | ||||
|               composeDispatch({ type: 'visibility', payload: 'unlisted' }) | ||||
|               break | ||||
|             case 2: | ||||
|               analytics('compose_actions_visibility_press', { | ||||
|                 current: composeState.visibility, | ||||
|                 new: 'private' | ||||
|               }) | ||||
|               composeDispatch({ type: 'visibility', payload: 'private' }) | ||||
|               break | ||||
|             case 3: | ||||
|               analytics('compose_actions_visibility_press', { | ||||
|                 current: composeState.visibility, | ||||
|                 new: 'direct' | ||||
|               }) | ||||
|               composeDispatch({ type: 'visibility', payload: 'direct' }) | ||||
|               break | ||||
|           } | ||||
|         } | ||||
|       ) | ||||
|     } | ||||
|   }, []) | ||||
|   }, [composeState.visibility]) | ||||
|  | ||||
|   const spoilerOnPress = useCallback(() => { | ||||
|     analytics('compose_actions_spoiler_press', { | ||||
|       current: composeState.spoiler.active | ||||
|     }) | ||||
|     if (composeState.spoiler.active) { | ||||
|       composeState.textInputFocus.refs.text.current?.focus() | ||||
|     } | ||||
| @@ -124,20 +150,15 @@ const ComposeActions: React.FC = () => { | ||||
|     } | ||||
|   }, [composeState.emoji.active, composeState.emoji.emojis]) | ||||
|   const emojiOnPress = useCallback(() => { | ||||
|     analytics('compose_actions_emojis_press', { | ||||
|       current: composeState.emoji.active | ||||
|     }) | ||||
|     if (composeState.emoji.emojis) { | ||||
|       if (composeState.emoji.active) { | ||||
|         layoutAnimation() | ||||
|         composeDispatch({ | ||||
|           type: 'emoji', | ||||
|           payload: { ...composeState.emoji, active: false } | ||||
|         }) | ||||
|       } else { | ||||
|         layoutAnimation() | ||||
|         composeDispatch({ | ||||
|           type: 'emoji', | ||||
|           payload: { ...composeState.emoji, active: true } | ||||
|         }) | ||||
|       } | ||||
|       layoutAnimation() | ||||
|       composeDispatch({ | ||||
|         type: 'emoji', | ||||
|         payload: { ...composeState.emoji, active: !composeState.emoji.active } | ||||
|       }) | ||||
|     } | ||||
|   }, [composeState.emoji.active, composeState.emoji.emojis]) | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| @@ -39,14 +40,15 @@ const ComposeAttachments: React.FC = () => { | ||||
|   const flatListRef = useRef<FlatList>(null) | ||||
|   let prevOffsets = useRef<number[]>() | ||||
|  | ||||
|   const sensitiveOnPress = useCallback( | ||||
|     () => | ||||
|       composeDispatch({ | ||||
|         type: 'attachments/sensitive', | ||||
|         payload: { sensitive: !composeState.attachments.sensitive } | ||||
|       }), | ||||
|     [composeState.attachments.sensitive] | ||||
|   ) | ||||
|   const sensitiveOnPress = useCallback(() => { | ||||
|     analytics('compose_attachment_sensitive_press', { | ||||
|       current: composeState.attachments.sensitive | ||||
|     }) | ||||
|     composeDispatch({ | ||||
|       type: 'attachments/sensitive', | ||||
|       payload: { sensitive: !composeState.attachments.sensitive } | ||||
|     }) | ||||
|   }, [composeState.attachments.sensitive]) | ||||
|  | ||||
|   const calculateWidth = useCallback(item => { | ||||
|     if (item.local) { | ||||
| @@ -158,6 +160,7 @@ const ComposeAttachments: React.FC = () => { | ||||
|                 round | ||||
|                 overlay | ||||
|                 onPress={() => { | ||||
|                   analytics('compose_attachment_delete') | ||||
|                   layoutAnimation() | ||||
|                   composeDispatch({ | ||||
|                     type: 'attachment/delete', | ||||
| @@ -172,11 +175,12 @@ const ComposeAttachments: React.FC = () => { | ||||
|                 spacing='M' | ||||
|                 round | ||||
|                 overlay | ||||
|                 onPress={() => | ||||
|                 onPress={() => { | ||||
|                   analytics('compose_attachment_edit') | ||||
|                   navigation.navigate('Screen-Shared-Compose-EditAttachment', { | ||||
|                     index | ||||
|                   }) | ||||
|                 } | ||||
|                 }} | ||||
|               /> | ||||
|             </View> | ||||
|           )} | ||||
| @@ -196,9 +200,10 @@ const ComposeAttachments: React.FC = () => { | ||||
|             backgroundColor: theme.backgroundOverlay | ||||
|           } | ||||
|         ]} | ||||
|         onPress={async () => | ||||
|         onPress={async () => { | ||||
|           analytics('compose_attachment_add_container_press') | ||||
|           await addAttachment({ composeDispatch, showActionSheetWithOptions }) | ||||
|         } | ||||
|         }} | ||||
|       > | ||||
|         <Button | ||||
|           type='icon' | ||||
| @@ -206,9 +211,10 @@ const ComposeAttachments: React.FC = () => { | ||||
|           spacing='M' | ||||
|           round | ||||
|           overlay | ||||
|           onPress={async () => | ||||
|           onPress={async () => { | ||||
|             analytics('compose_attachment_add_button_press') | ||||
|             await addAttachment({ composeDispatch, showActionSheetWithOptions }) | ||||
|           } | ||||
|           }} | ||||
|           style={{ | ||||
|             position: 'absolute', | ||||
|             top: | ||||
|   | ||||
| @@ -12,10 +12,12 @@ import { | ||||
| } from 'react-native' | ||||
| import ComposeContext from '../../utils/createContext' | ||||
| import updateText from '../../updateText' | ||||
| import analytics from '@components/analytics' | ||||
|  | ||||
| const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => { | ||||
|   const { composeState, composeDispatch } = useContext(ComposeContext) | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('compose_emoji_add') | ||||
|     updateText({ | ||||
|       composeState, | ||||
|       composeDispatch, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import Icon from '@components/Icon' | ||||
| import { MenuRow } from '@components/Menu' | ||||
| @@ -79,6 +80,7 @@ const ComposePoll: React.FC = () => { | ||||
|         <View style={styles.firstButton}> | ||||
|           <Button | ||||
|             onPress={() => { | ||||
|               analytics('compose_poll_reduce_press') | ||||
|               total > 2 && | ||||
|                 composeDispatch({ | ||||
|                   type: 'poll', | ||||
| @@ -93,6 +95,7 @@ const ComposePoll: React.FC = () => { | ||||
|         </View> | ||||
|         <Button | ||||
|           onPress={() => { | ||||
|             analytics('compose_poll_increase_press') | ||||
|             total < 4 && | ||||
|               composeDispatch({ | ||||
|                 type: 'poll', | ||||
| @@ -122,12 +125,18 @@ const ComposePoll: React.FC = () => { | ||||
|               ], | ||||
|               cancelButtonIndex: 2 | ||||
|             }, | ||||
|             index => | ||||
|               index < 2 && | ||||
|               composeDispatch({ | ||||
|                 type: 'poll', | ||||
|                 payload: { multiple: index === 1 } | ||||
|               }) | ||||
|             index => { | ||||
|               if (index < 2) { | ||||
|                 analytics('compose_poll_expiration_press', { | ||||
|                   current: multiple, | ||||
|                   new: index === 1 | ||||
|                 }) | ||||
|                 composeDispatch({ | ||||
|                   type: 'poll', | ||||
|                   payload: { multiple: index === 1 } | ||||
|                 }) | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|         } | ||||
|         iconBack='ChevronRight' | ||||
| @@ -155,12 +164,18 @@ const ComposePoll: React.FC = () => { | ||||
|               ], | ||||
|               cancelButtonIndex: 7 | ||||
|             }, | ||||
|             index => | ||||
|               index < 7 && | ||||
|               composeDispatch({ | ||||
|                 type: 'poll', | ||||
|                 payload: { expire: expirations[index] } | ||||
|               }) | ||||
|             index => { | ||||
|               if (index < 7) { | ||||
|                 analytics('compose_poll_expiration_press', { | ||||
|                   current: expire, | ||||
|                   new: expirations[index] | ||||
|                 }) | ||||
|                 composeDispatch({ | ||||
|                   type: 'poll', | ||||
|                   payload: { expire: expirations[index] } | ||||
|                 }) | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|         }} | ||||
|         iconBack='ChevronRight' | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { Alert, Linking } from 'react-native' | ||||
| import { ComposeAction } from '../../utils/types' | ||||
| import { ActionSheetOptions } from '@expo/react-native-action-sheet' | ||||
| import i18next from 'i18next' | ||||
| import analytics from '@components/analytics' | ||||
|  | ||||
| export interface Props { | ||||
|   composeDispatch: Dispatch<ComposeAction> | ||||
| @@ -160,14 +161,23 @@ const addAttachment = async ({ | ||||
|                   'sharedCompose:content.root.actions.attachment.actions.library.alert.buttons.cancel' | ||||
|                 ), | ||||
|                 style: 'cancel', | ||||
|                 onPress: () => {} | ||||
|                 onPress: () => { | ||||
|                   analytics('compose_addattachment_medialibrary_nopermission', { | ||||
|                     action: 'cancel' | ||||
|                   }) | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 text: i18next.t( | ||||
|                   'sharedCompose:content.root.actions.attachment.actions.library.alert.buttons.settings' | ||||
|                 ), | ||||
|                 style: 'default', | ||||
|                 onPress: () => Linking.openURL('app-settings:') | ||||
|                 onPress: () => { | ||||
|                   analytics('compose_addattachment_medialibrary_nopermission', { | ||||
|                     action: 'settings' | ||||
|                   }) | ||||
|                   Linking.openURL('app-settings:') | ||||
|                 } | ||||
|               } | ||||
|             ] | ||||
|           ) | ||||
| @@ -197,14 +207,23 @@ const addAttachment = async ({ | ||||
|                   'sharedCompose:content.root.actions.attachment.actions.photo.alert.buttons.cancel' | ||||
|                 ), | ||||
|                 style: 'cancel', | ||||
|                 onPress: () => {} | ||||
|                 onPress: () => { | ||||
|                   analytics('compose_addattachment_camera_nopermission', { | ||||
|                     action: 'cancel' | ||||
|                   }) | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 text: i18next.t( | ||||
|                   'sharedCompose:content.root.actions.attachment.actions.photo.alert.buttons.settings' | ||||
|                 ), | ||||
|                 style: 'default', | ||||
|                 onPress: () => Linking.openURL('app-settings:') | ||||
|                 onPress: () => { | ||||
|                   analytics('compose_addattachment_camera_nopermission', { | ||||
|                     action: 'settings' | ||||
|                   }) | ||||
|                   Linking.openURL('app-settings:') | ||||
|                 } | ||||
|               } | ||||
|             ] | ||||
|           ) | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import React, { useContext } from 'react' | ||||
| import { StyleSheet, View } from 'react-native' | ||||
| import { useSelector } from 'react-redux' | ||||
| import ComposeContext from '../utils/createContext' | ||||
| import PostingAs from './Header/PostingAs' | ||||
| import ComposePostingAs from './Header/PostingAs' | ||||
| import ComposeSpoilerInput from './Header/SpoilerInput' | ||||
| import ComposeTextInput from './Header/TextInput' | ||||
|  | ||||
| @@ -22,10 +22,7 @@ const ComposeRootHeader: React.FC = () => { | ||||
|         localInstances.length && | ||||
|         localInstances.length > 1 && ( | ||||
|           <View style={styles.postingAs}> | ||||
|             <PostingAs | ||||
|               id={localInstances[localActiveIndex].account.id} | ||||
|               domain={localInstances[localActiveIndex].uri} | ||||
|             /> | ||||
|             <ComposePostingAs /> | ||||
|           </View> | ||||
|         )} | ||||
|       {composeState.spoiler.active ? <ComposeSpoilerInput /> : null} | ||||
|   | ||||
| @@ -1,39 +1,30 @@ | ||||
| import { useAccountQuery } from '@utils/queryHooks/account' | ||||
| import { InstanceLocal } from '@utils/slices/instancesSlice' | ||||
| import { getLocalAccount, getLocalUri } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { StyleSheet, Text } from 'react-native' | ||||
| import { Chase } from 'react-native-animated-spinkit' | ||||
| import { useSelector } from 'react-redux' | ||||
|  | ||||
| const ComposePostingAs: React.FC<{ | ||||
|   id: Mastodon.Account['id'] | ||||
|   domain: InstanceLocal['url'] | ||||
| }> = ({ id, domain }) => { | ||||
|   const { t } = useTranslation('sharedCompose') | ||||
|   const { theme } = useTheme() | ||||
| const ComposePostingAs = React.memo( | ||||
|   () => { | ||||
|     const { t } = useTranslation('sharedCompose') | ||||
|     const { theme } = useTheme() | ||||
|  | ||||
|   const { data, status } = useAccountQuery({ id }) | ||||
|     const localAccount = useSelector(getLocalAccount) | ||||
|     const localUri = useSelector(getLocalUri) | ||||
|  | ||||
|   switch (status) { | ||||
|     case 'loading': | ||||
|       return ( | ||||
|         <Chase | ||||
|           size={StyleConstants.Font.LineHeight.M - 2} | ||||
|           color={theme.secondary} | ||||
|         /> | ||||
|       ) | ||||
|     case 'success': | ||||
|       return ( | ||||
|         <Text style={[styles.text, { color: theme.secondary }]}> | ||||
|           {t('content.root.header.postingAs', { acct: data?.acct, domain })} | ||||
|         </Text> | ||||
|       ) | ||||
|     default: | ||||
|       return null | ||||
|   } | ||||
| } | ||||
|     return ( | ||||
|       <Text style={[styles.text, { color: theme.secondary }]}> | ||||
|         {t('content.root.header.postingAs', { | ||||
|           acct: localAccount?.acct, | ||||
|           domain: localUri | ||||
|         })} | ||||
|       </Text> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   text: { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import ComponentAccount from '@components/Account' | ||||
| import analytics from '@components/analytics' | ||||
| import haptics from '@components/haptics' | ||||
| import ComponentHashtag from '@components/Hashtag' | ||||
| import React, { Dispatch, useCallback } from 'react' | ||||
| @@ -16,6 +17,9 @@ const ComposeRootSuggestion = React.memo( | ||||
|     composeDispatch: Dispatch<ComposeAction> | ||||
|   }) => { | ||||
|     const onPress = useCallback(() => { | ||||
|       analytics('compose_suggestion_press', { | ||||
|         type: item.acct ? 'account' : 'hashtag' | ||||
|       }) | ||||
|       const focusedInput = composeState.textInputFocus.current | ||||
|       updateText({ | ||||
|         composeState: { | ||||
| @@ -36,9 +40,9 @@ const ComposeRootSuggestion = React.memo( | ||||
|     }, []) | ||||
|  | ||||
|     return item.acct ? ( | ||||
|       <ComponentAccount account={item} onPress={onPress} /> | ||||
|       <ComponentAccount account={item} onPress={onPress} origin='suggestion' /> | ||||
|     ) : ( | ||||
|       <ComponentHashtag tag={item} onPress={onPress} /> | ||||
|       <ComponentHashtag hashtag={item} onPress={onPress} origin='suggestion' /> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
|   | ||||
| @@ -3,51 +3,50 @@ import { getLocalAccount } from '@utils/slices/instancesSlice' | ||||
| import composeInitialState from './initialState' | ||||
| import { ComposeState } from './types' | ||||
|  | ||||
| export interface Props { | ||||
|   type: 'reply' | 'conversation' | 'edit' | ||||
|   incomingStatus: Mastodon.Status | ||||
| } | ||||
|  | ||||
| const composeParseState = ({ type, incomingStatus }: Props): ComposeState => { | ||||
|   switch (type) { | ||||
| const composeParseState = ( | ||||
|   params: NonNullable<Nav.SharedStackParamList['Screen-Shared-Compose']> | ||||
| ): ComposeState => { | ||||
|   switch (params.type) { | ||||
|     case 'edit': | ||||
|       return { | ||||
|         ...composeInitialState, | ||||
|         ...(incomingStatus.spoiler_text && { | ||||
|         ...(params.incomingStatus.spoiler_text && { | ||||
|           spoiler: { ...composeInitialState.spoiler, active: true } | ||||
|         }), | ||||
|         ...(incomingStatus.poll && { | ||||
|         ...(params.incomingStatus.poll && { | ||||
|           poll: { | ||||
|             active: true, | ||||
|             total: incomingStatus.poll.options.length, | ||||
|             total: params.incomingStatus.poll.options.length, | ||||
|             options: { | ||||
|               '0': incomingStatus.poll.options[0]?.title || undefined, | ||||
|               '1': incomingStatus.poll.options[1]?.title || undefined, | ||||
|               '2': incomingStatus.poll.options[2]?.title || undefined, | ||||
|               '3': incomingStatus.poll.options[3]?.title || undefined | ||||
|               '0': params.incomingStatus.poll.options[0]?.title || undefined, | ||||
|               '1': params.incomingStatus.poll.options[1]?.title || undefined, | ||||
|               '2': params.incomingStatus.poll.options[2]?.title || undefined, | ||||
|               '3': params.incomingStatus.poll.options[3]?.title || undefined | ||||
|             }, | ||||
|             multiple: incomingStatus.poll.multiple, | ||||
|             multiple: params.incomingStatus.poll.multiple, | ||||
|             expire: '86400' // !!! | ||||
|           } | ||||
|         }), | ||||
|         ...(incomingStatus.media_attachments && { | ||||
|         ...(params.incomingStatus.media_attachments && { | ||||
|           attachments: { | ||||
|             sensitive: incomingStatus.sensitive, | ||||
|             uploads: incomingStatus.media_attachments.map(media => ({ | ||||
|             sensitive: params.incomingStatus.sensitive, | ||||
|             uploads: params.incomingStatus.media_attachments.map(media => ({ | ||||
|               remote: media | ||||
|             })) | ||||
|           } | ||||
|         }), | ||||
|         visibility: | ||||
|           incomingStatus.visibility || | ||||
|           params.incomingStatus.visibility || | ||||
|           getLocalAccount(store.getState())?.preferences[ | ||||
|             'posting:default:visibility' | ||||
|           ] || | ||||
|           'public', | ||||
|         ...(incomingStatus.visibility === 'direct' && { visibilityLock: true }) | ||||
|         ...(params.incomingStatus.visibility === 'direct' && { | ||||
|           visibilityLock: true | ||||
|         }) | ||||
|       } | ||||
|     case 'reply': | ||||
|       const actualStatus = incomingStatus.reblog || incomingStatus | ||||
|       const actualStatus = params.incomingStatus.reblog || params.incomingStatus | ||||
|       return { | ||||
|         ...composeInitialState, | ||||
|         visibility: actualStatus.visibility, | ||||
| @@ -57,12 +56,6 @@ const composeParseState = ({ type, incomingStatus }: Props): ComposeState => { | ||||
|     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 | ||||
|       } | ||||
|   | ||||
| @@ -1,8 +1,16 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import { HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { findIndex } from 'lodash' | ||||
| import React, { useCallback, useState } from 'react' | ||||
| import { Image, Platform, Share, StatusBar, StyleSheet, Text } from 'react-native' | ||||
| import { | ||||
|   Image, | ||||
|   Platform, | ||||
|   Share, | ||||
|   StatusBar, | ||||
|   StyleSheet, | ||||
|   Text | ||||
| } from 'react-native' | ||||
| import ImageViewer from 'react-native-image-zoom-viewer' | ||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||||
| import { createNativeStackNavigator } from 'react-native-screens/native-stack' | ||||
| @@ -47,6 +55,7 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({ | ||||
|   ) | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('imageviewer_share_press') | ||||
|     switch (Platform.OS) { | ||||
|       case 'ios': | ||||
|         return Share.share({ url: imageUrls[currentIndex].url }) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import ComponentSeparator from '@components/Separator' | ||||
| import TimelineEmpty from '@components/Timelines/Timeline/Empty' | ||||
| import TimelineEnd from '@components/Timelines/Timeline/End' | ||||
| import { useNavigation, useScrollToTop } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { useRelationshipsQuery } from '@utils/queryHooks/relationships' | ||||
| import React, { useCallback, useMemo, useRef } from 'react' | ||||
| import { RefreshControl, StyleSheet } from 'react-native' | ||||
| @@ -14,7 +15,9 @@ export interface Props { | ||||
| } | ||||
|  | ||||
| const RelationshipsList: React.FC<Props> = ({ id, type }) => { | ||||
|   const navigation = useNavigation() | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.LocalStackParamList> | ||||
|   >() | ||||
|   const { | ||||
|     status, | ||||
|     data, | ||||
| @@ -43,14 +46,7 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => { | ||||
|  | ||||
|   const keyExtractor = useCallback(({ id }) => id, []) | ||||
|   const renderItem = useCallback( | ||||
|     ({ item }) => ( | ||||
|       <ComponentAccount | ||||
|         account={item} | ||||
|         onPress={() => | ||||
|           navigation.push('Screen-Shared-Account', { account: item }) | ||||
|         } | ||||
|       /> | ||||
|     ), | ||||
|     ({ item }) => <ComponentAccount account={item} origin='relationship' />, | ||||
|     [] | ||||
|   ) | ||||
|   const flItemEmptyComponent = useMemo( | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import ComponentAccount from '@components/Account' | ||||
| import analytics from '@components/analytics' | ||||
| import ComponentHashtag from '@components/Hashtag' | ||||
| import ComponentSeparator from '@components/Separator' | ||||
| import TimelineDefault from '@components/Timelines/Timeline/Default' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { useSearchQuery } from '@utils/queryHooks/search' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| @@ -24,7 +24,6 @@ export interface Props { | ||||
|  | ||||
| const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => { | ||||
|   const { t } = useTranslation('sharedSearch') | ||||
|   const navigation = useNavigation() | ||||
|   const { theme } = useTheme() | ||||
|   const { status, data, refetch } = useSearchQuery({ | ||||
|     term: searchTerm, | ||||
| @@ -158,28 +157,11 @@ const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => { | ||||
|   const listItem = useCallback(({ item, section }) => { | ||||
|     switch (section.title) { | ||||
|       case 'accounts': | ||||
|         return ( | ||||
|           <ComponentAccount | ||||
|             account={item} | ||||
|             onPress={() => { | ||||
|               navigation.push('Screen-Shared-Account', { account: item }) | ||||
|             }} | ||||
|           /> | ||||
|         ) | ||||
|         return <ComponentAccount account={item} origin='search' /> | ||||
|       case 'hashtags': | ||||
|         return ( | ||||
|           <ComponentHashtag | ||||
|             tag={item} | ||||
|             onPress={() => { | ||||
|               navigation.goBack() | ||||
|               navigation.push('Screen-Shared-Hashtag', { | ||||
|                 hashtag: item.name | ||||
|               }) | ||||
|             }} | ||||
|           /> | ||||
|         ) | ||||
|         return <ComponentHashtag hashtag={item} origin='search' /> | ||||
|       case 'statuses': | ||||
|         return <TimelineDefault item={item} disableDetails /> | ||||
|         return <TimelineDefault item={item} disableDetails origin='search' /> | ||||
|       default: | ||||
|         return null | ||||
|     } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ const netInfo = async (): Promise<{ | ||||
|  | ||||
|   if (netInfo.isConnected) { | ||||
|     log('log', 'netInfo', 'network connected') | ||||
|     if (activeIndex) { | ||||
|     if (activeIndex !== null) { | ||||
|       log('log', 'netInfo', 'checking locally stored credentials') | ||||
|       return client<Mastodon.Account>({ | ||||
|         method: 'get', | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import log from './log' | ||||
| const sentry = () => { | ||||
|   log('log', 'Sentry', 'initializing') | ||||
|   Sentry.init({ | ||||
|     environment: Constants.manifest.extra.sentryEnv, | ||||
|     dsn: Constants.manifest.extra.sentryDSN, | ||||
|     enableInExpoDevelopment: false, | ||||
|     debug: __DEV__ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user