This commit is contained in:
Zhiyuan Zheng 2021-01-24 02:25:43 +01:00
parent 2a0ad51b24
commit 08f3036753
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
72 changed files with 978 additions and 440 deletions

View File

@ -19,7 +19,8 @@ export default (): ExpoConfig => ({
scheme: 'tooot', scheme: 'tooot',
assetBundlePatterns: ['assets/*'], assetBundlePatterns: ['assets/*'],
extra: { extra: {
sentryDSN: process.env.SENTRY_DSN sentryDSN: process.env.SENTRY_DSN,
sentryEnv: process.env.SENTRY_DEPLOY_ENV
}, },
hooks: { hooks: {
postPublish: [ postPublish: [

View File

@ -3,7 +3,7 @@
"pages": [ "pages": [
[ [
{ {
"id": "1", "id": "999",
"created_at": "2021-01-22T03:48:33.901Z", "created_at": "2021-01-22T03:48:33.901Z",
"sensitive": false, "sensitive": false,
"visibility": "public", "visibility": "public",
@ -21,17 +21,38 @@
"website": "https://tooot.app" "website": "https://tooot.app"
}, },
"account": { "account": {
"id": "1", "id": "999",
"username": "tooot📱", "username": "tooot📱",
"acct": "tooot@xmflsct.com", "acct": "tooot@xmflsct.com",
"display_name": "tooot📱", "display_name": "tooot📱",
"avatar_static": "https://avatars.githubusercontent.com/u/77554750?s=200&v=4" "avatar_static": "https://avatars.githubusercontent.com/u/77554750?s=200&v=4"
}, },
"media_attachments": [], "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", "created_at": "2021-01-22T03:48:33.901Z",
"sensitive": false, "sensitive": false,
"spoiler_text": "", "spoiler_text": "",
@ -50,7 +71,7 @@
"website": null "website": null
}, },
"account": { "account": {
"id": "2", "id": "1000",
"username": "Mastodon", "username": "Mastodon",
"acct": "mastodon", "acct": "mastodon",
"display_name": "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!", "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", "type": "link",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Mastodon_Logotype_%28Simple%29.svg/1200px-Mastodon_Logotype_%28Simple%29.svg.png" "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", "created_at": "2021-01-22T03:48:33.901Z",
"spoiler_text": "", "spoiler_text": "",
"visibility": "public", "visibility": "public",
@ -84,13 +106,70 @@
"website": null "website": null
}, },
"account": { "account": {
"id": "3", "id": "1001",
"username": "Fediverse", "username": "Fediverse",
"acct": "fediverse", "acct": "fediverse",
"display_name": "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" "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": []
} }
] ]
] ]

View File

@ -9,13 +9,13 @@ declare namespace Nav {
type SharedStackParamList = { type SharedStackParamList = {
'Screen-Shared-Account': { 'Screen-Shared-Account': {
account: Pick<Mastodon.Account, 'id' | 'username' | 'acct' | 'url'> account: Mastodon.Account | Mastodon.Mention
} }
'Screen-Shared-Announcements': { showAll?: boolean } 'Screen-Shared-Announcements': { showAll?: boolean }
'Screen-Shared-Attachments': { account: Mastodon.Account } 'Screen-Shared-Attachments': { account: Mastodon.Account }
'Screen-Shared-Compose': 'Screen-Shared-Compose':
| { | {
type: 'reply' | 'conversation' | 'edit' type: 'edit'
incomingStatus: Mastodon.Status incomingStatus: Mastodon.Status
queryKey?: [ queryKey?: [
'Timeline', '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 | undefined
'Screen-Shared-Hashtag': { 'Screen-Shared-Hashtag': {
hashtag: Mastodon.Tag['name'] hashtag: Mastodon.Tag['name']

View File

@ -33,6 +33,7 @@ import React, {
useMemo, useMemo,
useRef useRef
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next'
import { Image, Platform, StatusBar } from 'react-native' import { Image, Platform, StatusBar } from 'react-native'
import Toast from 'react-native-toast-message' import Toast from 'react-native-toast-message'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
@ -70,11 +71,12 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
// }, [isConnected, firstRender]) // }, [isConnected, firstRender])
// On launch display login credentials corrupt information // On launch display login credentials corrupt information
const { t } = useTranslation('common')
useEffect(() => { useEffect(() => {
const showLocalCorrect = localCorrupt const showLocalCorrect = localCorrupt
? toast({ ? toast({
type: 'error', type: 'error',
message: '登录已过期', message: t('index.localCorrupt'),
description: localCorrupt.length ? localCorrupt : undefined, description: localCorrupt.length ? localCorrupt : undefined,
autoHide: false autoHide: false
}) })
@ -193,8 +195,8 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
<Image <Image
source={{ uri: localAccount?.avatarStatic }} source={{ uri: localAccount?.avatarStatic }}
style={{ style={{
width: size + 2, width: size,
height: size + 2, height: size,
borderRadius: size, borderRadius: size,
borderWidth: focused ? 2 : 0, borderWidth: focused ? 2 : 0,
borderColor: focused ? theme.secondary : color borderColor: focused ? theme.secondary : color

View File

@ -1,22 +1,38 @@
import { ParseEmojis } from '@components/Parse' import { ParseEmojis } from '@components/Parse'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React, { useCallback } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import analytics from './analytics'
import GracefullyImage from './GracefullyImage' import GracefullyImage from './GracefullyImage'
export interface Props { export interface Props {
account: Mastodon.Account 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 { theme } = useTheme()
const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const onPress = useCallback(() => {
analytics('search_account_press', { page: origin })
navigation.push('Screen-Shared-Account', { account })
}, [])
return ( return (
<Pressable <Pressable
style={[styles.itemDefault, styles.itemAccount]} style={[styles.itemDefault, styles.itemAccount]}
onPress={onPress} onPress={customOnPress || onPress}
> >
<GracefullyImage <GracefullyImage
uri={{ original: account.avatar_static }} uri={{ original: account.avatar_static }}

View File

@ -18,6 +18,7 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming withTiming
} from 'react-native-reanimated' } from 'react-native-reanimated'
import analytics from './analytics'
export interface Props { export interface Props {
children: React.ReactNode children: React.ReactNode
@ -42,6 +43,7 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
} }
}) })
const callDismiss = () => { const callDismiss = () => {
analytics('bottomsheet_swipe_close')
handleDismiss() handleDismiss()
} }
const onGestureEvent = useAnimatedGestureHandler({ const onGestureEvent = useAnimatedGestureHandler({
@ -90,7 +92,10 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
<Button <Button
type='text' type='text'
content='取消' content='取消'
onPress={() => handleDismiss()} onPress={() => {
analytics('bottomsheet_cancel')
handleDismiss()
}}
style={styles.button} style={styles.button}
/> />
</Animated.View> </Animated.View>

View File

@ -1,23 +1,39 @@
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React, { useCallback } from 'react'
import { Pressable, StyleSheet, Text } from 'react-native' import { Pressable, StyleSheet, Text } from 'react-native'
import analytics from './analytics'
export interface Props { export interface Props {
tag: Mastodon.Tag hashtag: Mastodon.Tag
onPress: () => void onPress?: () => void
origin?: string
} }
const ComponentHashtag: React.FC<Props> = ({ tag, onPress }) => { const ComponentHashtag: React.FC<Props> = ({
hashtag,
onPress: customOnPress,
origin
}) => {
const { theme } = useTheme() 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 ( return (
<Pressable <Pressable
style={[styles.itemDefault, { borderBottomColor: theme.border }]} style={[styles.itemDefault, { borderBottomColor: theme.border }]}
onPress={onPress} onPress={customOnPress || onPress}
> >
<Text style={[styles.itemHashtag, { color: theme.primary }]}> <Text style={[styles.itemHashtag, { color: theme.primary }]}>
#{tag.name} #{hashtag.name}
</Text> </Text>
</Pressable> </Pressable>
) )

View File

@ -16,6 +16,7 @@ import { Alert, Image, StyleSheet, Text, TextInput, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { Placeholder, Fade } from 'rn-placeholder' import { Placeholder, Fade } from 'rn-placeholder'
import analytics from './analytics'
import InstanceAuth from './Instance/Auth' import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info' import InstanceInfo from './Instance/Info'
import { toast } from './toast' import { toast } from './toast'
@ -70,6 +71,7 @@ const ComponentInstance: React.FC<Props> = ({
if (instanceDomain) { if (instanceDomain) {
switch (type) { switch (type) {
case 'local': case 'local':
analytics('instance_local_login')
if ( if (
localInstances && localInstances &&
localInstances.filter(instance => instance.url === instanceDomain) localInstances.filter(instance => instance.url === instanceDomain)
@ -96,6 +98,7 @@ const ComponentInstance: React.FC<Props> = ({
} }
break break
case 'remote': case 'remote':
analytics('instance_remote_register')
haptics('Success') haptics('Success')
const queryKey: QueryKeyTimeline = [ const queryKey: QueryKeyTimeline = [
'Timeline', 'Timeline',
@ -112,6 +115,7 @@ const ComponentInstance: React.FC<Props> = ({
const onSubmitEditing = useCallback( const onSubmitEditing = useCallback(
({ nativeEvent: { text } }) => { ({ nativeEvent: { text } }) => {
analytics('instance_textinput_submit', { match: text === instanceDomain })
if ( if (
text === instanceDomain && text === instanceDomain &&
instanceQuery.isSuccess && instanceQuery.isSuccess &&
@ -276,7 +280,10 @@ const ComponentInstance: React.FC<Props> = ({
{t('server.disclaimer')} {t('server.disclaimer')}
<Text <Text
style={{ color: theme.blue }} 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 https://tooot.app/privacy
</Text> </Text>

View File

@ -1,7 +1,9 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import openLink from '@components/openLink' import openLink from '@components/openLink'
import ParseEmojis from '@components/Parse/Emojis' import ParseEmojis from '@components/Parse/Emojis'
import { useNavigation, useRoute } from '@react-navigation/native' import { useNavigation, useRoute } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
@ -29,7 +31,7 @@ const renderNode = ({
node: any node: any
index: number index: number
size: 'M' | 'L' size: 'M' | 'L'
navigation: any navigation: StackNavigationProp<Nav.LocalStackParamList>
mentions?: Mastodon.Mention[] mentions?: Mastodon.Mention[]
tags?: Mastodon.Tag[] tags?: Mastodon.Tag[]
showFullLink: boolean showFullLink: boolean
@ -53,6 +55,7 @@ const renderNode = ({
...StyleConstants.FontStyle[size] ...StyleConstants.FontStyle[size]
}} }}
onPress={() => { onPress={() => {
analytics('status_hashtag_press')
!disableDetails && !disableDetails &&
differentTag && differentTag &&
navigation.push('Screen-Shared-Hashtag', { navigation.push('Screen-Shared-Hashtag', {
@ -79,6 +82,7 @@ const renderNode = ({
...StyleConstants.FontStyle[size] ...StyleConstants.FontStyle[size]
}} }}
onPress={() => { onPress={() => {
analytics('status_mention_press')
accountIndex !== -1 && accountIndex !== -1 &&
!disableDetails && !disableDetails &&
differentAccount && differentAccount &&
@ -107,13 +111,14 @@ const renderNode = ({
...StyleConstants.FontStyle[size], ...StyleConstants.FontStyle[size],
alignItems: 'center' alignItems: 'center'
}} }}
onPress={async () => onPress={async () => {
analytics('status_link_press')
!disableDetails && !shouldBeTag !disableDetails && !shouldBeTag
? await openLink(href) ? await openLink(href)
: navigation.push('Screen-Shared-Hashtag', { : navigation.push('Screen-Shared-Hashtag', {
hashtag: content.substring(1) hashtag: content.substring(1)
}) })
} }}
> >
{content || (showFullLink ? href : domain[1])} {content || (showFullLink ? href : domain[1])}
{!shouldBeTag ? ( {!shouldBeTag ? (
@ -161,7 +166,9 @@ const ParseHTML: React.FC<Props> = ({
expandHint, expandHint,
disableDetails = false disableDetails = false
}) => { }) => {
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const route = useRoute() const route = useRoute()
const { theme } = useTheme() const { theme } = useTheme()
const { t, i18n } = useTranslation('componentParse') const { t, i18n } = useTranslation('componentParse')
@ -229,6 +236,7 @@ const ParseHTML: React.FC<Props> = ({
{expandAllow ? ( {expandAllow ? (
<Pressable <Pressable
onPress={() => { onPress={() => {
analytics('status_readmore', { allow: expandAllow, expanded })
layoutAnimation() layoutAnimation()
setExpanded(!expanded) setExpanded(!expanded)
}} }}

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { toast } from '@components/toast' import { toast } from '@components/toast'
@ -58,26 +59,28 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
type='icon' type='icon'
content='X' content='X'
loading={mutation.isLoading} loading={mutation.isLoading}
onPress={() => onPress={() => {
analytics('relationship_incoming_press_reject')
mutation.mutate({ mutation.mutate({
id, id,
type: 'incoming', type: 'incoming',
payload: { action: 'reject' } payload: { action: 'reject' }
}) })
} }}
/> />
<Button <Button
round round
type='icon' type='icon'
content='Check' content='Check'
loading={mutation.isLoading} loading={mutation.isLoading}
onPress={() => onPress={() => {
analytics('relationship_incoming_press_authorize')
mutation.mutate({ mutation.mutate({
id, id,
type: 'incoming', type: 'incoming',
payload: { action: 'authorize' } payload: { action: 'authorize' }
}) })
} }}
style={styles.approve} style={styles.approve}
/> />
</View> </View>

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { toast } from '@components/toast' import { toast } from '@components/toast'
@ -40,7 +41,7 @@ const RelationshipOutgoing = React.memo(
toast({ toast({
type: 'error', type: 'error',
message: t('common:toastMessage.error.message', { message: t('common:toastMessage.error.message', {
function: t(`button.${action}.function`) function: t(`${action}.function`)
}), }),
...(err.status && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&
@ -61,12 +62,17 @@ const RelationshipOutgoing = React.memo(
onPress = () => {} onPress = () => {}
} else { } else {
if (query.data?.blocked_by) { if (query.data?.blocked_by) {
analytics('relationship_outgoing_blocked_by')
content = t('button.blocked_by') content = t('button.blocked_by')
onPress = () => null onPress = () => {
analytics('relationship_outgoing_blocked_by_press')
}
} else { } else {
if (query.data?.blocking) { if (query.data?.blocking) {
analytics('relationship_outgoing_blocking')
content = t('button.blocking') content = t('button.blocking')
onPress = () => onPress = () => {
analytics('relationship_outgoing_blocking_press')
mutation.mutate({ mutation.mutate({
id, id,
type: 'outgoing', type: 'outgoing',
@ -75,10 +81,13 @@ const RelationshipOutgoing = React.memo(
state: query.data?.blocking state: query.data?.blocking
} }
}) })
}
} else { } else {
if (query.data?.following) { if (query.data?.following) {
analytics('relationship_outgoing_following')
content = t('button.following') content = t('button.following')
onPress = () => onPress = () => {
analytics('relationship_outgoing_following_press')
mutation.mutate({ mutation.mutate({
id, id,
type: 'outgoing', type: 'outgoing',
@ -87,10 +96,13 @@ const RelationshipOutgoing = React.memo(
state: query.data?.following state: query.data?.following
} }
}) })
}
} else { } else {
if (query.data?.requested) { if (query.data?.requested) {
analytics('relationship_outgoing_requested')
content = t('button.requested') content = t('button.requested')
onPress = () => onPress = () => {
analytics('relationship_outgoing_requested_press')
mutation.mutate({ mutation.mutate({
id, id,
type: 'outgoing', type: 'outgoing',
@ -99,9 +111,12 @@ const RelationshipOutgoing = React.memo(
state: query.data?.requested state: query.data?.requested
} }
}) })
}
} else { } else {
analytics('relationship_outgoing_default')
content = t('button.default') content = t('button.default')
onPress = () => onPress = () => {
analytics('relationship_outgoing_default_press')
mutation.mutate({ mutation.mutate({
id, id,
type: 'outgoing', type: 'outgoing',
@ -110,6 +125,7 @@ const RelationshipOutgoing = React.memo(
state: false state: false
} }
}) })
}
} }
} }
} }

View File

@ -12,6 +12,7 @@ import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { TabView } from 'react-native-tab-view' import { TabView } from 'react-native-tab-view'
import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter' import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import analytics from './analytics'
const Stack = createNativeStackNavigator< const Stack = createNativeStackNavigator<
Nav.LocalStackParamList | Nav.RemoteStackParamList Nav.LocalStackParamList | Nav.RemoteStackParamList
@ -41,6 +42,7 @@ const Timelines: React.FC<Props> = ({ name }) => {
const localActiveIndex = useSelector(getLocalActiveIndex) const localActiveIndex = useSelector(getLocalActiveIndex)
const onPressSearch = useCallback(() => { const onPressSearch = useCallback(() => {
analytics('search_tap', { page: mapNameToContent[name][segment].page })
navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' }) navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' })
}, []) }, [])

View File

@ -1,13 +1,10 @@
import ComponentSeparator from '@components/Separator' 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 { 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 { localUpdateNotification } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { findIndex } from 'lodash'
import React, { useCallback, useEffect, useMemo, useRef } from 'react' import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { import {
FlatListProps, FlatListProps,
@ -17,9 +14,12 @@ import {
} from 'react-native' } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import TimelineConversation from './Timeline/Conversation'
import { findIndex } from 'lodash' import TimelineDefault from './Timeline/Default'
import { getPublicRemoteNotice } from '@utils/slices/contextsSlice' import TimelineEmpty from './Timeline/Empty'
import TimelineEnd from './Timeline/End'
import TimelineHeader from './Timeline/Header'
import TimelineNotifications from './Timeline/Notifications'
export interface Props { export interface Props {
page: App.Pages page: App.Pages
@ -99,8 +99,10 @@ const Timeline: React.FC<Props> = ({
}, [navigation, flattenData]) }, [navigation, flattenData])
const flRef = useRef<FlatList<any>>(null) const flRef = useRef<FlatList<any>>(null)
const scrolled = useRef(false)
useEffect(() => { useEffect(() => {
if (toot && isSuccess) { if (toot && isSuccess && !scrolled.current) {
scrolled.current = true
const pointer = findIndex(flattenData, ['id', toot]) const pointer = findIndex(flattenData, ['id', toot])
setTimeout(() => { setTimeout(() => {
flRef.current?.scrollToIndex({ flRef.current?.scrollToIndex({
@ -109,7 +111,7 @@ const Timeline: React.FC<Props> = ({
}) })
}, 500) }, 500)
} }
}, [isSuccess, flattenData]) }, [isSuccess, flattenData.length, scrolled])
const keyExtractor = useCallback(({ id }) => id, []) const keyExtractor = useCallback(({ id }) => id, [])
const renderItem = useCallback(({ item }) => { const renderItem = useCallback(({ item }) => {

View File

@ -1,5 +1,8 @@
import client from '@api/client' import client from '@api/client'
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -9,11 +12,42 @@ import { Pressable, StyleSheet, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useMutation, useQueryClient } from 'react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TimelineActions from './Shared/Actions' import TimelineActions from './Shared/Actions'
import TimelineAvatar from './Shared/Avatar'
import TimelineContent from './Shared/Content' import TimelineContent from './Shared/Content'
import TimelineHeaderConversation from './Shared/HeaderConversation' import TimelineHeaderConversation from './Shared/HeaderConversation'
import TimelinePoll from './Shared/Poll' 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 { export interface Props {
conversation: Mastodon.Conversation conversation: Mastodon.Conversation
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -42,9 +76,11 @@ const TimelineConversation: React.FC<Props> = ({
} }
}) })
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const onPress = useCallback(() => { const onPress = useCallback(() => {
analytics('timeline_conversation_press')
if (conversation.last_status) { if (conversation.last_status) {
conversation.unread && mutate() conversation.unread && mutate()
navigation.push('Screen-Shared-Toot', { navigation.push('Screen-Shared-Toot', {
@ -68,10 +104,7 @@ const TimelineConversation: React.FC<Props> = ({
onPress={onPress} onPress={onPress}
> >
<View style={styles.header}> <View style={styles.header}>
<TimelineAvatar <Avatars accounts={conversation.accounts} />
queryKey={queryKey}
account={conversation.accounts[0]}
/>
<TimelineHeaderConversation <TimelineHeaderConversation
queryKey={queryKey} queryKey={queryKey}
conversation={conversation} conversation={conversation}
@ -112,6 +145,7 @@ const TimelineConversation: React.FC<Props> = ({
<TimelineActions <TimelineActions
queryKey={queryKey} queryKey={queryKey}
status={conversation.last_status} status={conversation.last_status}
accts={conversation.accounts.map(account => account.acct)}
reblog={false} reblog={false}
/> />
</View> </View>

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned' import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned'
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions' import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
import TimelineAttachment from '@components/Timelines/Timeline/Shared/Attachment' 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 TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderDefault'
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll' import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -17,6 +19,7 @@ import { useSelector } from 'react-redux'
export interface Props { export interface Props {
item: Mastodon.Status & { isPinned?: boolean } item: Mastodon.Status & { isPinned?: boolean }
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
origin?: string
highlighted?: boolean highlighted?: boolean
disableDetails?: boolean disableDetails?: boolean
disableOnPress?: boolean disableOnPress?: boolean
@ -26,27 +29,42 @@ export interface Props {
const TimelineDefault: React.FC<Props> = ({ const TimelineDefault: React.FC<Props> = ({
item, item,
queryKey, queryKey,
origin,
highlighted = false, highlighted = false,
disableDetails = false, disableDetails = false,
disableOnPress = false disableOnPress = false
}) => { }) => {
const localAccount = useSelector(getLocalAccount) const localAccount = useSelector(getLocalAccount)
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
let actualStatus = item.reblog ? item.reblog : item let actualStatus = item.reblog ? item.reblog : item
const onPress = useCallback( const onPress = useCallback(() => {
() => analytics('timeline_default_press', {
!disableOnPress && page: queryKey ? queryKey[1].page : origin
})
!disableOnPress &&
!highlighted && !highlighted &&
navigation.push('Screen-Shared-Toot', { navigation.push('Screen-Shared-Toot', {
toot: actualStatus toot: actualStatus
}), })
[] }, [])
)
return ( return (
<Pressable style={styles.statusView} onPress={onPress}> <Pressable
style={[
styles.statusView,
{
paddingBottom:
disableDetails && disableOnPress
? StyleConstants.Spacing.Global.PagePadding
: 0
}
]}
onPress={onPress}
>
{item.reblog ? ( {item.reblog ? (
<TimelineActioned action='reblog' account={item.account} /> <TimelineActioned action='reblog' account={item.account} />
) : item.isPinned ? ( ) : item.isPinned ? (
@ -107,6 +125,11 @@ const TimelineDefault: React.FC<Props> = ({
<TimelineActions <TimelineActions
queryKey={queryKey} queryKey={queryKey}
status={actualStatus} 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} reblog={item.reblog ? true : false}
/> />
</View> </View>

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -37,7 +38,10 @@ const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
<Button <Button
type='text' type='text'
content={t('empty.error.button')} content={t('empty.error.button')}
onPress={() => refetch()} onPress={() => {
analytics('timeline_error_press_refetch')
refetch()
}}
/> />
</> </>
) )

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import Icon from '@root/components/Icon' import Icon from '@root/components/Icon'
import { StyleConstants } from '@root/utils/styles/constants' import { StyleConstants } from '@root/utils/styles/constants'
@ -22,6 +23,7 @@ const TimelineHeader = React.memo(
<Text <Text
style={{ color: theme.blue }} style={{ color: theme.blue }}
onPress={() => { onPress={() => {
analytics('timeline_remote_header_press')
dispatch(updatePublicRemoteNotice(1)) dispatch(updatePublicRemoteNotice(1))
navigation.navigate('Screen-Me', { navigation.navigate('Screen-Me', {
screen: 'Screen-Me-Root', screen: 'Screen-Me-Root',

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned' import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned'
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions' import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
import TimelineAttachment from '@components/Timelines/Timeline/Shared/Attachment' 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 TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/HeaderNotification'
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll' import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -26,19 +28,20 @@ const TimelineNotifications: React.FC<Props> = ({
highlighted = false highlighted = false
}) => { }) => {
const localAccount = useSelector(getLocalAccount) const localAccount = useSelector(getLocalAccount)
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const actualAccount = notification.status const actualAccount = notification.status
? notification.status.account ? notification.status.account
: notification.account : notification.account
const onPress = useCallback( const onPress = useCallback(() => {
() => analytics('timeline_notification_press')
notification.status && notification.status &&
navigation.push('Screen-Shared-Toot', { navigation.push('Screen-Shared-Toot', {
toot: notification.status toot: notification.status
}), })
[] }, [])
)
return ( return (
<Pressable style={styles.notificationView} onPress={onPress}> <Pressable style={styles.notificationView} onPress={onPress}>
@ -112,6 +115,11 @@ const TimelineNotifications: React.FC<Props> = ({
<TimelineActions <TimelineActions
queryKey={queryKey} queryKey={queryKey}
status={notification.status} 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} reblog={false}
/> />
</View> </View>

View File

@ -1,6 +1,8 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { ParseEmojis } from '@components/Parse' import { ParseEmojis } from '@components/Parse'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
@ -20,7 +22,9 @@ const TimelineActioned: React.FC<Props> = ({
}) => { }) => {
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const name = account.display_name || account.username const name = account.display_name || account.username
const iconColor = theme.primary const iconColor = theme.primary
@ -29,6 +33,7 @@ const TimelineActioned: React.FC<Props> = ({
) )
const onPress = useCallback(() => { const onPress = useCallback(() => {
analytics('timeline_shared_actioned_press', { action })
navigation.push('Screen-Shared-Account', { account }) navigation.push('Screen-Shared-Account', { account })
}, []) }, [])

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { toast } from '@components/toast' import { toast } from '@components/toast'
@ -17,10 +18,16 @@ import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
status: Mastodon.Status status: Mastodon.Status
accts: Mastodon.Account['acct'][] // When replying to conversations
reblog: boolean reblog: boolean
} }
const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => { const TimelineActions: React.FC<Props> = ({
queryKey,
status,
accts,
reblog
}) => {
const navigation = useNavigation() const navigation = useNavigation()
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { theme } = useTheme() const { theme } = useTheme()
@ -92,63 +99,74 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
} }
}) })
const onPressReply = useCallback( const onPressReply = useCallback(() => {
() => analytics('timeline_shared_actions_reply_press', {
navigation.navigate('Screen-Shared-Compose', { page: queryKey[1].page,
type: 'reply', count: status.replies_count
incomingStatus: status, })
queryKey navigation.navigate('Screen-Shared-Compose', {
}), type: 'reply',
[] incomingStatus: status,
) accts,
const onPressReblog = useCallback( queryKey
() => })
mutation.mutate({ }, [status.replies_count])
type: 'updateStatusProperty', const onPressReblog = useCallback(() => {
queryKey, analytics('timeline_shared_actions_reblog_press', {
id: status.id, page: queryKey[1].page,
reblog, count: status.reblogs_count,
payload: { current: status.reblogged
property: 'reblogged', })
currentValue: status.reblogged, mutation.mutate({
propertyCount: 'reblogs_count', type: 'updateStatusProperty',
countValue: status.reblogs_count queryKey,
} id: status.id,
}), reblog,
[status.reblogged] payload: {
) property: 'reblogged',
const onPressFavourite = useCallback( currentValue: status.reblogged,
() => propertyCount: 'reblogs_count',
mutation.mutate({ countValue: status.reblogs_count
type: 'updateStatusProperty', }
queryKey, })
id: status.id, }, [status.reblogged, status.reblogs_count])
reblog, const onPressFavourite = useCallback(() => {
payload: { analytics('timeline_shared_actions_favourite_press', {
property: 'favourited', page: queryKey[1].page,
currentValue: status.favourited, count: status.favourites_count,
propertyCount: 'favourites_count', current: status.favourited
countValue: status.favourites_count })
} mutation.mutate({
}), type: 'updateStatusProperty',
[status.favourited] queryKey,
) id: status.id,
const onPressBookmark = useCallback( reblog,
() => payload: {
mutation.mutate({ property: 'favourited',
type: 'updateStatusProperty', currentValue: status.favourited,
queryKey, propertyCount: 'favourites_count',
id: status.id, countValue: status.favourites_count
reblog, }
payload: { })
property: 'bookmarked', }, [status.favourited, status.favourites_count])
currentValue: status.bookmarked, const onPressBookmark = useCallback(() => {
propertyCount: undefined, analytics('timeline_shared_actions_bookmark_press', {
countValue: undefined page: queryKey[1].page,
} current: status.bookmarked
}), })
[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( const childrenReply = useMemo(
() => ( () => (

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import AttachmentAudio from '@components/Timelines/Timeline/Shared/Attachment/Audio' 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 [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
const onPressBlurView = useCallback(() => { const onPressBlurView = useCallback(() => {
analytics('timeline_shared_attachment_blurview_press_show')
layoutAnimation() layoutAnimation()
setSensitiveShown(false) setSensitiveShown(false)
haptics('Medium') haptics('Light')
}, []) }, [])
const onPressShow = useCallback(() => { const onPressShow = useCallback(() => {
analytics('timeline_shared_attachment_blurview_press_hide')
setSensitiveShown(true) setSensitiveShown(true)
haptics('Medium') haptics('Light')
}, []) }, [])
let imageUrls: (IImageInfo & { let imageUrls: (IImageInfo & {

View File

@ -9,6 +9,7 @@ import { Blurhash } from 'gl-react-blurhash'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import attachmentAspectRatio from './aspectRatio' import attachmentAspectRatio from './aspectRatio'
import analytics from '@components/analytics'
export interface Props { export interface Props {
total: number total: number
@ -29,6 +30,7 @@ const AttachmentAudio: React.FC<Props> = ({
const [audioPlaying, setAudioPlaying] = useState(false) const [audioPlaying, setAudioPlaying] = useState(false)
const [audioPosition, setAudioPosition] = useState(0) const [audioPosition, setAudioPosition] = useState(0)
const playAudio = useCallback(async () => { const playAudio = useCallback(async () => {
analytics('timeline_shared_attachment_audio_play_press', { id: audio.id })
if (!audioPlayer) { if (!audioPlayer) {
const { sound } = await Audio.Sound.createAsync( const { sound } = await Audio.Sound.createAsync(
{ uri: audio.url }, { uri: audio.url },
@ -44,6 +46,7 @@ const AttachmentAudio: React.FC<Props> = ({
} }
}, [audioPlayer, audioPosition]) }, [audioPlayer, audioPosition])
const pauseAudio = useCallback(async () => { const pauseAudio = useCallback(async () => {
analytics('timeline_shared_attachment_audio_pause_press', { id: audio.id })
audioPlayer!.pauseAsync() audioPlayer!.pauseAsync()
setAudioPlaying(false) setAudioPlaying(false)
}, [audioPlayer]) }, [audioPlayer])

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
@ -19,7 +20,10 @@ const AttachmentImage: React.FC<Props> = ({
image, image,
navigateToImagesViewer navigateToImagesViewer
}) => { }) => {
const onPress = useCallback(() => navigateToImagesViewer(index), []) const onPress = useCallback(() => {
analytics('timeline_shared_attachment_image_press', { id: image.id })
navigateToImagesViewer(index)
}, [])
return ( return (
<GracefullyImage <GracefullyImage

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import openLink from '@components/openLink' import openLink from '@components/openLink'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -59,9 +60,10 @@ const AttachmentUnsupported: React.FC<Props> = ({
content={t('shared.attachment.unsupported.button')} content={t('shared.attachment.unsupported.button')}
size='S' size='S'
overlay overlay
onPress={async () => onPress={async () => {
analytics('timeline_shared_attachment_unsupported_press')
attachment.remote_url && (await openLink(attachment.remote_url)) attachment.remote_url && (await openLink(attachment.remote_url))
} }}
/> />
) : null} ) : null}
</> </>

View File

@ -6,6 +6,7 @@ import { Blurhash } from 'gl-react-blurhash'
import React, { useCallback, useRef, useState } from 'react' import React, { useCallback, useRef, useState } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import attachmentAspectRatio from './aspectRatio' import attachmentAspectRatio from './aspectRatio'
import analytics from '@components/analytics'
export interface Props { export interface Props {
total: number total: number
@ -25,6 +26,13 @@ const AttachmentVideo: React.FC<Props> = ({
const [videoLoaded, setVideoLoaded] = useState(false) const [videoLoaded, setVideoLoaded] = useState(false)
const [videoPosition, setVideoPosition] = useState<number>(0) const [videoPosition, setVideoPosition] = useState<number>(0)
const playOnPress = useCallback(async () => { 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) setVideoLoading(true)
if (!videoLoaded) { if (!videoLoaded) {
await videoPlayer.current?.loadAsync({ uri: video.url }) await videoPlayer.current?.loadAsync({ uri: video.url })
@ -66,6 +74,10 @@ const AttachmentVideo: React.FC<Props> = ({
useNativeControls={false} useNativeControls={false}
onFullscreenUpdate={event => { onFullscreenUpdate={event => {
if (event.fullscreenUpdate === 3) { if (event.fullscreenUpdate === 3) {
analytics('timeline_shared_attachment_video_pause_press', {
id: video.id,
timestamp: Date.now()
})
videoPlayer.current?.pauseAsync() videoPlayer.current?.pauseAsync()
} }
}} }}

View File

@ -3,6 +3,8 @@ import { StyleConstants } from '@utils/styles/constants'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import { StackNavigationProp } from '@react-navigation/stack'
import analytics from '@components/analytics'
export interface Props { export interface Props {
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
@ -10,9 +12,12 @@ export interface Props {
} }
const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => { const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => {
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
// Need to fix go back root // Need to fix go back root
const onPress = useCallback(() => { const onPress = useCallback(() => {
analytics('timeline_shared_avatar_press', { page: queryKey[1].page })
queryKey && navigation.push('Screen-Shared-Account', { account }) queryKey && navigation.push('Screen-Shared-Account', { account })
}, []) }, [])

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import openLink from '@components/openLink' import openLink from '@components/openLink'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -15,7 +16,10 @@ const TimelineCard: React.FC<Props> = ({ card }) => {
return ( return (
<Pressable <Pressable
style={[styles.card, { borderColor: theme.border }]} 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' testID='base'
> >
{card.image && ( {card.image && (
@ -42,7 +46,10 @@ const TimelineCard: React.FC<Props> = ({ card }) => {
{card.description} {card.description}
</Text> </Text>
) : null} ) : null}
<Text numberOfLines={1} style={{ color: theme.secondary }}> <Text
numberOfLines={1}
style={[styles.rightLink, { color: theme.secondary }]}
>
{card.url} {card.url}
</Text> </Text>
</View> </View>
@ -54,14 +61,14 @@ const styles = StyleSheet.create({
card: { card: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
height: StyleConstants.Font.LineHeight.M * 4.5, height: StyleConstants.Font.LineHeight.M * 5,
marginTop: StyleConstants.Spacing.M, marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6 borderRadius: 6
}, },
left: { left: {
width: StyleConstants.Font.LineHeight.M * 4.5, width: StyleConstants.Font.LineHeight.M * 5,
height: StyleConstants.Font.LineHeight.M * 4.5 height: StyleConstants.Font.LineHeight.M * 5
}, },
image: { image: {
width: '100%', width: '100%',
@ -74,11 +81,16 @@ const styles = StyleSheet.create({
padding: StyleConstants.Spacing.S padding: StyleConstants.Spacing.S
}, },
rightTitle: { rightTitle: {
...StyleConstants.FontStyle.S,
marginBottom: StyleConstants.Spacing.XS, marginBottom: StyleConstants.Spacing.XS,
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}, },
rightDescription: { rightDescription: {
...StyleConstants.FontStyle.S,
marginBottom: StyleConstants.Spacing.XS marginBottom: StyleConstants.Spacing.XS
},
rightLink: {
...StyleConstants.FontStyle.S
} }
}) })

View File

@ -1,7 +1,9 @@
import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu' import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { import {
MutationVarsTimelineUpdateAccountProperty,
QueryKeyTimeline, QueryKeyTimeline,
useTimelineMutation useTimelineMutation
} from '@utils/queryHooks/timeline' } from '@utils/queryHooks/timeline'
@ -11,7 +13,7 @@ import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
account: Pick<Mastodon.Account, 'id' | 'acct'> account: Mastodon.Account
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>> setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
} }
@ -25,23 +27,30 @@ const HeaderActionsAccount: React.FC<Props> = ({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const mutateion = useTimelineMutation({ const mutateion = useTimelineMutation({
queryClient, queryClient,
onSuccess: (_, { payload: { property } }) => { onSuccess: (_, params) => {
const theParams = params as MutationVarsTimelineUpdateAccountProperty
haptics('Success') haptics('Success')
toast({ toast({
type: 'success', type: 'success',
message: t('common:toastMessage.success.message', { message: t('common:toastMessage.success.message', {
function: t(`shared.header.actions.account.${property}.function`, { function: t(
acct: account.acct `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') haptics('Error')
toast({ toast({
type: 'error', type: 'error',
message: t('common:toastMessage.error.message', { 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 && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&
@ -62,6 +71,9 @@ const HeaderActionsAccount: React.FC<Props> = ({
<MenuHeader heading={t('shared.header.actions.account.heading')} /> <MenuHeader heading={t('shared.header.actions.account.heading')} />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_account_mute_press', {
page: queryKey && queryKey[1].page
})
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutateion.mutate({ mutateion.mutate({
type: 'updateAccountProperty', type: 'updateAccountProperty',
@ -77,6 +89,9 @@ const HeaderActionsAccount: React.FC<Props> = ({
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_account_block_press', {
page: queryKey && queryKey[1].page
})
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutateion.mutate({ mutateion.mutate({
type: 'updateAccountProperty', type: 'updateAccountProperty',
@ -92,6 +107,9 @@ const HeaderActionsAccount: React.FC<Props> = ({
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_account_reports_press', {
page: queryKey && queryKey[1].page
})
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutateion.mutate({ mutateion.mutate({
type: 'updateAccountProperty', type: 'updateAccountProperty',

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import MenuContainer from '@components/Menu/Container' import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header' import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row' import MenuRow from '@components/Menu/Row'
@ -42,6 +43,9 @@ const HeaderActionsDomain: React.FC<Props> = ({
<MenuHeader heading={t(`shared.header.actions.domain.heading`)} /> <MenuHeader heading={t(`shared.header.actions.domain.heading`)} />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_domain_block_press', {
page: queryKey[1].page
})
Alert.alert( Alert.alert(
t('shared.header.actions.domain.alert.title', { domain }), t('shared.header.actions.domain.alert.title', { domain }),
t('shared.header.actions.domain.alert.message'), 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'), text: t('shared.header.actions.domain.alert.buttons.confirm'),
style: 'destructive', style: 'destructive',
onPress: () => { onPress: () => {
analytics(
'timeline_shared_headeractions_domain_block_confirm',
{
page: queryKey && queryKey[1].page
}
)
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutation.mutate({ mutation.mutate({
type: 'domainBlock', type: 'domainBlock',

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import BottomSheet from '@components/BottomSheet' import BottomSheet from '@components/BottomSheet'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
@ -33,7 +34,12 @@ const HeaderActions = React.memo(
const sameDomain = localDomain === statusDomain const sameDomain = localDomain === statusDomain
const [modalVisible, setBottomSheetVisible] = useState(false) 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( const children = useMemo(
() => ( () => (
<Icon <Icon

View File

@ -1,11 +1,7 @@
import analytics from '@components/analytics'
import MenuContainer from '@components/Menu/Container' import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header' import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row' import MenuRow from '@components/Menu/Row'
import { toast } from '@components/toast'
import {
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform, Share } from 'react-native' import { Platform, Share } from 'react-native'
@ -30,6 +26,7 @@ const HeaderActionsShare: React.FC<Props> = ({
iconFront='Share2' iconFront='Share2'
title={t(`shared.header.actions.share.${type}.button`)} title={t(`shared.header.actions.share.${type}.button`)}
onPress={async () => { onPress={async () => {
analytics('timeline_shared_headeractions_share_press')
switch (Platform.OS) { switch (Platform.OS) {
case 'ios': case 'ios':
await Share.share({ await Share.share({

View File

@ -10,6 +10,7 @@ import {
QueryKeyTimeline, QueryKeyTimeline,
useTimelineMutation useTimelineMutation
} from '@utils/queryHooks/timeline' } from '@utils/queryHooks/timeline'
import analytics from '@components/analytics'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -37,9 +38,7 @@ const HeaderActionsStatus: React.FC<Props> = ({
toast({ toast({
type: 'error', type: 'error',
message: t('common:toastMessage.error.message', { message: t('common:toastMessage.error.message', {
function: t( function: t(`shared.header.actions.status.${theFunction}.function`)
`shared.header.actions.status.${theFunction}.function`
)
}), }),
...(err.status && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&
@ -55,11 +54,12 @@ const HeaderActionsStatus: React.FC<Props> = ({
return ( return (
<MenuContainer> <MenuContainer>
<MenuHeader <MenuHeader heading={t('shared.header.actions.status.heading')} />
heading={t('shared.header.actions.status.heading')}
/>
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_status_delete_press', {
page: queryKey && queryKey[1].page
})
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutation.mutate({ mutation.mutate({
type: 'deleteItem', type: 'deleteItem',
@ -73,19 +73,31 @@ const HeaderActionsStatus: React.FC<Props> = ({
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_status_deleteedit_press', {
page: queryKey && queryKey[1].page
})
Alert.alert( Alert.alert(
t('shared.header.actions.status.edit.alert.title'), t('shared.header.actions.status.edit.alert.title'),
t( t('shared.header.actions.status.edit.alert.message'),
'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( text: t(
'shared.header.actions.status.edit.alert.buttons.confirm' 'shared.header.actions.status.edit.alert.buttons.confirm'
), ),
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
analytics(
'timeline_shared_headeractions_status_deleteedit_confirm',
{
page: queryKey && queryKey[1].page
}
)
setBottomSheetVisible(false) setBottomSheetVisible(false)
const res = await mutation.mutateAsync({ const res = await mutation.mutateAsync({
type: 'deleteItem', type: 'deleteItem',
@ -110,46 +122,54 @@ const HeaderActionsStatus: React.FC<Props> = ({
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_status_mute_press', {
page: queryKey && queryKey[1].page
})
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutation.mutate({ mutation.mutate({
type: 'updateStatusProperty', type: 'updateStatusProperty',
queryKey, queryKey,
id: status.id, id: status.id,
payload: { property: 'muted', currentValue: status.muted } payload: {
property: 'muted',
currentValue: status.muted,
propertyCount: undefined,
countValue: undefined
}
}) })
}} }}
iconFront='VolumeX' iconFront='VolumeX'
title={ title={
status.muted status.muted
? t( ? t('shared.header.actions.status.mute.button.negative')
'shared.header.actions.status.mute.button.negative' : t('shared.header.actions.status.mute.button.positive')
)
: t(
'shared.header.actions.status.mute.button.positive'
)
} }
/> />
{/* Also note that reblogs cannot be pinned. */} {/* Also note that reblogs cannot be pinned. */}
{(status.visibility === 'public' || status.visibility === 'unlisted') && ( {(status.visibility === 'public' || status.visibility === 'unlisted') && (
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_status_pin_press', {
page: queryKey && queryKey[1].page
})
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutation.mutate({ mutation.mutate({
type: 'updateStatusProperty', type: 'updateStatusProperty',
queryKey, queryKey,
id: status.id, id: status.id,
payload: { property: 'pinned', currentValue: status.pinned } payload: {
property: 'pinned',
currentValue: status.pinned,
propertyCount: undefined,
countValue: undefined
}
}) })
}} }}
iconFront='Anchor' iconFront='Anchor'
title={ title={
status.pinned status.pinned
? t( ? t('shared.header.actions.status.pin.button.negative')
'shared.header.actions.status.pin.button.negative' : t('shared.header.actions.status.pin.button.positive')
)
: t(
'shared.header.actions.status.pin.button.positive'
)
} }
/> />
)} )}

View File

@ -1,5 +1,7 @@
import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { ParseEmojis } from '@components/Parse'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { import {
QueryKeyTimeline, QueryKeyTimeline,
@ -9,12 +11,34 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' 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 { useQueryClient } from 'react-query'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted' 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 { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
conversation: Mastodon.Conversation conversation: Mastodon.Conversation
@ -49,16 +73,15 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
const { theme } = useTheme() const { theme } = useTheme()
const actionOnPress = useCallback( const actionOnPress = useCallback(() => {
() => analytics('timeline_conversation_delete_press')
mutation.mutate({ mutation.mutate({
type: 'deleteItem', type: 'deleteItem',
source: 'conversations', source: 'conversations',
queryKey, queryKey,
id: conversation.id id: conversation.id
}), })
[] }, [])
)
const actionChildren = useMemo( const actionChildren = useMemo(
() => ( () => (
@ -74,7 +97,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
return ( return (
<View style={styles.base}> <View style={styles.base}>
<View style={styles.nameAndMeta}> <View style={styles.nameAndMeta}>
<HeaderSharedAccount account={conversation.accounts[0]} /> <Names accounts={conversation.accounts} />
<View style={styles.meta}> <View style={styles.meta}>
{conversation.last_status?.created_at ? ( {conversation.last_status?.created_at ? (
<HeaderSharedCreated <HeaderSharedCreated
@ -100,7 +123,7 @@ const styles = StyleSheet.create({
flexDirection: 'row' flexDirection: 'row'
}, },
nameAndMeta: { nameAndMeta: {
flex: 4 flex: 3
}, },
meta: { meta: {
flexDirection: 'row', flexDirection: 'row',
@ -115,6 +138,9 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center' justifyContent: 'center'
},
namesLeading: {
...StyleConstants.FontStyle.M
} }
}) })

View File

@ -45,7 +45,7 @@ const styles = StyleSheet.create({
flexDirection: 'row' flexDirection: 'row'
}, },
accountAndMeta: { accountAndMeta: {
flex: 4 flex: 5
}, },
meta: { meta: {
flexDirection: 'row', flexDirection: 'row',

View File

@ -6,7 +6,7 @@ import { StyleSheet, Text, View } from 'react-native'
export interface Props { export interface Props {
account: Mastodon.Account account: Mastodon.Account
withoutName?: boolean withoutName?: boolean // For notification follow request etc.
} }
const HeaderSharedAccount: React.FC<Props> = ({ const HeaderSharedAccount: React.FC<Props> = ({

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import openLink from '@components/openLink' import openLink from '@components/openLink'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
@ -15,9 +16,12 @@ const HeaderSharedApplication: React.FC<Props> = ({ application }) => {
return application && application.name !== 'Web' ? ( return application && application.name !== 'Web' ? (
<Text <Text
onPress={async () => onPress={async () => {
analytics('timeline_shared_header_application_press', {
application
})
application.website && (await openLink(application.website)) application.website && (await openLink(application.website))
} }}
style={[styles.application, { color: theme.secondary }]} style={[styles.application, { color: theme.secondary }]}
> >
{t('shared.header.shared.application', { application: application.name })} {t('shared.header.shared.application', { application: application.name })}

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
@ -49,6 +50,7 @@ const TimelinePoll: React.FC<Props> = ({
toast({ toast({
type: 'error', type: 'error',
message: t('common:toastMessage.error.message', { message: t('common:toastMessage.error.message', {
// @ts-ignore
function: t(`shared.poll.meta.button.${theParams.payload.type}`) function: t(`shared.poll.meta.button.${theParams.payload.type}`)
}), }),
...(err.status && ...(err.status &&
@ -69,7 +71,8 @@ const TimelinePoll: React.FC<Props> = ({
return ( return (
<View style={styles.button}> <View style={styles.button}>
<Button <Button
onPress={() => onPress={() => {
analytics('timeline_shared_vote_vote_press')
mutation.mutate({ mutation.mutate({
type: 'updateStatusProperty', type: 'updateStatusProperty',
queryKey, queryKey,
@ -82,7 +85,7 @@ const TimelinePoll: React.FC<Props> = ({
options: allOptions options: allOptions
} }
}) })
} }}
type='text' type='text'
content={t('shared.poll.meta.button.vote')} content={t('shared.poll.meta.button.vote')}
loading={mutation.isLoading} loading={mutation.isLoading}
@ -94,7 +97,8 @@ const TimelinePoll: React.FC<Props> = ({
return ( return (
<View style={styles.button}> <View style={styles.button}>
<Button <Button
onPress={() => onPress={() => {
analytics('timeline_shared_vote_refresh_press')
mutation.mutate({ mutation.mutate({
type: 'updateStatusProperty', type: 'updateStatusProperty',
queryKey, queryKey,
@ -106,7 +110,7 @@ const TimelinePoll: React.FC<Props> = ({
type: 'refresh' type: 'refresh'
} }
}) })
} }}
type='text' type='text'
content={t('shared.poll.meta.button.refresh')} content={t('shared.poll.meta.button.refresh')}
loading={mutation.isLoading} loading={mutation.isLoading}
@ -199,6 +203,7 @@ const TimelinePoll: React.FC<Props> = ({
key={index} key={index}
style={styles.optionContainer} style={styles.optionContainer}
onPress={() => { onPress={() => {
analytics('timeline_shared_vote_option_press')
haptics('Light') haptics('Light')
if (poll.multiple) { if (poll.multiple) {
setAllOptions(allOptions.map((o, i) => (i === index ? !o : o))) setAllOptions(allOptions.map((o, i) => (i === index ? !o : o)))

View File

@ -1,6 +1,6 @@
import * as Analytics from 'expo-firebase-analytics' 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) Analytics.logEvent(event, params)
} }

View File

@ -1,4 +1,7 @@
export default { export default {
index: {
localCorrupt: 'Login expired, please login again'
},
buttons: { buttons: {
cancel: 'Cancel' cancel: 'Cancel'
}, },

View File

@ -57,6 +57,7 @@ export default {
application: 'Tooted with {{application}}' application: 'Tooted with {{application}}'
}, },
conversation: { conversation: {
withAccounts: 'With',
delete: { delete: {
function: 'Delete direct message' function: 'Delete direct message'
} }

View File

@ -1,4 +1,7 @@
export default { export default {
index: {
localCorrupt: '登录已过期,请重新登录'
},
buttons: { buttons: {
cancel: '取消' cancel: '取消'
}, },

View File

@ -57,6 +57,7 @@ export default {
application: '发自于 {{application}}' application: '发自于 {{application}}'
}, },
conversation: { conversation: {
withAccounts: '与',
delete: { delete: {
function: '删除私信' function: '删除私信'
} }
@ -141,7 +142,7 @@ export default {
}, },
count: { count: {
voters: '已投{{count}}人 • ', voters: '已投{{count}}人 • ',
votes: '{{count}}票 • ' votes: '已投{{count}}票 • '
}, },
expiration: { expiration: {
expired: '投票已结束', expired: '投票已结束',

View File

@ -3,7 +3,6 @@ import { useNavigation } from '@react-navigation/native'
import TimelineEmpty from '@root/components/Timelines/Timeline/Empty' import TimelineEmpty from '@root/components/Timelines/Timeline/Empty'
import { useListsQuery } from '@utils/queryHooks/lists' import { useListsQuery } from '@utils/queryHooks/lists'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { StyleSheet } from 'react-native'
const ScreenMeLists: React.FC = () => { const ScreenMeLists: React.FC = () => {
const navigation = useNavigation() const navigation = useNavigation()
@ -32,11 +31,4 @@ const ScreenMeLists: React.FC = () => {
return <>{children}</> return <>{children}</>
} }
const styles = StyleSheet.create({
loading: {
flex: 1,
alignItems: 'center'
}
})
export default ScreenMeLists export default ScreenMeLists

View File

@ -22,7 +22,7 @@ const MyInfo: React.FC<Props> = ({ setData }) => {
return ( return (
<> <>
<AccountHeader account={data} limitHeight /> <AccountHeader account={data} limitHeight />
<AccountInformation account={data} ownAccount /> <AccountInformation account={data} myInfo />
</> </>
) )
} }

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
@ -129,7 +130,12 @@ const ScreenMeSettings: React.FC = () => {
}, },
buttonIndex => { buttonIndex => {
if (buttonIndex < options.length) { if (buttonIndex < options.length) {
analytics('settings_language_press', {
current: i18n.language,
new: availableLanguages[buttonIndex]
})
haptics('Success') haptics('Success')
// @ts-ignore
dispatch(changeLanguage(availableLanguages[buttonIndex])) dispatch(changeLanguage(availableLanguages[buttonIndex]))
i18n.changeLanguage(availableLanguages[buttonIndex]) i18n.changeLanguage(availableLanguages[buttonIndex])
} }
@ -156,15 +162,27 @@ const ScreenMeSettings: React.FC = () => {
buttonIndex => { buttonIndex => {
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
analytics('settings_appearance_press', {
current: settingsTheme,
new: 'auto'
})
haptics('Success') haptics('Success')
dispatch(changeTheme('auto')) dispatch(changeTheme('auto'))
break break
case 1: case 1:
analytics('settings_appearance_press', {
current: settingsTheme,
new: 'light'
})
haptics('Success') haptics('Success')
dispatch(changeTheme('light')) dispatch(changeTheme('light'))
setTheme('light') setTheme('light')
break break
case 2: case 2:
analytics('settings_appearance_press', {
current: settingsTheme,
new: 'dark'
})
haptics('Success') haptics('Success')
dispatch(changeTheme('dark')) dispatch(changeTheme('dark'))
setTheme('dark') setTheme('dark')
@ -192,10 +210,18 @@ const ScreenMeSettings: React.FC = () => {
buttonIndex => { buttonIndex => {
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
analytics('settings_browser_press', {
current: settingsBrowser,
new: 'internal'
})
haptics('Success') haptics('Success')
dispatch(changeBrowser('internal')) dispatch(changeBrowser('internal'))
break break
case 1: case 1:
analytics('settings_browser_press', {
current: settingsBrowser,
new: 'external'
})
haptics('Success') haptics('Success')
dispatch(changeBrowser('external')) dispatch(changeBrowser('external'))
break break
@ -220,6 +246,9 @@ const ScreenMeSettings: React.FC = () => {
} }
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={async () => { onPress={async () => {
analytics('settings_cache_press', {
size: cacheSize ? prettyBytes(cacheSize) : 'empty'
})
await CacheManager.clearCache() await CacheManager.clearCache()
haptics('Success') haptics('Success')
setCacheSize(0) setCacheSize(0)
@ -237,7 +266,10 @@ const ScreenMeSettings: React.FC = () => {
/> />
} }
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => Linking.openURL('https://www.patreon.com/xmflsct')} onPress={() => {
analytics('settings_support_press')
Linking.openURL('https://www.patreon.com/xmflsct')
}}
/> />
<MenuRow <MenuRow
title={t('content.review.heading')} title={t('content.review.heading')}
@ -249,11 +281,12 @@ const ScreenMeSettings: React.FC = () => {
/> />
} }
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => onPress={() => {
analytics('settings_review_press')
StoreReview.isAvailableAsync().then(() => StoreReview.isAvailableAsync().then(() =>
StoreReview.requestReview() StoreReview.requestReview()
) )
} }}
/> />
</MenuContainer> </MenuContainer>
<MenuContainer> <MenuContainer>

View File

@ -1,4 +1,5 @@
import { HeaderCenter, HeaderLeft } from '@components/Header' import { HeaderCenter, HeaderLeft } from '@components/Header'
import { StackScreenProps } from '@react-navigation/stack'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform, StyleSheet } from 'react-native' import { Platform, StyleSheet } from 'react-native'
@ -7,7 +8,10 @@ import ScreenMeSwitchRoot from './Switch/Root'
const Stack = createNativeStackNavigator() const Stack = createNativeStackNavigator()
const ScreenMeSwitch: React.FC = ({ navigation }) => { const ScreenMeSwitch: React.FC<StackScreenProps<
Nav.MeStackParamList,
'Screen-Me-Switch'
>> = ({ navigation }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Stack.Navigator <Stack.Navigator

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import ComponentInstance from '@components/Instance' import ComponentInstance from '@components/Instance'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
@ -47,6 +48,7 @@ const AccountButton: React.FC<Props> = ({
disabled ? ' ✓' : '' disabled ? ' ✓' : ''
}`} }`}
onPress={() => { onPress={() => {
analytics('switch_existing_press')
dispatch(localUpdateActiveIndex(index)) dispatch(localUpdateActiveIndex(index))
queryClient.clear() queryClient.clear()
navigation.goBack() navigation.goBack()

View File

@ -34,7 +34,7 @@ const ScreenNotifications: React.FC = () => {
} }
</Stack.Screen> </Stack.Screen>
{sharedScreens(Stack)} {sharedScreens(Stack as any)}
</Stack.Navigator> </Stack.Navigator>
) )
} }

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import BottomSheet from '@components/BottomSheet' import BottomSheet from '@components/BottomSheet'
import { HeaderRight } from '@components/Header' import { HeaderRight } from '@components/Header'
import Timeline from '@components/Timelines/Timeline' import Timeline from '@components/Timelines/Timeline'
@ -49,7 +50,12 @@ const ScreenSharedAccount: React.FC<SharedAccountProp> = ({
headerRight: () => ( headerRight: () => (
<HeaderRight <HeaderRight
content='MoreHorizontal' content='MoreHorizontal'
onPress={() => setBottomSheetVisible(true)} onPress={() => {
analytics('bottomsheet_open_press', {
page: 'account'
})
setBottomSheetVisible(true)
}}
/> />
) )
}) })

View File

@ -1,6 +1,8 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { useTimelineQuery } from '@utils/queryHooks/timeline' import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
@ -22,7 +24,9 @@ export interface Props {
const AccountAttachments = React.memo( const AccountAttachments = React.memo(
({ account }: Props) => { ({ account }: Props) => {
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const { theme } = useTheme() const { theme } = useTheme()
const width = const width =
@ -58,9 +62,11 @@ const AccountAttachments = React.memo(
if (index === 3) { if (index === 3) {
return ( return (
<Pressable <Pressable
onPress={() => onPress={() => {
navigation.push('Screen-Shared-Attachments', { account }) analytics('account_attachment_more_press')
} account &&
navigation.push('Screen-Shared-Attachments', { account })
}}
children={ children={
<View <View
style={{ style={{
@ -92,9 +98,10 @@ const AccountAttachments = React.memo(
blurhash={item.media_attachments[0].blurhash} blurhash={item.media_attachments[0].blurhash}
dimension={{ width: width, height: width }} dimension={{ width: width, height: width }}
style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }} style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }}
onPress={() => onPress={() => {
analytics('account_attachment_item_press')
navigation.push('Screen-Shared-Toot', { toot: item }) navigation.push('Screen-Shared-Toot', { toot: item })
} }}
/> />
) )
} }

View File

@ -1,7 +1,9 @@
import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import { Placeholder, Fade } from 'rn-placeholder' import { Placeholder, Fade } from 'rn-placeholder'
import AccountInformationAccount from './Information/Account' import AccountInformationAccount from './Information/Account'
import AccountInformationActions from './Information/Actions' import AccountInformationActions from './Information/Actions'
@ -15,13 +17,11 @@ import AccountInformationSwitch from './Information/Switch'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
ownAccount?: boolean myInfo?: boolean // Showing from my info page
} }
const AccountInformation: React.FC<Props> = ({ const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => {
account, const ownAccount = account?.id === useSelector(getLocalAccount)?.id
ownAccount = false
}) => {
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const animation = useCallback( const animation = useCallback(
@ -35,21 +35,24 @@ const AccountInformation: React.FC<Props> = ({
<View style={styles.base}> <View style={styles.base}>
<Placeholder Animation={animation}> <Placeholder Animation={animation}>
<View style={styles.avatarAndActions}> <View style={styles.avatarAndActions}>
<AccountInformationAvatar account={account} /> <AccountInformationAvatar account={account} myInfo={myInfo} />
<View style={styles.actions}> <View style={styles.actions}>
{ownAccount ? ( {myInfo ? (
<AccountInformationSwitch /> <AccountInformationSwitch />
) : ( ) : (
<AccountInformationActions account={account} /> <AccountInformationActions
account={account}
ownAccount={ownAccount}
/>
)} )}
</View> </View>
</View> </View>
<AccountInformationName account={account} /> <AccountInformationName account={account} />
<AccountInformationAccount account={account} ownAccount={ownAccount} /> <AccountInformationAccount account={account} myInfo={myInfo} />
{!ownAccount ? ( {!myInfo ? (
<> <>
{account?.fields && account.fields.length > 0 ? ( {account?.fields && account.fields.length > 0 ? (
<AccountInformationFields account={account} /> <AccountInformationFields account={account} />
@ -64,7 +67,7 @@ const AccountInformation: React.FC<Props> = ({
</> </>
) : null} ) : null}
<AccountInformationStats account={account} ownAccount={ownAccount} /> <AccountInformationStats account={account} myInfo={myInfo} />
</Placeholder> </Placeholder>
</View> </View>
) )

View File

@ -9,13 +9,10 @@ import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
ownAccount?: boolean myInfo: boolean
} }
const AccountInformationAccount: React.FC<Props> = ({ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
account,
ownAccount
}) => {
const { theme } = useTheme() const { theme } = useTheme()
const localAccount = useSelector(getLocalAccount) const localAccount = useSelector(getLocalAccount)
const localUri = useSelector(getLocalUri) const localUri = useSelector(getLocalUri)
@ -45,7 +42,7 @@ const AccountInformationAccount: React.FC<Props> = ({
} }
}, [account?.moved]) }, [account?.moved])
if (account || (ownAccount && localAccount !== undefined)) { if (account || (myInfo && localAccount !== undefined)) {
return ( return (
<View <View
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]} style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
@ -60,8 +57,8 @@ const AccountInformationAccount: React.FC<Props> = ({
]} ]}
selectable selectable
> >
@{ownAccount ? localAccount?.acct : account?.acct} @{myInfo ? localAccount?.acct : account?.acct}
{ownAccount ? `@${localUri}` : null} {myInfo ? `@${localUri}` : null}
</Text> </Text>
{movedContent} {movedContent}
{account?.locked ? ( {account?.locked ? (

View File

@ -1,6 +1,8 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import { RelationshipOutgoing } from '@components/Relationship' import { RelationshipOutgoing } from '@components/Relationship'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { useRelationshipQuery } from '@utils/queryHooks/relationship' import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
@ -9,22 +11,25 @@ import { StyleSheet } from 'react-native'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
ownAccount: boolean
} }
const GoToMoved = ({ account }: { account: Mastodon.Account }) => { const GoToMoved = ({ accountMoved }: { accountMoved: Mastodon.Account }) => {
const { t } = useTranslation('sharedAccount') const { t } = useTranslation('sharedAccount')
const navigation = useNavigation() const navigation = useNavigation<
const query = useRelationshipQuery({ id: account.id }) StackNavigationProp<Nav.LocalStackParamList>
>()
return query.data && !query.data.blocked_by ? ( return (
<Button <Button
type='text' type='text'
content={t('content.moved')} content={t('content.moved')}
onPress={() => onPress={() => {
navigation.push('Screen-Shared-Account', { account: account.moved }) analytics('account_gotomoved_press')
} navigation.push('Screen-Shared-Account', { account: accountMoved })
}}
/> />
) : null )
} }
const Conversation = ({ account }: { account: Mastodon.Account }) => { const Conversation = ({ account }: { account: Mastodon.Account }) => {
@ -37,26 +42,30 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => {
type='icon' type='icon'
content='Mail' content='Mail'
style={styles.actionConversation} style={styles.actionConversation}
onPress={() => onPress={() => {
analytics('account_DM_press')
navigation.navigate('Screen-Shared-Compose', { navigation.navigate('Screen-Shared-Compose', {
type: 'conversation', type: 'conversation',
incomingStatus: { account } accts: [account.acct]
}) })
} }}
/> />
) : null ) : null
} }
const AccountInformationActions: React.FC<Props> = ({ account }) => { const AccountInformationActions: React.FC<Props> = ({
account,
ownAccount
}) => {
return account && account.id ? ( return account && account.id ? (
account.moved ? ( account.moved ? (
<GoToMoved account={account} /> <GoToMoved accountMoved={account.moved} />
) : ( ) : !ownAccount ? (
<> <>
<Conversation account={account} /> <Conversation account={account} />
<RelationshipOutgoing id={account.id} /> <RelationshipOutgoing id={account.id} />
</> </>
) ) : null
) : null ) : null
} }

View File

@ -1,13 +1,20 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { StyleSheet } from 'react-native' import { Pressable, StyleSheet } from 'react-native'
export interface Props { export interface Props {
account: Mastodon.Account | undefined 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( const dimension = useMemo(
() => ({ () => ({
width: StyleConstants.Avatar.L, width: StyleConstants.Avatar.L,
@ -17,11 +24,21 @@ const AccountInformationAvatar: React.FC<Props> = ({ account }) => {
) )
return ( return (
<GracefullyImage <Pressable
style={styles.base} disabled={!myInfo}
uri={{ original: account?.avatar }} onPress={() => {
dimension={dimension} analytics('account_avatar_press')
/> myInfo &&
account &&
navigation.push('Screen-Shared-Account', { account })
}}
>
<GracefullyImage
style={styles.base}
uri={{ original: account?.avatar }}
dimension={dimension}
/>
</Pressable>
) )
} }

View File

@ -1,4 +1,6 @@
import analytics from '@components/analytics'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@root/utils/styles/constants' import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager' import { useTheme } from '@root/utils/styles/ThemeManager'
import React from 'react' import React from 'react'
@ -8,11 +10,13 @@ import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
ownAccount?: boolean myInfo: boolean
} }
const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => { const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation('sharedAccount') const { t } = useTranslation('sharedAccount')
@ -24,9 +28,12 @@ const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => {
children={t('content.summary.statuses_count', { children={t('content.summary.statuses_count', {
count: account?.statuses_count || 0 count: account?.statuses_count || 0
})} })}
onPress={() => onPress={() => {
ownAccount && navigation.push('Screen-Shared-Account', { account }) analytics('account_stats_toots_press', {
} count: account.statuses_count
})
myInfo && navigation.push('Screen-Shared-Account', { account })
}}
/> />
) : ( ) : (
<PlaceholderLine <PlaceholderLine
@ -43,12 +50,15 @@ const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => {
children={t('content.summary.following_count', { children={t('content.summary.following_count', {
count: account?.following_count || 0 count: account?.following_count || 0
})} })}
onPress={() => onPress={() => {
analytics('account_stats_following_press', {
count: account.following_count
})
navigation.push('Screen-Shared-Relationships', { navigation.push('Screen-Shared-Relationships', {
account, account,
initialType: 'following' initialType: 'following'
}) })
} }}
/> />
) : ( ) : (
<PlaceholderLine <PlaceholderLine
@ -65,12 +75,15 @@ const AccountInformationStats: React.FC<Props> = ({ account, ownAccount }) => {
children={t('content.summary.followers_count', { children={t('content.summary.followers_count', {
count: account?.followers_count || 0 count: account?.followers_count || 0
})} })}
onPress={() => onPress={() => {
analytics('account_stats_followers_press', {
count: account.followers_count
})
navigation.push('Screen-Shared-Relationships', { navigation.push('Screen-Shared-Relationships', {
account, account,
initialType: 'followers' initialType: 'followers'
}) })
} }}
/> />
) : ( ) : (
<PlaceholderLine <PlaceholderLine

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
@ -108,14 +109,17 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
: theme.background : theme.background
} }
]} ]}
onPress={() => onPress={() => {
analytics('accnouncement_reaction_press', {
current: reaction.me
})
mutation.mutate({ mutation.mutate({
id: item.id, id: item.id,
type: 'reaction', type: 'reaction',
name: reaction.name, name: reaction.name,
me: reaction.me me: reaction.me
}) })
} }}
> >
{reaction.url ? ( {reaction.url ? (
<Image <Image
@ -153,13 +157,14 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
} }
loading={mutation.isLoading} loading={mutation.isLoading}
disabled={item.read} disabled={item.read}
onPress={() => onPress={() => {
analytics('accnouncement_read_press')
!item.read && !item.read &&
mutation.mutate({ mutation.mutate({
id: item.id, id: item.id,
type: 'dismiss' type: 'dismiss'
}) })
} }}
/> />
</View> </View>
</View> </View>

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import haptics from '@root/components/haptics' import haptics from '@root/components/haptics'
import { store } from '@root/store' import { store } from '@root/store'
@ -93,20 +94,11 @@ const Compose: React.FC<SharedComposeProp> = ({
}) })
break break
case 'reply': case 'reply':
const actualStatus =
params.incomingStatus.reblog || params.incomingStatus
formatText({
textInput: 'text',
composeDispatch,
content: `@${actualStatus.account.acct} `,
disableDebounce: true
})
break
case 'conversation': case 'conversation':
formatText({ formatText({
textInput: 'text', textInput: 'text',
composeDispatch, composeDispatch,
content: `@${params.incomingStatus.account.acct} `, content: params.accts.map(acct => `@${acct}`).join(' ') + ' ',
disableDebounce: true disableDebounce: true
}) })
break break
@ -123,23 +115,32 @@ const Compose: React.FC<SharedComposeProp> = ({
type='text' type='text'
content={t('heading.left.button')} content={t('heading.left.button')}
onPress={() => { onPress={() => {
analytics('compose_header_back_press')
if ( if (
totalTextCount === 0 && totalTextCount === 0 &&
composeState.attachments.uploads.length === 0 && composeState.attachments.uploads.length === 0 &&
composeState.poll.active === false composeState.poll.active === false
) { ) {
analytics('compose_header_back_empty')
navigation.goBack() navigation.goBack()
return return
} else { } else {
analytics('compose_header_back_state_occupied')
Alert.alert(t('heading.left.alert.title'), undefined, [ Alert.alert(t('heading.left.alert.title'), undefined, [
{ {
text: t('heading.left.alert.buttons.exit'), text: t('heading.left.alert.buttons.exit'),
style: 'destructive', style: 'destructive',
onPress: () => navigation.goBack() onPress: () => {
analytics('compose_header_back_occupied_confirm')
navigation.goBack()
}
}, },
{ {
text: t('heading.left.alert.buttons.continue'), 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') : t('heading.right.button.default')
} }
onPress={() => { onPress={() => {
analytics('compose_header_post_press')
composeDispatch({ type: 'posting', payload: true }) composeDispatch({ type: 'posting', payload: true })
composePost(params, composeState) composePost(params, composeState)
@ -186,13 +188,18 @@ const Compose: React.FC<SharedComposeProp> = ({
] ]
queryClient.invalidateQueries(queryKey) queryClient.invalidateQueries(queryKey)
if (params?.queryKey && params.queryKey[1].page === 'Toot') { switch (params?.type) {
queryClient.invalidateQueries(params.queryKey) case 'edit':
case 'reply':
if (params?.queryKey && params.queryKey[1].page === 'Toot') {
queryClient.invalidateQueries(params.queryKey)
}
break
} }
navigation.goBack() navigation.goBack()
}) })
.catch(error => { .catch(error => {
// Sentry.Native.captureException(error) Sentry.Native.captureException(error)
haptics('Error') haptics('Error')
composeDispatch({ type: 'posting', payload: false }) composeDispatch({ type: 'posting', payload: false })
Alert.alert(t('heading.right.alert.title'), undefined, [ Alert.alert(t('heading.right.alert.title'), undefined, [

View File

@ -1,4 +1,5 @@
import client from '@api/client' import client from '@api/client'
import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import React, { import React, {
@ -87,6 +88,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
content={t('content.editAttachment.header.right.button')} content={t('content.editAttachment.header.right.button')}
loading={isSubmitting} loading={isSubmitting}
onPress={() => { onPress={() => {
analytics('editattachment_confirm_press')
if (!altText && focus.current.x === 0 && focus.current.y === 0) { if (!altText && focus.current.x === 0 && focus.current.y === 0) {
navigation.goBack() navigation.goBack()
return return

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -28,6 +29,9 @@ const ComposeActions: React.FC = () => {
if (composeState.poll.active) return if (composeState.poll.active) return
if (composeState.attachments.uploads.length < 4) { if (composeState.attachments.uploads.length < 4) {
analytics('compose_actions_attachment_press', {
count: composeState.attachments.uploads.length
})
return await addAttachment({ return await addAttachment({
composeDispatch, composeDispatch,
showActionSheetWithOptions showActionSheetWithOptions
@ -46,6 +50,9 @@ const ComposeActions: React.FC = () => {
}, [composeState.poll.active, composeState.attachments.uploads]) }, [composeState.poll.active, composeState.attachments.uploads])
const pollOnPress = useCallback(() => { const pollOnPress = useCallback(() => {
if (!composeState.attachments.uploads.length) { if (!composeState.attachments.uploads.length) {
analytics('compose_actions_poll_press', {
current: composeState.poll.active
})
layoutAnimation() layoutAnimation()
composeDispatch({ composeDispatch({
type: 'poll', type: 'poll',
@ -86,24 +93,43 @@ const ComposeActions: React.FC = () => {
buttonIndex => { buttonIndex => {
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
analytics('compose_actions_visibility_press', {
current: composeState.visibility,
new: 'public'
})
composeDispatch({ type: 'visibility', payload: 'public' }) composeDispatch({ type: 'visibility', payload: 'public' })
break break
case 1: case 1:
analytics('compose_actions_visibility_press', {
current: composeState.visibility,
new: 'unlisted'
})
composeDispatch({ type: 'visibility', payload: 'unlisted' }) composeDispatch({ type: 'visibility', payload: 'unlisted' })
break break
case 2: case 2:
analytics('compose_actions_visibility_press', {
current: composeState.visibility,
new: 'private'
})
composeDispatch({ type: 'visibility', payload: 'private' }) composeDispatch({ type: 'visibility', payload: 'private' })
break break
case 3: case 3:
analytics('compose_actions_visibility_press', {
current: composeState.visibility,
new: 'direct'
})
composeDispatch({ type: 'visibility', payload: 'direct' }) composeDispatch({ type: 'visibility', payload: 'direct' })
break break
} }
} }
) )
} }
}, []) }, [composeState.visibility])
const spoilerOnPress = useCallback(() => { const spoilerOnPress = useCallback(() => {
analytics('compose_actions_spoiler_press', {
current: composeState.spoiler.active
})
if (composeState.spoiler.active) { if (composeState.spoiler.active) {
composeState.textInputFocus.refs.text.current?.focus() composeState.textInputFocus.refs.text.current?.focus()
} }
@ -124,20 +150,15 @@ const ComposeActions: React.FC = () => {
} }
}, [composeState.emoji.active, composeState.emoji.emojis]) }, [composeState.emoji.active, composeState.emoji.emojis])
const emojiOnPress = useCallback(() => { const emojiOnPress = useCallback(() => {
analytics('compose_actions_emojis_press', {
current: composeState.emoji.active
})
if (composeState.emoji.emojis) { if (composeState.emoji.emojis) {
if (composeState.emoji.active) { layoutAnimation()
layoutAnimation() composeDispatch({
composeDispatch({ type: 'emoji',
type: 'emoji', payload: { ...composeState.emoji, active: !composeState.emoji.active }
payload: { ...composeState.emoji, active: false } })
})
} else {
layoutAnimation()
composeDispatch({
type: 'emoji',
payload: { ...composeState.emoji, active: true }
})
}
} }
}, [composeState.emoji.active, composeState.emoji.emojis]) }, [composeState.emoji.active, composeState.emoji.emojis])

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
@ -39,14 +40,15 @@ const ComposeAttachments: React.FC = () => {
const flatListRef = useRef<FlatList>(null) const flatListRef = useRef<FlatList>(null)
let prevOffsets = useRef<number[]>() let prevOffsets = useRef<number[]>()
const sensitiveOnPress = useCallback( const sensitiveOnPress = useCallback(() => {
() => analytics('compose_attachment_sensitive_press', {
composeDispatch({ current: composeState.attachments.sensitive
type: 'attachments/sensitive', })
payload: { sensitive: !composeState.attachments.sensitive } composeDispatch({
}), type: 'attachments/sensitive',
[composeState.attachments.sensitive] payload: { sensitive: !composeState.attachments.sensitive }
) })
}, [composeState.attachments.sensitive])
const calculateWidth = useCallback(item => { const calculateWidth = useCallback(item => {
if (item.local) { if (item.local) {
@ -158,6 +160,7 @@ const ComposeAttachments: React.FC = () => {
round round
overlay overlay
onPress={() => { onPress={() => {
analytics('compose_attachment_delete')
layoutAnimation() layoutAnimation()
composeDispatch({ composeDispatch({
type: 'attachment/delete', type: 'attachment/delete',
@ -172,11 +175,12 @@ const ComposeAttachments: React.FC = () => {
spacing='M' spacing='M'
round round
overlay overlay
onPress={() => onPress={() => {
analytics('compose_attachment_edit')
navigation.navigate('Screen-Shared-Compose-EditAttachment', { navigation.navigate('Screen-Shared-Compose-EditAttachment', {
index index
}) })
} }}
/> />
</View> </View>
)} )}
@ -196,9 +200,10 @@ const ComposeAttachments: React.FC = () => {
backgroundColor: theme.backgroundOverlay backgroundColor: theme.backgroundOverlay
} }
]} ]}
onPress={async () => onPress={async () => {
analytics('compose_attachment_add_container_press')
await addAttachment({ composeDispatch, showActionSheetWithOptions }) await addAttachment({ composeDispatch, showActionSheetWithOptions })
} }}
> >
<Button <Button
type='icon' type='icon'
@ -206,9 +211,10 @@ const ComposeAttachments: React.FC = () => {
spacing='M' spacing='M'
round round
overlay overlay
onPress={async () => onPress={async () => {
analytics('compose_attachment_add_button_press')
await addAttachment({ composeDispatch, showActionSheetWithOptions }) await addAttachment({ composeDispatch, showActionSheetWithOptions })
} }}
style={{ style={{
position: 'absolute', position: 'absolute',
top: top:

View File

@ -12,10 +12,12 @@ import {
} from 'react-native' } from 'react-native'
import ComposeContext from '../../utils/createContext' import ComposeContext from '../../utils/createContext'
import updateText from '../../updateText' import updateText from '../../updateText'
import analytics from '@components/analytics'
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => { const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const onPress = useCallback(() => { const onPress = useCallback(() => {
analytics('compose_emoji_add')
updateText({ updateText({
composeState, composeState,
composeDispatch, composeDispatch,

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { MenuRow } from '@components/Menu' import { MenuRow } from '@components/Menu'
@ -79,6 +80,7 @@ const ComposePoll: React.FC = () => {
<View style={styles.firstButton}> <View style={styles.firstButton}>
<Button <Button
onPress={() => { onPress={() => {
analytics('compose_poll_reduce_press')
total > 2 && total > 2 &&
composeDispatch({ composeDispatch({
type: 'poll', type: 'poll',
@ -93,6 +95,7 @@ const ComposePoll: React.FC = () => {
</View> </View>
<Button <Button
onPress={() => { onPress={() => {
analytics('compose_poll_increase_press')
total < 4 && total < 4 &&
composeDispatch({ composeDispatch({
type: 'poll', type: 'poll',
@ -122,12 +125,18 @@ const ComposePoll: React.FC = () => {
], ],
cancelButtonIndex: 2 cancelButtonIndex: 2
}, },
index => index => {
index < 2 && if (index < 2) {
composeDispatch({ analytics('compose_poll_expiration_press', {
type: 'poll', current: multiple,
payload: { multiple: index === 1 } new: index === 1
}) })
composeDispatch({
type: 'poll',
payload: { multiple: index === 1 }
})
}
}
) )
} }
iconBack='ChevronRight' iconBack='ChevronRight'
@ -155,12 +164,18 @@ const ComposePoll: React.FC = () => {
], ],
cancelButtonIndex: 7 cancelButtonIndex: 7
}, },
index => index => {
index < 7 && if (index < 7) {
composeDispatch({ analytics('compose_poll_expiration_press', {
type: 'poll', current: expire,
payload: { expire: expirations[index] } new: expirations[index]
}) })
composeDispatch({
type: 'poll',
payload: { expire: expirations[index] }
})
}
}
) )
}} }}
iconBack='ChevronRight' iconBack='ChevronRight'

View File

@ -8,6 +8,7 @@ import { Alert, Linking } from 'react-native'
import { ComposeAction } from '../../utils/types' import { ComposeAction } from '../../utils/types'
import { ActionSheetOptions } from '@expo/react-native-action-sheet' import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import i18next from 'i18next' import i18next from 'i18next'
import analytics from '@components/analytics'
export interface Props { export interface Props {
composeDispatch: Dispatch<ComposeAction> composeDispatch: Dispatch<ComposeAction>
@ -160,14 +161,23 @@ const addAttachment = async ({
'sharedCompose:content.root.actions.attachment.actions.library.alert.buttons.cancel' 'sharedCompose:content.root.actions.attachment.actions.library.alert.buttons.cancel'
), ),
style: 'cancel', style: 'cancel',
onPress: () => {} onPress: () => {
analytics('compose_addattachment_medialibrary_nopermission', {
action: 'cancel'
})
}
}, },
{ {
text: i18next.t( text: i18next.t(
'sharedCompose:content.root.actions.attachment.actions.library.alert.buttons.settings' 'sharedCompose:content.root.actions.attachment.actions.library.alert.buttons.settings'
), ),
style: 'default', 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' 'sharedCompose:content.root.actions.attachment.actions.photo.alert.buttons.cancel'
), ),
style: 'cancel', style: 'cancel',
onPress: () => {} onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'cancel'
})
}
}, },
{ {
text: i18next.t( text: i18next.t(
'sharedCompose:content.root.actions.attachment.actions.photo.alert.buttons.settings' 'sharedCompose:content.root.actions.attachment.actions.photo.alert.buttons.settings'
), ),
style: 'default', style: 'default',
onPress: () => Linking.openURL('app-settings:') onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
} }
] ]
) )

View File

@ -7,7 +7,7 @@ import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext' import ComposeContext from '../utils/createContext'
import PostingAs from './Header/PostingAs' import ComposePostingAs from './Header/PostingAs'
import ComposeSpoilerInput from './Header/SpoilerInput' import ComposeSpoilerInput from './Header/SpoilerInput'
import ComposeTextInput from './Header/TextInput' import ComposeTextInput from './Header/TextInput'
@ -22,10 +22,7 @@ const ComposeRootHeader: React.FC = () => {
localInstances.length && localInstances.length &&
localInstances.length > 1 && ( localInstances.length > 1 && (
<View style={styles.postingAs}> <View style={styles.postingAs}>
<PostingAs <ComposePostingAs />
id={localInstances[localActiveIndex].account.id}
domain={localInstances[localActiveIndex].uri}
/>
</View> </View>
)} )}
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null} {composeState.spoiler.active ? <ComposeSpoilerInput /> : null}

View File

@ -1,39 +1,30 @@
import { useAccountQuery } from '@utils/queryHooks/account' import { getLocalAccount, getLocalUri } from '@utils/slices/instancesSlice'
import { InstanceLocal } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, Text } from 'react-native' import { StyleSheet, Text } from 'react-native'
import { Chase } from 'react-native-animated-spinkit' import { useSelector } from 'react-redux'
const ComposePostingAs: React.FC<{ const ComposePostingAs = React.memo(
id: Mastodon.Account['id'] () => {
domain: InstanceLocal['url'] const { t } = useTranslation('sharedCompose')
}> = ({ id, domain }) => { const { theme } = useTheme()
const { t } = useTranslation('sharedCompose')
const { theme } = useTheme()
const { data, status } = useAccountQuery({ id }) const localAccount = useSelector(getLocalAccount)
const localUri = useSelector(getLocalUri)
switch (status) { return (
case 'loading': <Text style={[styles.text, { color: theme.secondary }]}>
return ( {t('content.root.header.postingAs', {
<Chase acct: localAccount?.acct,
size={StyleConstants.Font.LineHeight.M - 2} domain: localUri
color={theme.secondary} })}
/> </Text>
) )
case 'success': },
return ( () => true
<Text style={[styles.text, { color: theme.secondary }]}> )
{t('content.root.header.postingAs', { acct: data?.acct, domain })}
</Text>
)
default:
return null
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
text: { text: {

View File

@ -1,4 +1,5 @@
import ComponentAccount from '@components/Account' import ComponentAccount from '@components/Account'
import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import ComponentHashtag from '@components/Hashtag' import ComponentHashtag from '@components/Hashtag'
import React, { Dispatch, useCallback } from 'react' import React, { Dispatch, useCallback } from 'react'
@ -16,6 +17,9 @@ const ComposeRootSuggestion = React.memo(
composeDispatch: Dispatch<ComposeAction> composeDispatch: Dispatch<ComposeAction>
}) => { }) => {
const onPress = useCallback(() => { const onPress = useCallback(() => {
analytics('compose_suggestion_press', {
type: item.acct ? 'account' : 'hashtag'
})
const focusedInput = composeState.textInputFocus.current const focusedInput = composeState.textInputFocus.current
updateText({ updateText({
composeState: { composeState: {
@ -36,9 +40,9 @@ const ComposeRootSuggestion = React.memo(
}, []) }, [])
return item.acct ? ( 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 () => true

View File

@ -3,51 +3,50 @@ import { getLocalAccount } from '@utils/slices/instancesSlice'
import composeInitialState from './initialState' import composeInitialState from './initialState'
import { ComposeState } from './types' import { ComposeState } from './types'
export interface Props { const composeParseState = (
type: 'reply' | 'conversation' | 'edit' params: NonNullable<Nav.SharedStackParamList['Screen-Shared-Compose']>
incomingStatus: Mastodon.Status ): ComposeState => {
} switch (params.type) {
const composeParseState = ({ type, incomingStatus }: Props): ComposeState => {
switch (type) {
case 'edit': case 'edit':
return { return {
...composeInitialState, ...composeInitialState,
...(incomingStatus.spoiler_text && { ...(params.incomingStatus.spoiler_text && {
spoiler: { ...composeInitialState.spoiler, active: true } spoiler: { ...composeInitialState.spoiler, active: true }
}), }),
...(incomingStatus.poll && { ...(params.incomingStatus.poll && {
poll: { poll: {
active: true, active: true,
total: incomingStatus.poll.options.length, total: params.incomingStatus.poll.options.length,
options: { options: {
'0': incomingStatus.poll.options[0]?.title || undefined, '0': params.incomingStatus.poll.options[0]?.title || undefined,
'1': incomingStatus.poll.options[1]?.title || undefined, '1': params.incomingStatus.poll.options[1]?.title || undefined,
'2': incomingStatus.poll.options[2]?.title || undefined, '2': params.incomingStatus.poll.options[2]?.title || undefined,
'3': incomingStatus.poll.options[3]?.title || undefined '3': params.incomingStatus.poll.options[3]?.title || undefined
}, },
multiple: incomingStatus.poll.multiple, multiple: params.incomingStatus.poll.multiple,
expire: '86400' // !!! expire: '86400' // !!!
} }
}), }),
...(incomingStatus.media_attachments && { ...(params.incomingStatus.media_attachments && {
attachments: { attachments: {
sensitive: incomingStatus.sensitive, sensitive: params.incomingStatus.sensitive,
uploads: incomingStatus.media_attachments.map(media => ({ uploads: params.incomingStatus.media_attachments.map(media => ({
remote: media remote: media
})) }))
} }
}), }),
visibility: visibility:
incomingStatus.visibility || params.incomingStatus.visibility ||
getLocalAccount(store.getState())?.preferences[ getLocalAccount(store.getState())?.preferences[
'posting:default:visibility' 'posting:default:visibility'
] || ] ||
'public', 'public',
...(incomingStatus.visibility === 'direct' && { visibilityLock: true }) ...(params.incomingStatus.visibility === 'direct' && {
visibilityLock: true
})
} }
case 'reply': case 'reply':
const actualStatus = incomingStatus.reblog || incomingStatus const actualStatus = params.incomingStatus.reblog || params.incomingStatus
return { return {
...composeInitialState, ...composeInitialState,
visibility: actualStatus.visibility, visibility: actualStatus.visibility,
@ -57,12 +56,6 @@ const composeParseState = ({ type, incomingStatus }: Props): ComposeState => {
case 'conversation': case 'conversation':
return { return {
...composeInitialState, ...composeInitialState,
text: {
count: incomingStatus.account.acct.length + 2,
raw: `@${incomingStatus.account.acct} `,
formatted: undefined,
selection: { start: 0, end: 0 }
},
visibility: 'direct', visibility: 'direct',
visibilityLock: true visibilityLock: true
} }

View File

@ -1,8 +1,16 @@
import analytics from '@components/analytics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import React, { useCallback, useState } from 'react' 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 ImageViewer from 'react-native-image-zoom-viewer'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
@ -47,6 +55,7 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({
) )
const onPress = useCallback(() => { const onPress = useCallback(() => {
analytics('imageviewer_share_press')
switch (Platform.OS) { switch (Platform.OS) {
case 'ios': case 'ios':
return Share.share({ url: imageUrls[currentIndex].url }) return Share.share({ url: imageUrls[currentIndex].url })

View File

@ -3,6 +3,7 @@ import ComponentSeparator from '@components/Separator'
import TimelineEmpty from '@components/Timelines/Timeline/Empty' import TimelineEmpty from '@components/Timelines/Timeline/Empty'
import TimelineEnd from '@components/Timelines/Timeline/End' import TimelineEnd from '@components/Timelines/Timeline/End'
import { useNavigation, useScrollToTop } from '@react-navigation/native' import { useNavigation, useScrollToTop } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { useRelationshipsQuery } from '@utils/queryHooks/relationships' import { useRelationshipsQuery } from '@utils/queryHooks/relationships'
import React, { useCallback, useMemo, useRef } from 'react' import React, { useCallback, useMemo, useRef } from 'react'
import { RefreshControl, StyleSheet } from 'react-native' import { RefreshControl, StyleSheet } from 'react-native'
@ -14,7 +15,9 @@ export interface Props {
} }
const RelationshipsList: React.FC<Props> = ({ id, type }) => { const RelationshipsList: React.FC<Props> = ({ id, type }) => {
const navigation = useNavigation() const navigation = useNavigation<
StackNavigationProp<Nav.LocalStackParamList>
>()
const { const {
status, status,
data, data,
@ -43,14 +46,7 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
const keyExtractor = useCallback(({ id }) => id, []) const keyExtractor = useCallback(({ id }) => id, [])
const renderItem = useCallback( const renderItem = useCallback(
({ item }) => ( ({ item }) => <ComponentAccount account={item} origin='relationship' />,
<ComponentAccount
account={item}
onPress={() =>
navigation.push('Screen-Shared-Account', { account: item })
}
/>
),
[] []
) )
const flItemEmptyComponent = useMemo( const flItemEmptyComponent = useMemo(

View File

@ -1,8 +1,8 @@
import ComponentAccount from '@components/Account' import ComponentAccount from '@components/Account'
import analytics from '@components/analytics'
import ComponentHashtag from '@components/Hashtag' import ComponentHashtag from '@components/Hashtag'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import TimelineDefault from '@components/Timelines/Timeline/Default' import TimelineDefault from '@components/Timelines/Timeline/Default'
import { useNavigation } from '@react-navigation/native'
import { useSearchQuery } from '@utils/queryHooks/search' import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
@ -24,7 +24,6 @@ export interface Props {
const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => { const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => {
const { t } = useTranslation('sharedSearch') const { t } = useTranslation('sharedSearch')
const navigation = useNavigation()
const { theme } = useTheme() const { theme } = useTheme()
const { status, data, refetch } = useSearchQuery({ const { status, data, refetch } = useSearchQuery({
term: searchTerm, term: searchTerm,
@ -158,28 +157,11 @@ const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => {
const listItem = useCallback(({ item, section }) => { const listItem = useCallback(({ item, section }) => {
switch (section.title) { switch (section.title) {
case 'accounts': case 'accounts':
return ( return <ComponentAccount account={item} origin='search' />
<ComponentAccount
account={item}
onPress={() => {
navigation.push('Screen-Shared-Account', { account: item })
}}
/>
)
case 'hashtags': case 'hashtags':
return ( return <ComponentHashtag hashtag={item} origin='search' />
<ComponentHashtag
tag={item}
onPress={() => {
navigation.goBack()
navigation.push('Screen-Shared-Hashtag', {
hashtag: item.name
})
}}
/>
)
case 'statuses': case 'statuses':
return <TimelineDefault item={item} disableDetails /> return <TimelineDefault item={item} disableDetails origin='search' />
default: default:
return null return null
} }

View File

@ -17,7 +17,7 @@ const netInfo = async (): Promise<{
if (netInfo.isConnected) { if (netInfo.isConnected) {
log('log', 'netInfo', 'network connected') log('log', 'netInfo', 'network connected')
if (activeIndex) { if (activeIndex !== null) {
log('log', 'netInfo', 'checking locally stored credentials') log('log', 'netInfo', 'checking locally stored credentials')
return client<Mastodon.Account>({ return client<Mastodon.Account>({
method: 'get', method: 'get',

View File

@ -5,6 +5,7 @@ import log from './log'
const sentry = () => { const sentry = () => {
log('log', 'Sentry', 'initializing') log('log', 'Sentry', 'initializing')
Sentry.init({ Sentry.init({
environment: Constants.manifest.extra.sentryEnv,
dsn: Constants.manifest.extra.sentryDSN, dsn: Constants.manifest.extra.sentryDSN,
enableInExpoDevelopment: false, enableInExpoDevelopment: false,
debug: __DEV__ debug: __DEV__