Editing can update media

This commit is contained in:
Zhiyuan Zheng 2022-04-30 21:29:08 +02:00
parent d4f91a5756
commit f93d6f7db8
9 changed files with 666 additions and 746 deletions

View File

@ -1,6 +1,6 @@
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { import {
AccessibilityProps, AccessibilityProps,
Image, Image,
@ -39,125 +39,103 @@ export interface Props {
> >
} }
const GracefullyImage = React.memo( const GracefullyImage = ({
({ accessibilityLabel,
accessibilityLabel, accessibilityHint,
accessibilityHint, hidden = false,
hidden = false, uri,
uri, blurhash,
blurhash, dimension,
dimension, onPress,
onPress, style,
style, imageStyle,
imageStyle, setImageDimensions
setImageDimensions }: Props) => {
}: Props) => { const { reduceMotionEnabled } = useAccessibility()
const { reduceMotionEnabled } = useAccessibility() const { colors } = useTheme()
const { colors } = useTheme() const [originalFailed, setOriginalFailed] = useState(false)
const [originalFailed, setOriginalFailed] = useState(false) const [imageLoaded, setImageLoaded] = useState(false)
const [imageLoaded, setImageLoaded] = useState(false)
const source = useMemo(() => { const source = originalFailed
if (originalFailed) { ? { uri: uri.remote || undefined }
return { uri: uri.remote || undefined } : {
} else { uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
return {
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
}
} }
}, [originalFailed])
const onLoad = useCallback(() => { const onLoad = () => {
setImageLoaded(true) setImageLoaded(true)
if (setImageDimensions && source.uri) { if (setImageDimensions && source.uri) {
Image.getSize(source.uri, (width, height) => Image.getSize(source.uri, (width, height) =>
setImageDimensions({ width, height }) setImageDimensions({ width, height })
)
}
}
const onError = () => {
if (!originalFailed) {
setOriginalFailed(true)
}
}
const blurhashView = useMemo(() => {
if (hidden || !imageLoaded) {
if (blurhash) {
return (
<Blurhash
decodeAsync
blurhash={blurhash}
style={styles.placeholder}
/>
) )
} } else {
}, [source.uri]) return (
const onError = useCallback(() => { <View
if (!originalFailed) {
setOriginalFailed(true)
}
}, [originalFailed])
const previewView = useMemo(
() =>
uri.preview && !imageLoaded ? (
<Image
fadeDuration={0}
source={{ uri: uri.preview }}
style={[ style={[
styles.placeholder, styles.placeholder,
{ backgroundColor: colors.shimmerDefault } { backgroundColor: colors.shimmerDefault }
]} ]}
/> />
) : null, )
[] }
) } else {
const originalView = useMemo( return null
() => ( }
}, [hidden, imageLoaded])
return (
<Pressable
{...(onPress
? { accessibilityRole: 'imagebutton' }
: { accessibilityRole: 'image' })}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
style={[style, dimension, { backgroundColor: colors.shimmerDefault }]}
{...(onPress
? hidden
? { disabled: true }
: { onPress }
: { disabled: true })}
>
{uri.preview && !imageLoaded ? (
<Image <Image
fadeDuration={0} fadeDuration={0}
source={source} source={{ uri: uri.preview }}
style={[{ flex: 1 }, imageStyle]} style={[
onLoad={onLoad} styles.placeholder,
onError={onError} { backgroundColor: colors.shimmerDefault }
]}
/> />
), ) : null}
[source] <Image
) fadeDuration={0}
const blurhashView = useMemo(() => { source={source}
if (hidden || !imageLoaded) { style={[{ flex: 1 }, imageStyle]}
if (blurhash) { onLoad={onLoad}
return ( onError={onError}
<Blurhash />
decodeAsync {blurhashView}
blurhash={blurhash} </Pressable>
style={styles.placeholder} )
/> }
)
} else {
return (
<View
style={[
styles.placeholder,
{ backgroundColor: colors.shimmerDefault }
]}
/>
)
}
} else {
return null
}
}, [hidden, imageLoaded])
return (
<Pressable
{...(onPress
? { accessibilityRole: 'imagebutton' }
: { accessibilityRole: 'image' })}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
style={[style, dimension, { backgroundColor: colors.shimmerDefault }]}
{...(onPress
? hidden
? { disabled: true }
: { onPress }
: { disabled: true })}
>
{previewView}
{originalView}
{blurhashView}
</Pressable>
)
},
(prev, next) =>
prev.hidden === next.hidden &&
prev.uri.preview === next.uri.preview &&
prev.uri.original === next.uri.original &&
prev.uri.remote === next.uri.remote
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
placeholder: { placeholder: {

View File

@ -10,10 +10,9 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators' import { RootStackParamList } from '@utils/navigation/navigators'
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 React, { useCallback, useMemo, useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, View } from 'react-native'
export interface Props { export interface Props {
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'> status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
@ -24,24 +23,13 @@ const TimelineAttachment = React.memo(
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive) const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
const onPressBlurView = useCallback(() => {
analytics('timeline_shared_attachment_blurview_press_show')
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}, [])
const onPressShow = useCallback(() => {
analytics('timeline_shared_attachment_blurview_press_hide')
setSensitiveShown(true)
haptics('Light')
}, [])
const imageUrls = useRef< const imageUrls = useRef<
RootStackParamList['Screen-ImagesViewer']['imageUrls'] RootStackParamList['Screen-ImagesViewer']['imageUrls']
>([]) >([])
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>() const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
useEffect(() => { const navigateToImagesViewer = (id: string) => {
status.media_attachments.forEach((attachment, index) => { status.media_attachments.forEach(attachment => {
switch (attachment.type) { switch (attachment.type) {
case 'image': case 'image':
imageUrls.current.push({ imageUrls.current.push({
@ -55,117 +43,136 @@ const TimelineAttachment = React.memo(
}) })
} }
}) })
}, [])
const navigateToImagesViewer = (id: string) =>
navigation.navigate('Screen-ImagesViewer', { navigation.navigate('Screen-ImagesViewer', {
imageUrls: imageUrls.current, imageUrls: imageUrls.current,
id id
}) })
const attachments = useMemo( }
() =>
status.media_attachments.map((attachment, index) => { return (
switch (attachment.type) { <View>
case 'image': <View
return ( style={{
<AttachmentImage marginTop: StyleConstants.Spacing.S,
key={index} flex: 1,
total={status.media_attachments.length} flexDirection: 'row',
index={index} flexWrap: 'wrap',
sensitiveShown={sensitiveShown} justifyContent: 'center',
image={attachment} alignContent: 'stretch'
navigateToImagesViewer={navigateToImagesViewer} }}
/> >
) {status.media_attachments.map((attachment, index) => {
case 'video': switch (attachment.type) {
return ( case 'image':
<AttachmentVideo
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
/>
)
case 'gifv':
return (
<AttachmentVideo
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
gifv
/>
)
case 'audio':
return (
<AttachmentAudio
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
audio={attachment}
/>
)
default:
if (
attachment.preview_url?.endsWith('.jpg') ||
attachment.preview_url?.endsWith('.jpeg') ||
attachment.preview_url?.endsWith('.png') ||
attachment.preview_url?.endsWith('.gif') ||
attachment.remote_url?.endsWith('.jpg') ||
attachment.remote_url?.endsWith('.jpeg') ||
attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif')
) {
imageUrls.current.push({
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
})
return ( return (
<AttachmentImage <AttachmentImage
key={index} key={index}
total={status.media_attachments.length} total={status.media_attachments.length}
index={index} index={index}
sensitiveShown={sensitiveShown} sensitiveShown={sensitiveShown}
// @ts-ignore
image={attachment} image={attachment}
navigateToImagesViewer={navigateToImagesViewer} navigateToImagesViewer={navigateToImagesViewer}
/> />
) )
} else { case 'video':
return ( return (
<AttachmentUnsupported <AttachmentVideo
key={index} key={index}
total={status.media_attachments.length} total={status.media_attachments.length}
index={index} index={index}
sensitiveShown={sensitiveShown} sensitiveShown={sensitiveShown}
attachment={attachment} video={attachment}
/> />
) )
} case 'gifv':
} return (
}), <AttachmentVideo
[sensitiveShown] key={index}
) total={status.media_attachments.length}
index={index}
return ( sensitiveShown={sensitiveShown}
<View> video={attachment}
<View style={styles.container} children={attachments} /> gifv
/>
)
case 'audio':
return (
<AttachmentAudio
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
audio={attachment}
/>
)
default:
if (
attachment.preview_url?.endsWith('.jpg') ||
attachment.preview_url?.endsWith('.jpeg') ||
attachment.preview_url?.endsWith('.png') ||
attachment.preview_url?.endsWith('.gif') ||
attachment.remote_url?.endsWith('.jpg') ||
attachment.remote_url?.endsWith('.jpeg') ||
attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif')
) {
imageUrls.current.push({
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
})
return (
<AttachmentImage
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
// @ts-ignore
image={attachment}
navigateToImagesViewer={navigateToImagesViewer}
/>
)
} else {
return (
<AttachmentUnsupported
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
attachment={attachment}
/>
)
}
}
})}
</View>
{status.sensitive && {status.sensitive &&
(sensitiveShown ? ( (sensitiveShown ? (
<Pressable style={styles.sensitiveBlur}> <Pressable
style={{
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button <Button
type='text' type='text'
content={t('shared.attachment.sensitive.button')} content={t('shared.attachment.sensitive.button')}
overlay overlay
onPress={onPressBlurView} onPress={() => {
analytics('timeline_shared_attachment_blurview_press_show')
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}}
/> />
</Pressable> </Pressable>
) : ( ) : (
@ -174,7 +181,11 @@ const TimelineAttachment = React.memo(
content='EyeOff' content='EyeOff'
round round
overlay overlay
onPress={onPressShow} onPress={() => {
analytics('timeline_shared_attachment_blurview_press_hide')
setSensitiveShown(true)
haptics('Light')
}}
style={{ style={{
position: 'absolute', position: 'absolute',
top: StyleConstants.Spacing.S * 2, top: StyleConstants.Spacing.S * 2,
@ -185,33 +196,28 @@ const TimelineAttachment = React.memo(
</View> </View>
) )
}, },
() => true (prev, next) => {
let isEqual = true
if (
prev.status.media_attachments.length !==
next.status.media_attachments.length
) {
isEqual = false
return isEqual
}
prev.status.media_attachments.forEach((attachment, index) => {
if (
attachment.preview_url !==
next.status.media_attachments[index].preview_url
) {
isEqual = false
}
})
return isEqual
}
) )
const styles = StyleSheet.create({
container: {
marginTop: StyleConstants.Spacing.S,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignContent: 'stretch'
},
sensitiveBlur: {
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
sensitiveBlurButton: {
padding: StyleConstants.Spacing.S,
borderRadius: 6
},
sensitiveText: {
...StyleConstants.FontStyle.M
}
})
export default TimelineAttachment export default TimelineAttachment

View File

@ -1,8 +1,8 @@
import analytics from '@components/analytics' 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 from 'react'
import { StyleSheet, View } from 'react-native' import { View } from 'react-native'
import attachmentAspectRatio from './aspectRatio' import attachmentAspectRatio from './aspectRatio'
export interface Props { export interface Props {
@ -13,44 +13,43 @@ export interface Props {
navigateToImagesViewer: (imageIndex: string) => void navigateToImagesViewer: (imageIndex: string) => void
} }
const AttachmentImage = React.memo( const AttachmentImage = ({
({ total, index, sensitiveShown, image, navigateToImagesViewer }: Props) => { total,
const onPress = useCallback(() => { index,
analytics('timeline_shared_attachment_image_press', { id: image.id }) sensitiveShown,
navigateToImagesViewer(image.id) image,
}, []) navigateToImagesViewer
}: Props) => {
return ( return (
<View style={styles.base}> <View
<GracefullyImage style={{
accessibilityLabel={image.description} flex: 1,
hidden={sensitiveShown} flexBasis: '50%',
uri={{ original: image.preview_url, remote: image.remote_url }} padding: StyleConstants.Spacing.XS / 2
blurhash={image.blurhash} }}
onPress={onPress} >
style={{ <GracefullyImage
aspectRatio: accessibilityLabel={image.description}
total > 1 || hidden={sensitiveShown}
!image.meta?.original?.width || uri={{ original: image.preview_url, remote: image.remote_url }}
!image.meta?.original?.height blurhash={image.blurhash}
? attachmentAspectRatio({ total, index }) onPress={() => {
: image.meta.original.height / image.meta.original.width > 1 analytics('timeline_shared_attachment_image_press', { id: image.id })
? 1 navigateToImagesViewer(image.id)
: image.meta.original.width / image.meta.original.height }}
}} style={{
/> aspectRatio:
</View> total > 1 ||
) !image.meta?.original?.width ||
}, !image.meta?.original?.height
(prev, next) => prev.sensitiveShown === next.sensitiveShown ? attachmentAspectRatio({ total, index })
) : image.meta.original.height / image.meta.original.width > 1
? 1
const styles = StyleSheet.create({ : image.meta.original.width / image.meta.original.height
base: { }}
flex: 1, />
flexBasis: '50%', </View>
padding: StyleConstants.Spacing.XS / 2 )
} }
})
export default AttachmentImage export default AttachmentImage

View File

@ -10,142 +10,116 @@ 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, Text, View } from 'react-native' import { Pressable, Text, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedMuted from './HeaderShared/Muted'
const Names = React.memo( const Names = ({ accounts }: { accounts: Mastodon.Account[] }) => {
({ accounts }: { accounts: Mastodon.Account[] }) => { const { t } = useTranslation('componentTimeline')
const { t } = useTranslation('componentTimeline') const { colors } = useTheme()
const { colors } = useTheme()
return ( return (
<Text <Text
numberOfLines={1} numberOfLines={1}
style={[styles.namesLeading, { color: colors.secondary }]} style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
> >
<Text>{t('shared.header.conversation.withAccounts')}</Text> <Text>{t('shared.header.conversation.withAccounts')}</Text>
{accounts.map((account, index) => ( {accounts.map((account, index) => (
<Text key={account.id} numberOfLines={1}> <Text key={account.id} numberOfLines={1}>
{index !== 0 ? t('common:separator') : undefined} {index !== 0 ? t('common:separator') : undefined}
<ParseEmojis <ParseEmojis
content={account.display_name || account.username} content={account.display_name || account.username}
emojis={account.emojis} emojis={account.emojis}
fontBold fontBold
/> />
</Text> </Text>
))} ))}
</Text> </Text>
) )
}, }
() => true
)
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
conversation: Mastodon.Conversation conversation: Mastodon.Conversation
} }
const HeaderConversation = React.memo( const HeaderConversation = ({ queryKey, conversation }: Props) => {
({ queryKey, conversation }: Props) => { const { colors, theme } = useTheme()
const { colors, theme } = useTheme() const { t } = useTranslation('componentTimeline')
const { t } = useTranslation('componentTimeline')
const queryClient = useQueryClient() const queryClient = useQueryClient()
const mutation = useTimelineMutation({ const mutation = useTimelineMutation({
onMutate: true, onMutate: true,
onError: (err: any, _, oldData) => { onError: (err: any, _, oldData) => {
displayMessage({ displayMessage({
theme, theme,
type: 'error', type: 'error',
message: t('common:message.error.message', { message: t('common:message.error.message', {
function: t(`shared.header.conversation.delete.function`) function: t(`shared.header.conversation.delete.function`)
}), }),
...(err.status && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&
err.data && err.data &&
err.data.error && err.data.error &&
typeof err.data.error === 'string' && { typeof err.data.error === 'string' && {
description: err.data.error description: err.data.error
}) })
})
queryClient.setQueryData(queryKey, oldData)
}
})
const actionOnPress = useCallback(() => {
analytics('timeline_conversation_delete_press')
mutation.mutate({
type: 'deleteItem',
source: 'conversations',
queryKey,
id: conversation.id
}) })
}, []) queryClient.setQueryData(queryKey, oldData)
}
})
const actionChildren = useMemo( const actionOnPress = useCallback(() => {
() => ( analytics('timeline_conversation_delete_press')
<Icon mutation.mutate({
name='Trash' type: 'deleteItem',
color={colors.secondary} source: 'conversations',
size={StyleConstants.Font.Size.L} queryKey,
/> id: conversation.id
), })
[] }, [])
)
return ( const actionChildren = useMemo(
<View style={styles.base}> () => (
<View style={styles.nameAndMeta}> <Icon
<Names accounts={conversation.accounts} /> name='Trash'
<View style={styles.meta}> color={colors.secondary}
{conversation.last_status?.created_at ? ( size={StyleConstants.Font.Size.L}
<HeaderSharedCreated />
created_at={conversation.last_status?.created_at} ),
edited_at={conversation.last_status?.edited_at} []
/> )
) : null}
<HeaderSharedMuted muted={conversation.last_status?.muted} /> return (
</View> <View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 3 }}>
<Names accounts={conversation.accounts} />
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
>
{conversation.last_status?.created_at ? (
<HeaderSharedCreated
created_at={conversation.last_status?.created_at}
edited_at={conversation.last_status?.edited_at}
/>
) : null}
<HeaderSharedMuted muted={conversation.last_status?.muted} />
</View> </View>
<Pressable
style={styles.action}
onPress={actionOnPress}
children={actionChildren}
/>
</View> </View>
)
},
() => true
)
const styles = StyleSheet.create({ <Pressable
base: { style={{ flex: 1, flexDirection: 'row', justifyContent: 'center' }}
flex: 1, onPress={actionOnPress}
flexDirection: 'row' children={actionChildren}
}, />
nameAndMeta: { </View>
flex: 3 )
}, }
meta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
},
created_at: {
...StyleConstants.FontStyle.S
},
action: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
},
namesLeading: {
...StyleConstants.FontStyle.M
}
})
export default HeaderConversation export default HeaderConversation

View File

@ -7,7 +7,7 @@ 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 { Pressable, StyleSheet, View } from 'react-native' import { Pressable, View } from 'react-native'
import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
@ -20,77 +20,61 @@ export interface Props {
status: Mastodon.Status status: Mastodon.Status
} }
const TimelineHeaderDefault = React.memo( const TimelineHeaderDefault = ({ queryKey, rootQueryKey, status }: Props) => {
({ queryKey, rootQueryKey, status }: Props) => { const { t } = useTranslation('componentTimeline')
const { t } = useTranslation('componentTimeline') const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>() const { colors } = useTheme()
const { colors } = useTheme()
return ( return (
<View style={styles.base}> <View style={{ flex: 1, flexDirection: 'row' }}>
<View style={styles.accountAndMeta}> <View style={{ flex: 5 }}>
<HeaderSharedAccount account={status.account} /> <HeaderSharedAccount account={status.account} />
<View style={styles.meta}> <View
<HeaderSharedCreated style={{
created_at={status.created_at} flexDirection: 'row',
edited_at={status.edited_at} alignItems: 'center',
/> marginTop: StyleConstants.Spacing.XS,
<HeaderSharedVisibility visibility={status.visibility} /> marginBottom: StyleConstants.Spacing.S
<HeaderSharedMuted muted={status.muted} /> }}
<HeaderSharedApplication application={status.application} /> >
</View> <HeaderSharedCreated
</View> created_at={status.created_at}
edited_at={status.edited_at}
{queryKey ? (
<Pressable
accessibilityHint={t('shared.header.actions.accessibilityHint')}
style={styles.action}
onPress={() =>
navigation.navigate('Screen-Actions', {
queryKey,
rootQueryKey,
status,
type: 'status'
})
}
children={
<Icon
name='MoreHorizontal'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
}
/> />
) : null} <HeaderSharedVisibility visibility={status.visibility} />
<HeaderSharedMuted muted={status.muted} />
<HeaderSharedApplication application={status.application} />
</View>
</View> </View>
)
},
() => true
)
const styles = StyleSheet.create({ {queryKey ? (
base: { <Pressable
flex: 1, accessibilityHint={t('shared.header.actions.accessibilityHint')}
flexDirection: 'row' style={{
}, flex: 1,
accountAndMeta: { flexDirection: 'row',
flex: 5 justifyContent: 'center',
}, paddingBottom: StyleConstants.Spacing.S
meta: { }}
flexDirection: 'row', onPress={() =>
alignItems: 'center', navigation.navigate('Screen-Actions', {
marginTop: StyleConstants.Spacing.XS, queryKey,
marginBottom: StyleConstants.Spacing.S rootQueryKey,
}, status,
created_at: { type: 'status'
...StyleConstants.FontStyle.S })
}, }
action: { children={
flex: 1, <Icon
flexDirection: 'row', name='MoreHorizontal'
justifyContent: 'center', color={colors.secondary}
paddingBottom: StyleConstants.Spacing.S size={StyleConstants.Font.Size.L}
} />
}) }
/>
) : null}
</View>
)
}
export default TimelineHeaderDefault export default TimelineHeaderDefault

View File

@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
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, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, View } from 'react-native'
import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
@ -22,115 +22,103 @@ export interface Props {
notification: Mastodon.Notification notification: Mastodon.Notification
} }
const TimelineHeaderNotification = React.memo( const TimelineHeaderNotification = ({ queryKey, notification }: Props) => {
({ queryKey, notification }: Props) => { const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>() const { colors } = useTheme()
const { colors } = useTheme()
const actions = useMemo(() => { const actions = useMemo(() => {
switch (notification.type) { switch (notification.type) {
case 'follow': case 'follow':
return <RelationshipOutgoing id={notification.account.id} /> return <RelationshipOutgoing id={notification.account.id} />
case 'follow_request': case 'follow_request':
return <RelationshipIncoming id={notification.account.id} /> return <RelationshipIncoming id={notification.account.id} />
default: default:
if (notification.status) { if (notification.status) {
return ( return (
<Pressable <Pressable
style={{ style={{
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
paddingBottom: StyleConstants.Spacing.S paddingBottom: StyleConstants.Spacing.S
}} }}
onPress={() => onPress={() =>
navigation.navigate('Screen-Actions', { navigation.navigate('Screen-Actions', {
queryKey, queryKey,
status: notification.status!, status: notification.status!,
type: 'status' type: 'status'
}) })
} }
children={ children={
<Icon <Icon
name='MoreHorizontal' name='MoreHorizontal'
color={colors.secondary} color={colors.secondary}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
/> />
} }
/>
)
}
}
}, [notification.type])
return (
<View style={styles.base}>
<View
style={{
flex:
notification.type === 'follow' ||
notification.type === 'follow_request'
? 1
: 4
}}
>
<HeaderSharedAccount
account={
notification.status
? notification.status.account
: notification.account
}
{...((notification.type === 'follow' ||
notification.type === 'follow_request') && { withoutName: true })}
/>
<View style={styles.meta}>
<HeaderSharedCreated
created_at={notification.created_at}
edited_at={notification.status?.edited_at}
/> />
{notification.status?.visibility ? ( )
<HeaderSharedVisibility }
visibility={notification.status.visibility} }
/> }, [notification.type])
) : null}
<HeaderSharedMuted muted={notification.status?.muted} />
<HeaderSharedApplication
application={notification.status?.application}
/>
</View>
</View>
<View return (
style={[ <View style={{ flex: 1, flexDirection: 'row' }}>
styles.relationship, <View
style={{
flex:
notification.type === 'follow' || notification.type === 'follow' ||
notification.type === 'follow_request' notification.type === 'follow_request'
? { flexShrink: 1 } ? 1
: { flex: 1 } : 4
]} }}
>
<HeaderSharedAccount
account={
notification.status
? notification.status.account
: notification.account
}
{...((notification.type === 'follow' ||
notification.type === 'follow_request') && { withoutName: true })}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
> >
{actions} <HeaderSharedCreated
created_at={notification.created_at}
edited_at={notification.status?.edited_at}
/>
{notification.status?.visibility ? (
<HeaderSharedVisibility
visibility={notification.status.visibility}
/>
) : null}
<HeaderSharedMuted muted={notification.status?.muted} />
<HeaderSharedApplication
application={notification.status?.application}
/>
</View> </View>
</View> </View>
)
},
() => true
)
const styles = StyleSheet.create({ <View
base: { style={[
flex: 1, { marginLeft: StyleConstants.Spacing.M },
flexDirection: 'row' notification.type === 'follow' ||
}, notification.type === 'follow_request'
meta: { ? { flexShrink: 1 }
flexDirection: 'row', : { flex: 1 }
alignItems: 'center', ]}
marginTop: StyleConstants.Spacing.XS, >
marginBottom: StyleConstants.Spacing.S {actions}
}, </View>
relationship: { </View>
marginLeft: StyleConstants.Spacing.M )
} }
})
export default TimelineHeaderNotification export default TimelineHeaderNotification

View File

@ -35,191 +35,189 @@ import ActionsNotificationsFilter from './Actions/NotificationsFilter'
import ActionsShare from './Actions/Share' import ActionsShare from './Actions/Share'
import ActionsStatus from './Actions/Status' import ActionsStatus from './Actions/Status'
const ScreenActions = React.memo( const ScreenActions = ({
({ route: { params },
route: { params }, navigation
navigation }: RootStackScreenProps<'Screen-Actions'>) => {
}: RootStackScreenProps<'Screen-Actions'>) => { const { t } = useTranslation()
const { t } = useTranslation()
const instanceAccount = useSelector( const instanceAccount = useSelector(
getInstanceAccount, getInstanceAccount,
(prev, next) => prev?.id === next?.id (prev, next) => prev?.id === next?.id
) )
let sameAccount = false let sameAccount = false
switch (params.type) {
case 'status':
console.log('media length', params.status.media_attachments.length)
sameAccount = instanceAccount?.id === params.status.account.id
break
case 'account':
sameAccount = instanceAccount?.id === params.account.id
break
}
const instanceDomain = useSelector(getInstanceUrl)
let sameDomain = true
let statusDomain: string
switch (params.type) {
case 'status':
statusDomain = params.status.uri
? params.status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
: ''
sameDomain = instanceDomain === statusDomain
break
}
const { colors } = useTheme()
const insets = useSafeAreaInsets()
const DEFAULT_VALUE = 350
const screenHeight = Dimensions.get('screen').height
const panY = useSharedValue(DEFAULT_VALUE)
useEffect(() => {
panY.value = withTiming(0)
}, [])
const styleTop = useAnimatedStyle(() => {
return {
bottom: interpolate(
panY.value,
[0, screenHeight],
[0, -screenHeight],
Extrapolate.CLAMP
)
}
})
const dismiss = useCallback(() => {
navigation.goBack()
}, [])
const onGestureEvent = useAnimatedGestureHandler({
onActive: ({ translationY }) => {
panY.value = translationY
},
onEnd: ({ velocityY }) => {
if (velocityY > 500) {
runOnJS(dismiss)()
} else {
panY.value = withTiming(0)
}
}
})
const actions = () => {
switch (params.type) { switch (params.type) {
case 'status': case 'status':
sameAccount = instanceAccount?.id === params.status.account.id return (
break <>
case 'account': {!sameAccount ? (
sameAccount = instanceAccount?.id === params.account.id <ActionsAccount
break queryKey={params.queryKey}
} rootQueryKey={params.rootQueryKey}
account={params.status.account}
const instanceDomain = useSelector(getInstanceUrl) dismiss={dismiss}
let sameDomain = true
let statusDomain: string
switch (params.type) {
case 'status':
statusDomain = params.status.uri
? params.status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
: ''
sameDomain = instanceDomain === statusDomain
break
}
const { colors } = useTheme()
const insets = useSafeAreaInsets()
const DEFAULT_VALUE = 350
const screenHeight = Dimensions.get('screen').height
const panY = useSharedValue(DEFAULT_VALUE)
useEffect(() => {
panY.value = withTiming(0)
}, [])
const styleTop = useAnimatedStyle(() => {
return {
bottom: interpolate(
panY.value,
[0, screenHeight],
[0, -screenHeight],
Extrapolate.CLAMP
)
}
})
const dismiss = useCallback(() => {
navigation.goBack()
}, [])
const onGestureEvent = useAnimatedGestureHandler({
onActive: ({ translationY }) => {
panY.value = translationY
},
onEnd: ({ velocityY }) => {
if (velocityY > 500) {
runOnJS(dismiss)()
} else {
panY.value = withTiming(0)
}
}
})
const actions = useMemo(() => {
switch (params.type) {
case 'status':
return (
<>
{!sameAccount ? (
<ActionsAccount
queryKey={params.queryKey}
rootQueryKey={params.rootQueryKey}
account={params.status.account}
dismiss={dismiss}
/>
) : null}
{sameAccount && params.status ? (
<ActionsStatus
navigation={navigation}
queryKey={params.queryKey}
rootQueryKey={params.rootQueryKey}
status={params.status}
dismiss={dismiss}
/>
) : null}
{!sameDomain && statusDomain ? (
<ActionsDomain
queryKey={params.queryKey}
rootQueryKey={params.rootQueryKey}
domain={statusDomain}
dismiss={dismiss}
/>
) : null}
{params.status.visibility !== 'direct' ? (
<ActionsShare
url={params.status.url || params.status.uri}
type={params.type}
dismiss={dismiss}
/>
) : null}
<Button
type='text'
content={t('common:buttons.cancel')}
onPress={() => {
analytics('bottomsheet_acknowledge')
}}
style={styles.button}
/> />
</> ) : null}
) {sameAccount && params.status ? (
case 'account': <ActionsStatus
return ( navigation={navigation}
<> queryKey={params.queryKey}
{!sameAccount ? ( rootQueryKey={params.rootQueryKey}
<ActionsAccount account={params.account} dismiss={dismiss} /> status={params.status}
) : null} dismiss={dismiss}
/>
) : null}
{!sameDomain && statusDomain ? (
<ActionsDomain
queryKey={params.queryKey}
rootQueryKey={params.rootQueryKey}
domain={statusDomain}
dismiss={dismiss}
/>
) : null}
{params.status.visibility !== 'direct' ? (
<ActionsShare <ActionsShare
url={params.account.url} url={params.status.url || params.status.uri}
type={params.type} type={params.type}
dismiss={dismiss} dismiss={dismiss}
/> />
<Button ) : null}
type='text' <Button
content={t('common:buttons.cancel')} type='text'
onPress={() => { content={t('common:buttons.cancel')}
analytics('bottomsheet_acknowledge') onPress={() => {
}} analytics('bottomsheet_acknowledge')
style={styles.button} }}
/> style={styles.button}
</> />
) </>
case 'notifications_filter': )
return <ActionsNotificationsFilter /> case 'account':
} return (
}, []) <>
{!sameAccount ? (
<ActionsAccount account={params.account} dismiss={dismiss} />
) : null}
<ActionsShare
url={params.account.url}
type={params.type}
dismiss={dismiss}
/>
<Button
type='text'
content={t('common:buttons.cancel')}
onPress={() => {
analytics('bottomsheet_acknowledge')
}}
style={styles.button}
/>
</>
)
case 'notifications_filter':
return <ActionsNotificationsFilter />
}
}
return ( return (
<SafeAreaProvider> <SafeAreaProvider>
<Animated.View style={{ flex: 1 }}> <Animated.View style={{ flex: 1 }}>
<TapGestureHandler <TapGestureHandler
onHandlerStateChange={({ nativeEvent }) => { onHandlerStateChange={({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) { if (nativeEvent.state === State.ACTIVE) {
dismiss() dismiss()
} }
}} }}
>
<Animated.View
style={[
styles.overlay,
{ backgroundColor: colors.backgroundOverlayInvert }
]}
> >
<Animated.View <PanGestureHandler onGestureEvent={onGestureEvent}>
style={[ <Animated.View
styles.overlay, style={[
{ backgroundColor: colors.backgroundOverlayInvert } styles.container,
]} styleTop,
> {
<PanGestureHandler onGestureEvent={onGestureEvent}> backgroundColor: colors.backgroundDefault,
<Animated.View paddingBottom: insets.bottom || StyleConstants.Spacing.L
}
]}
>
<View
style={[ style={[
styles.container, styles.handle,
styleTop, { backgroundColor: colors.primaryOverlay }
{
backgroundColor: colors.backgroundDefault,
paddingBottom: insets.bottom || StyleConstants.Spacing.L
}
]} ]}
> />
<View {actions()}
style={[ </Animated.View>
styles.handle, </PanGestureHandler>
{ backgroundColor: colors.primaryOverlay } </Animated.View>
]} </TapGestureHandler>
/> </Animated.View>
{actions} </SafeAreaProvider>
</Animated.View> )
</PanGestureHandler> }
</Animated.View>
</TapGestureHandler>
</Animated.View>
</SafeAreaProvider>
)
},
() => true
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
overlay: { overlay: {

View File

@ -305,7 +305,6 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
switch (params?.type) { switch (params?.type) {
case 'edit': case 'edit':
console.log('firing mutation')
mutateTimeline.mutate({ mutateTimeline.mutate({
type: 'editItem', type: 'editItem',
queryKey: params.queryKey, queryKey: params.queryKey,

View File

@ -7,15 +7,12 @@ const editItem = ({
rootQueryKey, rootQueryKey,
status status
}: MutationVarsTimelineEditItem) => { }: MutationVarsTimelineEditItem) => {
console.log('START')
queryKey && queryKey &&
queryClient.setQueryData<InfiniteData<any> | undefined>(queryKey, old => { queryClient.setQueryData<InfiniteData<any> | undefined>(queryKey, old => {
if (old) { if (old) {
old.pages = old.pages.map(page => { old.pages = old.pages.map(page => {
page.body = page.body.map((item: Mastodon.Status) => { page.body = page.body.map((item: Mastodon.Status) => {
if (item.id === status.id) { if (item.id === status.id) {
console.log('found queryKey', queryKey)
console.log('new content', status.content)
item = status item = status
} }
return item return item
@ -34,8 +31,6 @@ const editItem = ({
old.pages = old.pages.map(page => { old.pages = old.pages.map(page => {
page.body = page.body.map((item: Mastodon.Status) => { page.body = page.body.map((item: Mastodon.Status) => {
if (item.id === status.id) { if (item.id === status.id) {
console.log('found rootQueryKey', queryKey)
console.log('new content', status.content)
item = status item = status
} }
return item return item
@ -46,7 +41,6 @@ const editItem = ({
} }
} }
) )
console.log('EDN')
} }
export default editItem export default editItem