mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Add haptics and analytics
This commit is contained in:
@ -77,7 +77,7 @@ const renderNode = ({
|
||||
} else {
|
||||
const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/))
|
||||
// Need example here
|
||||
const content = node.children && node.children[0].data
|
||||
const content = node.children && node.children[0] && node.children[0].data
|
||||
const shouldBeTag =
|
||||
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
||||
return (
|
||||
|
@ -1,7 +1,3 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned'
|
||||
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
|
||||
import TimelineAttachment from '@components/Timelines/Timeline/Shared/Attachment'
|
||||
@ -10,10 +6,12 @@ import TimelineCard from '@components/Timelines/Timeline/Shared/Card'
|
||||
import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
|
||||
import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderDefault'
|
||||
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getLocalAccountId } from '@root/utils/slices/instancesSlice'
|
||||
|
||||
export interface Props {
|
||||
item: Mastodon.Status
|
||||
@ -36,13 +34,6 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
const navigation = useNavigation()
|
||||
|
||||
let actualStatus = item.reblog ? item.reblog : item
|
||||
const contentWidth = highlighted
|
||||
? Dimensions.get('window').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 // Global page padding on both sides
|
||||
: Dimensions.get('window').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 - // Global page padding on both sides
|
||||
StyleConstants.Avatar.M - // Avatar width
|
||||
StyleConstants.Spacing.S // Avatar margin to the right
|
||||
|
||||
const onPress = useCallback(
|
||||
() =>
|
||||
@ -93,10 +84,7 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{actualStatus.media_attachments.length > 0 && (
|
||||
<TimelineAttachment
|
||||
status={actualStatus}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
<TimelineAttachment status={actualStatus} />
|
||||
)}
|
||||
{actualStatus.card && <TimelineCard card={actualStatus.card} />}
|
||||
</View>
|
||||
|
@ -1,7 +1,3 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import TimelineActioned from '@components/Timelines/Timeline/Shared/Actioned'
|
||||
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
|
||||
import TimelineAttachment from '@components/Timelines/Timeline/Shared/Attachment'
|
||||
@ -10,10 +6,12 @@ import TimelineCard from '@components/Timelines/Timeline/Shared/Card'
|
||||
import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
|
||||
import TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/HeaderNotification'
|
||||
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getLocalAccountId } from '@root/utils/slices/instancesSlice'
|
||||
|
||||
export interface Props {
|
||||
notification: Mastodon.Notification
|
||||
@ -31,13 +29,6 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
const actualAccount = notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
const contentWidth = highlighted
|
||||
? Dimensions.get('window').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 // Global page padding on both sides
|
||||
: Dimensions.get('window').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 - // Global page padding on both sides
|
||||
StyleConstants.Avatar.M - // Avatar width
|
||||
StyleConstants.Spacing.S // Avatar margin to the right
|
||||
|
||||
const onPress = useCallback(
|
||||
() =>
|
||||
@ -92,10 +83,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{notification.status.media_attachments.length > 0 && (
|
||||
<TimelineAttachment
|
||||
status={notification.status}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
<TimelineAttachment status={notification.status} />
|
||||
)}
|
||||
{notification.status.card && (
|
||||
<TimelineCard card={notification.status.card} />
|
||||
|
@ -10,6 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { findIndex } from 'lodash'
|
||||
import { TimelineData } from '../../Timeline'
|
||||
import haptics from '@root/components/haptics'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
@ -110,12 +111,14 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
|
||||
return old
|
||||
})
|
||||
haptics('Success')
|
||||
break
|
||||
}
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
haptics('Error')
|
||||
toast({ type: 'error', content: '请重试' })
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
@ -172,8 +175,8 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
'com.apple.UIKit.activity.OpenInIBooks'
|
||||
]
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
() => haptics('Success'),
|
||||
() => haptics('Error')
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
@ -1,29 +1,30 @@
|
||||
import Button from '@components/Button'
|
||||
import AttachmentAudio from '@components/Timelines/Timeline/Shared/Attachment/Audio'
|
||||
import AttachmentImage from '@components/Timelines/Timeline/Shared/Attachment/Image'
|
||||
import AttachmentUnsupported from '@components/Timelines/Timeline/Shared/Attachment/Unsupported'
|
||||
import AttachmentVideo from '@components/Timelines/Timeline/Shared/Attachment/Video'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import haptics from '@root/components/haptics'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
||||
import AttachmentImage from '@root/components/Timelines/Timeline/Shared/Attachment/Image'
|
||||
import AttachmentVideo from '@root/components/Timelines/Timeline/Shared/Attachment/Video'
|
||||
import { IImageInfo } from 'react-native-image-zoom-viewer/built/image-viewer.type'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import AttachmentUnsupported from './Attachment/Unsupported'
|
||||
import AttachmentAudio from './Attachment/Audio'
|
||||
import layoutAnimation from '@root/utils/styles/layoutAnimation'
|
||||
import Button from '@root/components/Button'
|
||||
|
||||
export interface Props {
|
||||
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
|
||||
contentWidth: number
|
||||
}
|
||||
|
||||
const TimelineAttachment: React.FC<Props> = ({ status, contentWidth }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
|
||||
const onPressBlurView = useCallback(() => {
|
||||
layoutAnimation()
|
||||
setSensitiveShown(false)
|
||||
haptics('Medium')
|
||||
}, [])
|
||||
const onPressShow = useCallback(() => {
|
||||
setSensitiveShown(true)
|
||||
haptics('Medium')
|
||||
}, [])
|
||||
|
||||
let imageUrls: (IImageInfo & {
|
||||
@ -65,8 +66,6 @@ const TimelineAttachment: React.FC<Props> = ({ status, contentWidth }) => {
|
||||
key={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
width={contentWidth}
|
||||
height={(contentWidth / 16) * 9}
|
||||
/>
|
||||
)
|
||||
case 'gifv':
|
||||
@ -75,8 +74,6 @@ const TimelineAttachment: React.FC<Props> = ({ status, contentWidth }) => {
|
||||
key={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
width={contentWidth}
|
||||
height={(contentWidth / 16) * 9}
|
||||
/>
|
||||
)
|
||||
case 'audio':
|
||||
@ -88,7 +85,13 @@ const TimelineAttachment: React.FC<Props> = ({ status, contentWidth }) => {
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return <AttachmentUnsupported key={index} attachment={attachment} />
|
||||
return (
|
||||
<AttachmentUnsupported
|
||||
key={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}),
|
||||
[sensitiveShown]
|
||||
@ -114,7 +117,7 @@ const TimelineAttachment: React.FC<Props> = ({ status, contentWidth }) => {
|
||||
content='eye-off'
|
||||
round
|
||||
overlay
|
||||
onPress={() => setSensitiveShown(!sensitiveShown)}
|
||||
onPress={onPressShow}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.S,
|
||||
|
@ -8,10 +8,14 @@ import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
sensitiveShown: boolean
|
||||
attachment: Mastodon.AttachmentUnknown
|
||||
}
|
||||
|
||||
const AttachmentUnsupported: React.FC<Props> = ({ attachment }) => {
|
||||
const AttachmentUnsupported: React.FC<Props> = ({
|
||||
sensitiveShown,
|
||||
attachment
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
@ -26,22 +30,26 @@ const AttachmentUnsupported: React.FC<Props> = ({ attachment }) => {
|
||||
<Blurhash hash={attachment.blurhash} />
|
||||
</Surface>
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
{ color: attachment.blurhash ? theme.background : theme.primary }
|
||||
]}
|
||||
>
|
||||
文件不支持
|
||||
</Text>
|
||||
{attachment.remote_url ? (
|
||||
<Button
|
||||
type='text'
|
||||
content='尝试远程链接'
|
||||
size='S'
|
||||
overlay
|
||||
onPress={async () => await openLink(attachment.remote_url!)}
|
||||
/>
|
||||
{!sensitiveShown ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
{ color: attachment.blurhash ? theme.background : theme.primary }
|
||||
]}
|
||||
>
|
||||
文件不支持
|
||||
</Text>
|
||||
{attachment.remote_url ? (
|
||||
<Button
|
||||
type='text'
|
||||
content='尝试远程链接'
|
||||
size='S'
|
||||
overlay
|
||||
onPress={async () => await openLink(attachment.remote_url!)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import relativeTime from '@utils/relativeTime'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
|
||||
import haptics from '@root/components/haptics'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
@ -50,10 +51,12 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
||||
pointer: paging.pointer
|
||||
}))
|
||||
)
|
||||
haptics('Success')
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
haptics('Error')
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from 'react-query'
|
||||
import client from '@api/client'
|
||||
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||
import { toast } from '@components/toast'
|
||||
import haptics from '@root/components/haptics'
|
||||
|
||||
const fireMutation = async ({
|
||||
type,
|
||||
@ -69,6 +70,7 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
||||
const queryClient = useQueryClient()
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onSettled: () => {
|
||||
haptics('Success')
|
||||
queryKey && queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
|
@ -11,6 +11,7 @@ import relativeTime from '@utils/relativeTime'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import haptics from '@root/components/haptics'
|
||||
|
||||
export interface Props {
|
||||
notification: Mastodon.Notification
|
||||
@ -67,8 +68,10 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
}).then(res => {
|
||||
if (res.body.id === (updateData && updateData.id) || data!.id) {
|
||||
setUpdateData(res.body)
|
||||
haptics('Success')
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
haptics('Error')
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
return Promise.reject()
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import Emojis from './Emojis'
|
||||
import { TimelineData } from '../../Timeline'
|
||||
import { findIndex } from 'lodash'
|
||||
import haptics from '@root/components/haptics'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
@ -95,6 +96,8 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
}
|
||||
return old
|
||||
})
|
||||
|
||||
haptics('Success')
|
||||
}
|
||||
})
|
||||
|
||||
@ -207,6 +210,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
<Pressable
|
||||
style={[styles.optionUnselected]}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
if (poll.multiple) {
|
||||
setAllOptions(
|
||||
allOptions.map((o, i) => (i === index ? !o : o))
|
||||
|
10
src/components/analytics.ts
Normal file
10
src/components/analytics.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import * as Analytics from 'expo-firebase-analytics'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
|
||||
const analytics = (event: string, params?: { [key: string]: string }) => {
|
||||
Analytics.logEvent(event, params).catch(error =>
|
||||
Sentry.Native.captureException(error)
|
||||
)
|
||||
}
|
||||
|
||||
export default analytics
|
24
src/components/haptics.ts
Normal file
24
src/components/haptics.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as Haptics from 'expo-haptics'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
|
||||
const haptics = (
|
||||
type: 'Success' | 'Warning' | 'Error' | 'Light' | 'Medium' | 'Heavy'
|
||||
) => {
|
||||
switch (type) {
|
||||
case 'Success':
|
||||
case 'Warning':
|
||||
case 'Error':
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType[type]).catch(
|
||||
error => Sentry.Native.captureException(error)
|
||||
)
|
||||
break
|
||||
case 'Light':
|
||||
case 'Medium':
|
||||
case 'Heavy':
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle[type]).catch(error =>
|
||||
Sentry.Native.captureException(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default haptics
|
Reference in New Issue
Block a user