mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Updates
This commit is contained in:
		| @@ -1,5 +1,5 @@ | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import React, { useLayoutEffect, useMemo } from 'react' | ||||
| import React, { useEffect, useMemo } from 'react' | ||||
| import { | ||||
|   Pressable, | ||||
|   StyleProp, | ||||
| @@ -46,7 +46,7 @@ const Button: React.FC<Props> = ({ | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   useLayoutEffect(() => layoutAnimation(), [content, loading, disabled]) | ||||
|   useEffect(() => layoutAnimation(), [content, loading, disabled]) | ||||
|  | ||||
|   const loadingSpinkit = useMemo( | ||||
|     () => ( | ||||
|   | ||||
							
								
								
									
										4
									
								
								src/components/Parse.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/components/Parse.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| import ParseEmojis from './Parse/Emojis' | ||||
| import ParseHTML from './Parse/HTML' | ||||
|  | ||||
| export { ParseEmojis, ParseHTML } | ||||
							
								
								
									
										70
									
								
								src/components/Parse/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/components/Parse/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import React from 'react' | ||||
| import { Image, StyleSheet, Text } from 'react-native' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
|  | ||||
| const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/) | ||||
|  | ||||
| export interface Props { | ||||
|   content: string | ||||
|   emojis?: Mastodon.Emoji[] | ||||
|   size?: 'S' | 'M' | 'L' | ||||
|   fontBold?: boolean | ||||
| } | ||||
|  | ||||
| const ParseEmojis: React.FC<Props> = ({ | ||||
|   content, | ||||
|   emojis, | ||||
|   size = 'M', | ||||
|   fontBold = false | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const styles = StyleSheet.create({ | ||||
|     text: { | ||||
|       color: theme.primary, | ||||
|       ...StyleConstants.FontStyle[size], | ||||
|       ...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold }) | ||||
|     }, | ||||
|     image: { | ||||
|       width: StyleConstants.Font.Size[size], | ||||
|       height: StyleConstants.Font.Size[size] | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   return ( | ||||
|     <Text style={styles.text}> | ||||
|       {emojis ? ( | ||||
|         content | ||||
|           .split(regexEmoji) | ||||
|           .filter(f => f) | ||||
|           .map((str, i) => { | ||||
|             if (str.match(regexEmoji)) { | ||||
|               const emojiShortcode = str.split(regexEmoji)[1] | ||||
|               const emojiIndex = emojis.findIndex(emoji => { | ||||
|                 return emojiShortcode === `:${emoji.shortcode}:` | ||||
|               }) | ||||
|               return emojiIndex === -1 ? ( | ||||
|                 <Text key={i}>{emojiShortcode}</Text> | ||||
|               ) : ( | ||||
|                 <Text key={i}> | ||||
|                   {/* When emoji starts a paragraph, lineHeight will break */} | ||||
|                   {i === 0 ? <Text> </Text> : null} | ||||
|                   <Image | ||||
|                     resizeMode='contain' | ||||
|                     source={{ uri: emojis[emojiIndex].url }} | ||||
|                     style={[styles.image]} | ||||
|                   /> | ||||
|                 </Text> | ||||
|               ) | ||||
|             } else { | ||||
|               return <Text key={i}>{str}</Text> | ||||
|             } | ||||
|           }) | ||||
|       ) : ( | ||||
|         <Text>{content}</Text> | ||||
|       )} | ||||
|     </Text> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default React.memo(ParseEmojis, () => true) | ||||
| @@ -1,14 +1,14 @@ | ||||
| import openLink from '@components/openLink' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { LinearGradient } from 'expo-linear-gradient' | ||||
| import React, { useCallback, useMemo, useState } from 'react' | ||||
| import { Pressable, Text, View } from 'react-native' | ||||
| import HTMLView from 'react-native-htmlview' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import Emojis from '@components/Timelines/Timeline/Shared/Emojis' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import openLink from '@root/utils/openLink' | ||||
| import layoutAnimation from '@root/utils/styles/layoutAnimation' | ||||
| 
 | ||||
| // Prevent going to the same hashtag multiple times
 | ||||
| const renderNode = ({ | ||||
| @@ -126,7 +126,7 @@ export interface Props { | ||||
|   expandHint?: string | ||||
| } | ||||
| 
 | ||||
| const ParseContent: React.FC<Props> = ({ | ||||
| const ParseHTML: React.FC<Props> = ({ | ||||
|   content, | ||||
|   size = 'M', | ||||
|   emojis, | ||||
| @@ -155,11 +155,7 @@ const ParseContent: React.FC<Props> = ({ | ||||
|   ) | ||||
|   const textComponent = useCallback(({ children }) => { | ||||
|     if (children) { | ||||
|       return emojis ? ( | ||||
|         <Emojis content={children.toString()} emojis={emojis} size={size} /> | ||||
|       ) : ( | ||||
|         <Text style={{ ...StyleConstants.FontStyle[size] }}>{children}</Text> | ||||
|       ) | ||||
|       return <ParseEmojis content={children.toString()} emojis={emojis} /> | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
| @@ -170,7 +166,9 @@ const ParseContent: React.FC<Props> = ({ | ||||
| 
 | ||||
|       const [heightOriginal, setHeightOriginal] = useState<number>() | ||||
|       const [heightTruncated, setHeightTruncated] = useState<number>() | ||||
|       const [allowExpand, setAllowExpand] = useState(false) | ||||
|       const [allowExpand, setAllowExpand] = useState( | ||||
|         numberOfLines === 0 ? true : undefined | ||||
|       ) | ||||
|       const [showAllText, setShowAllText] = useState(false) | ||||
| 
 | ||||
|       const calNumberOfLines = useMemo(() => { | ||||
| @@ -189,27 +187,36 @@ const ParseContent: React.FC<Props> = ({ | ||||
|         } | ||||
|       }, [heightOriginal, heightTruncated, allowExpand, showAllText]) | ||||
| 
 | ||||
|       const onLayout = useCallback( | ||||
|         ({ nativeEvent }) => { | ||||
|           if (!heightOriginal) { | ||||
|             setHeightOriginal(nativeEvent.layout.height) | ||||
|           } else { | ||||
|             if (!heightTruncated) { | ||||
|               setHeightTruncated(nativeEvent.layout.height) | ||||
|             } else { | ||||
|               if (heightOriginal > heightTruncated) { | ||||
|                 setAllowExpand(true) | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         [heightOriginal, heightTruncated] | ||||
|       ) | ||||
| 
 | ||||
|       return ( | ||||
|         <View> | ||||
|           <Text | ||||
|             style={{ color: theme.primary, overflow: 'hidden' }} | ||||
|             style={{ | ||||
|               ...StyleConstants.FontStyle[size], | ||||
|               color: theme.primary, | ||||
|               overflow: 'hidden' | ||||
|             }} | ||||
|             children={children} | ||||
|             numberOfLines={calNumberOfLines} | ||||
|             onLayout={({ nativeEvent }) => { | ||||
|               if (!heightOriginal) { | ||||
|                 setHeightOriginal(nativeEvent.layout.height) | ||||
|               } else { | ||||
|                 if (!heightTruncated) { | ||||
|                   setHeightTruncated(nativeEvent.layout.height) | ||||
|                 } else { | ||||
|                   if (heightOriginal > heightTruncated) { | ||||
|                     setAllowExpand(true) | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|             }} | ||||
|             onLayout={allowExpand === undefined ? onLayout : undefined} | ||||
|           /> | ||||
|           {allowExpand && ( | ||||
|           {allowExpand ? ( | ||||
|             <Pressable | ||||
|               onPress={() => { | ||||
|                 layoutAnimation() | ||||
| @@ -239,7 +246,7 @@ const ParseContent: React.FC<Props> = ({ | ||||
|                 </Text> | ||||
|               </LinearGradient> | ||||
|             </Pressable> | ||||
|           )} | ||||
|           ) : null} | ||||
|         </View> | ||||
|       ) | ||||
|     }, | ||||
| @@ -256,4 +263,4 @@ const ParseContent: React.FC<Props> = ({ | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default ParseContent | ||||
| export default ParseHTML | ||||
| @@ -34,19 +34,22 @@ const Timelines: React.FC<Props> = ({ name, content }) => { | ||||
|     .filter(p => (localRegistered ? true : p.page === 'RemotePublic')) | ||||
|     .map(p => ({ key: p.page })) | ||||
|  | ||||
|   const renderScene = ({ | ||||
|     route | ||||
|   }: { | ||||
|     route: { | ||||
|       key: App.Pages | ||||
|     } | ||||
|   }) => { | ||||
|     return ( | ||||
|       (localRegistered || route.key === 'RemotePublic') && ( | ||||
|         <Timeline page={route.key} /> | ||||
|   const renderScene = useCallback( | ||||
|     ({ | ||||
|       route | ||||
|     }: { | ||||
|       route: { | ||||
|         key: App.Pages | ||||
|       } | ||||
|     }) => { | ||||
|       return ( | ||||
|         (localRegistered || route.key === 'RemotePublic') && ( | ||||
|           <Timeline page={route.key} /> | ||||
|         ) | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
|     }, | ||||
|     [localRegistered] | ||||
|   ) | ||||
|  | ||||
|   const screenComponent = useCallback( | ||||
|     () => ( | ||||
|   | ||||
| @@ -29,6 +29,7 @@ export interface Props { | ||||
|   toot?: Mastodon.Status | ||||
|   account?: string | ||||
|   disableRefresh?: boolean | ||||
|   disableInfinity?: boolean | ||||
| } | ||||
|  | ||||
| const Timeline: React.FC<Props> = ({ | ||||
| @@ -37,7 +38,8 @@ const Timeline: React.FC<Props> = ({ | ||||
|   list, | ||||
|   toot, | ||||
|   account, | ||||
|   disableRefresh = false | ||||
|   disableRefresh = false, | ||||
|   disableInfinity = false | ||||
| }) => { | ||||
|   const queryKey: QueryKey.Timeline = [ | ||||
|     page, | ||||
| @@ -152,11 +154,11 @@ const Timeline: React.FC<Props> = ({ | ||||
|     [status] | ||||
|   ) | ||||
|   const onEndReached = useCallback( | ||||
|     () => !disableRefresh && !isFetchingNextPage && fetchNextPage(), | ||||
|     () => !disableInfinity && !isFetchingNextPage && fetchNextPage(), | ||||
|     [isFetchingNextPage] | ||||
|   ) | ||||
|   const ListFooterComponent = useCallback( | ||||
|     () => <TimelineEnd hasNextPage={!disableRefresh ? hasNextPage : false} />, | ||||
|     () => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />, | ||||
|     [hasNextPage] | ||||
|   ) | ||||
|   const refreshControl = useMemo( | ||||
| @@ -197,6 +199,10 @@ const Timeline: React.FC<Props> = ({ | ||||
|       {...(!disableRefresh && { refreshControl })} | ||||
|       ItemSeparatorComponent={ItemSeparatorComponent} | ||||
|       {...(toot && isSuccess && { onScrollToIndexFailed })} | ||||
|       maintainVisibleContentPosition={{ | ||||
|         minIndexForVisible: 0, | ||||
|         autoscrollToTopThreshold: 2 | ||||
|       }} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| @@ -207,4 +213,6 @@ const styles = StyleSheet.create({ | ||||
|   } | ||||
| }) | ||||
|  | ||||
| // Timeline.whyDidYouRender = true | ||||
|  | ||||
| export default Timeline | ||||
|   | ||||
| @@ -111,7 +111,7 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
| const styles = StyleSheet.create({ | ||||
|   statusView: { | ||||
|     padding: StyleConstants.Spacing.Global.PagePadding, | ||||
|     paddingBottom: StyleConstants.Spacing.M | ||||
|     paddingBottom: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   header: { | ||||
|     flex: 1, | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import React from 'react' | ||||
| import { StyleSheet, Text, View } from 'react-native' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
|  | ||||
| import Emojis from '@components/Timelines/Timeline/Shared/Emojis' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import React, { useCallback, useMemo } from 'react' | ||||
| import { Pressable, StyleSheet, View } from 'react-native' | ||||
| import { ParseEmojis } from '@root/components/Parse' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
|  | ||||
| export interface Props { | ||||
|   account: Mastodon.Account | ||||
|   action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog' | 'pinned' | ||||
|   action: 'favourite' | 'follow' | 'poll' | 'reblog' | 'pinned' | 'mention' | ||||
|   notification?: boolean | ||||
| } | ||||
|  | ||||
| @@ -18,96 +18,106 @@ const TimelineActioned: React.FC<Props> = ({ | ||||
|   notification = false | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const navigation = useNavigation() | ||||
|   const name = account.display_name || account.username | ||||
|   const iconColor = theme.primary | ||||
|  | ||||
|   let icon | ||||
|   let content | ||||
|   switch (action) { | ||||
|     case 'pinned': | ||||
|       icon = ( | ||||
|         <Feather | ||||
|           name='anchor' | ||||
|           size={StyleConstants.Font.Size.S} | ||||
|           color={iconColor} | ||||
|           style={styles.icon} | ||||
|         /> | ||||
|       ) | ||||
|       content = `置顶` | ||||
|       break | ||||
|     case 'favourite': | ||||
|       icon = ( | ||||
|         <Feather | ||||
|           name='heart' | ||||
|           size={StyleConstants.Font.Size.S} | ||||
|           color={iconColor} | ||||
|           style={styles.icon} | ||||
|         /> | ||||
|       ) | ||||
|       content = `${name} 喜欢了你的嘟嘟` | ||||
|       break | ||||
|     case 'follow': | ||||
|       icon = ( | ||||
|         <Feather | ||||
|           name='user-plus' | ||||
|           size={StyleConstants.Font.Size.S} | ||||
|           color={iconColor} | ||||
|           style={styles.icon} | ||||
|         /> | ||||
|       ) | ||||
|       content = `${name} 开始关注你` | ||||
|       break | ||||
|     case 'poll': | ||||
|       icon = ( | ||||
|         <Feather | ||||
|           name='bar-chart-2' | ||||
|           size={StyleConstants.Font.Size.S} | ||||
|           color={iconColor} | ||||
|           style={styles.icon} | ||||
|         /> | ||||
|       ) | ||||
|       content = `你参与的投票已结束` | ||||
|       break | ||||
|     case 'reblog': | ||||
|       icon = ( | ||||
|         <Feather | ||||
|           name='repeat' | ||||
|           size={StyleConstants.Font.Size.S} | ||||
|           color={iconColor} | ||||
|           style={styles.icon} | ||||
|         /> | ||||
|       ) | ||||
|       content = `${name} 转嘟了${notification ? '你的嘟嘟' : ''}` | ||||
|       break | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <View style={styles.actioned}> | ||||
|       {icon} | ||||
|       {content && ( | ||||
|         <View style={styles.content}> | ||||
|           {account.emojis ? ( | ||||
|             <Emojis content={content} emojis={account.emojis} size='S' /> | ||||
|           ) : ( | ||||
|             <Text>{content}</Text> | ||||
|           )} | ||||
|         </View> | ||||
|       )} | ||||
|     </View> | ||||
|   const content = (content: string) => ( | ||||
|     <ParseEmojis content={content} emojis={account.emojis} size='S' /> | ||||
|   ) | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|     navigation.push('Screen-Shared-Account', { account }) | ||||
|   }, []) | ||||
|  | ||||
|   const children = useMemo(() => { | ||||
|     switch (action) { | ||||
|       case 'pinned': | ||||
|         return ( | ||||
|           <> | ||||
|             <Feather | ||||
|               name='anchor' | ||||
|               size={StyleConstants.Font.Size.S} | ||||
|               color={iconColor} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|             {content('置顶')} | ||||
|           </> | ||||
|         ) | ||||
|         break | ||||
|       case 'favourite': | ||||
|         return ( | ||||
|           <> | ||||
|             <Feather | ||||
|               name='heart' | ||||
|               size={StyleConstants.Font.Size.S} | ||||
|               color={iconColor} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|             <Pressable onPress={onPress}> | ||||
|               {content(`${name} 喜欢了你的嘟嘟`)} | ||||
|             </Pressable> | ||||
|           </> | ||||
|         ) | ||||
|         break | ||||
|       case 'follow': | ||||
|         return ( | ||||
|           <> | ||||
|             <Feather | ||||
|               name='user-plus' | ||||
|               size={StyleConstants.Font.Size.S} | ||||
|               color={iconColor} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|             <Pressable onPress={onPress}> | ||||
|               {content(`${name} 开始关注你`)} | ||||
|             </Pressable> | ||||
|           </> | ||||
|         ) | ||||
|         break | ||||
|       case 'poll': | ||||
|         return ( | ||||
|           <> | ||||
|             <Feather | ||||
|               name='bar-chart-2' | ||||
|               size={StyleConstants.Font.Size.S} | ||||
|               color={iconColor} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|             {content('你参与的投票已结束')} | ||||
|           </> | ||||
|         ) | ||||
|         break | ||||
|       case 'reblog': | ||||
|         return ( | ||||
|           <> | ||||
|             <Feather | ||||
|               name='repeat' | ||||
|               size={StyleConstants.Font.Size.S} | ||||
|               color={iconColor} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|             <Pressable onPress={onPress}> | ||||
|               {content(`${name} 转嘟了${notification ? '你的嘟嘟' : ''}`)} | ||||
|             </Pressable> | ||||
|           </> | ||||
|         ) | ||||
|         break | ||||
|     } | ||||
|   }, []) | ||||
|  | ||||
|   return <View style={styles.actioned} children={children} /> | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   actioned: { | ||||
|     flexDirection: 'row', | ||||
|     marginBottom: StyleConstants.Spacing.S | ||||
|     marginBottom: StyleConstants.Spacing.S, | ||||
|     paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S, | ||||
|     paddingRight: StyleConstants.Spacing.Global.PagePadding | ||||
|   }, | ||||
|   icon: { | ||||
|     marginLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S, | ||||
|     marginRight: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   content: { | ||||
|     flexDirection: 'row' | ||||
|     paddingRight: StyleConstants.Spacing.S | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,16 +1,15 @@ | ||||
| import client from '@api/client' | ||||
| import haptics from '@components/haptics' | ||||
| import { TimelineData } from '@components/Timelines/Timeline' | ||||
| import { toast } from '@components/toast' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { findIndex } from 'lodash' | ||||
| import React, { useCallback, useMemo } from 'react' | ||||
| import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import { useMutation, useQueryClient } from 'react-query' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
|  | ||||
| import client from '@api/client' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { toast } from '@components/toast' | ||||
| 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, | ||||
| @@ -35,10 +34,8 @@ const fireMutation = async ({ | ||||
|       }) // bug in response from Mastodon | ||||
|  | ||||
|       if (!res.body[stateKey] === prevState) { | ||||
|         toast({ type: 'success', content: '功能成功' }) | ||||
|         return Promise.resolve(res.body) | ||||
|       } else { | ||||
|         toast({ type: 'error', content: '功能错误' }) | ||||
|         return Promise.reject() | ||||
|       } | ||||
|       break | ||||
| @@ -64,6 +61,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => { | ||||
|       queryClient.cancelQueries(queryKey) | ||||
|       const oldData = queryClient.getQueryData(queryKey) | ||||
|  | ||||
|       haptics('Success') | ||||
|       switch (type) { | ||||
|         case 'favourite': | ||||
|         case 'reblog': | ||||
| @@ -111,7 +109,6 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => { | ||||
|  | ||||
|             return old | ||||
|           }) | ||||
|           haptics('Success') | ||||
|           break | ||||
|       } | ||||
|  | ||||
| @@ -175,8 +172,8 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => { | ||||
|             'com.apple.UIKit.activity.OpenInIBooks' | ||||
|           ] | ||||
|         }, | ||||
|         () => haptics('Success'), | ||||
|         () => haptics('Error') | ||||
|         () => haptics('Error'), | ||||
|         () => haptics('Success') | ||||
|       ), | ||||
|     [] | ||||
|   ) | ||||
| @@ -294,12 +291,13 @@ const styles = StyleSheet.create({ | ||||
|     width: '100%', | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     marginTop: StyleConstants.Spacing.M | ||||
|     marginTop: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   action: { | ||||
|     width: '20%', | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'center' | ||||
|     justifyContent: 'center', | ||||
|     paddingVertical: StyleConstants.Spacing.S | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import Button from '@components/Button' | ||||
| import openLink from '@root/utils/openLink' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { useTheme } from '@root/utils/styles/ThemeManager' | ||||
| import openLink from '@components/openLink' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { Surface } from 'gl-react-expo' | ||||
| import { Blurhash } from 'gl-react-blurhash' | ||||
| import React from 'react' | ||||
| @@ -38,7 +38,7 @@ const AttachmentUnsupported: React.FC<Props> = ({ | ||||
|               { color: attachment.blurhash ? theme.background : theme.primary } | ||||
|             ]} | ||||
|           > | ||||
|             文件不支持 | ||||
|             文件读取错误 | ||||
|           </Text> | ||||
|           {attachment.remote_url ? ( | ||||
|             <Button | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import React, { useEffect, useMemo, useState } from 'react' | ||||
| import { Image, Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import openLink from '@components/openLink' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import openLink from '@root/utils/openLink' | ||||
| import { Surface } from 'gl-react-expo' | ||||
| import { Blurhash } from 'gl-react-blurhash' | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { ParseHTML } from '@components/Parse' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import React from 'react' | ||||
| import { View } from 'react-native' | ||||
| import ParseContent from '@components/ParseContent' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
|  | ||||
| export interface Props { | ||||
|   status: Mastodon.Status | ||||
| @@ -18,28 +18,28 @@ const TimelineContent: React.FC<Props> = ({ | ||||
|     <> | ||||
|       {status.spoiler_text ? ( | ||||
|         <> | ||||
|           <ParseContent | ||||
|             content={status.spoiler_text} | ||||
|             size={highlighted ? 'L' : 'M'} | ||||
|             emojis={status.emojis} | ||||
|             mentions={status.mentions} | ||||
|             tags={status.tags} | ||||
|             numberOfLines={999} | ||||
|           /> | ||||
|           <View style={{ marginTop: StyleConstants.Font.Size.M }}> | ||||
|             <ParseContent | ||||
|               content={status.content} | ||||
|           <View style={{ marginBottom: StyleConstants.Font.Size.M }}> | ||||
|             <ParseHTML | ||||
|               content={status.spoiler_text} | ||||
|               size={highlighted ? 'L' : 'M'} | ||||
|               emojis={status.emojis} | ||||
|               mentions={status.mentions} | ||||
|               tags={status.tags} | ||||
|               numberOfLines={1} | ||||
|               expandHint='隐藏内容' | ||||
|               numberOfLines={999} | ||||
|             /> | ||||
|           </View> | ||||
|           <ParseHTML | ||||
|             content={status.content} | ||||
|             size={highlighted ? 'L' : 'M'} | ||||
|             emojis={status.emojis} | ||||
|             mentions={status.mentions} | ||||
|             tags={status.tags} | ||||
|             numberOfLines={0} | ||||
|             expandHint='隐藏内容' | ||||
|           /> | ||||
|         </> | ||||
|       ) : ( | ||||
|         <ParseContent | ||||
|         <ParseHTML | ||||
|           content={status.content} | ||||
|           size={highlighted ? 'L' : 'M'} | ||||
|           emojis={status.emojis} | ||||
|   | ||||
| @@ -1,72 +0,0 @@ | ||||
| import React from 'react' | ||||
| import { Image, StyleSheet, Text } from 'react-native' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
|  | ||||
| const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/) | ||||
|  | ||||
| export interface Props { | ||||
|   content: string | ||||
|   emojis: Mastodon.Emoji[] | ||||
|   size?: 'S' | 'M' | 'L' | ||||
|   fontBold?: boolean | ||||
|   numberOfLines?: number | ||||
| } | ||||
|  | ||||
| const Emojis: React.FC<Props> = ({ | ||||
|   content, | ||||
|   emojis, | ||||
|   size = 'M', | ||||
|   fontBold = false, | ||||
|   numberOfLines | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const styles = StyleSheet.create({ | ||||
|     text: { | ||||
|       color: theme.primary, | ||||
|       ...StyleConstants.FontStyle[size], | ||||
|       ...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold }) | ||||
|     }, | ||||
|     image: { | ||||
|       width: StyleConstants.Font.Size[size], | ||||
|       height: StyleConstants.Font.Size[size], | ||||
|       marginBottom: -2 // hacking | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   return ( | ||||
|     <Text numberOfLines={numberOfLines || undefined}> | ||||
|       {content.split(regexEmoji).map((str, i) => { | ||||
|         if (str.match(regexEmoji)) { | ||||
|           const emojiShortcode = str.split(regexEmoji)[1] | ||||
|           const emojiIndex = emojis.findIndex(emoji => { | ||||
|             return emojiShortcode === `:${emoji.shortcode}:` | ||||
|           }) | ||||
|           return emojiIndex === -1 ? ( | ||||
|             <Text key={i} style={styles.text}> | ||||
|               {emojiShortcode} | ||||
|             </Text> | ||||
|           ) : ( | ||||
|             <Image | ||||
|               key={i} | ||||
|               resizeMode='contain' | ||||
|               source={{ uri: emojis[emojiIndex].url }} | ||||
|               style={[styles.image]} | ||||
|             /> | ||||
|           ) | ||||
|         } else { | ||||
|           return str ? ( | ||||
|             <Text key={i} style={styles.text}> | ||||
|               {str} | ||||
|             </Text> | ||||
|           ) : ( | ||||
|             undefined | ||||
|           ) | ||||
|         } | ||||
|       })} | ||||
|     </Text> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| // export default React.memo(Emojis, () => true) | ||||
| export default Emojis | ||||
| @@ -1,15 +1,14 @@ | ||||
| import client from '@api/client' | ||||
| import haptics from '@components/haptics' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import relativeTime from '@components/relativeTime' | ||||
| import { toast } from '@components/toast' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useMemo } from 'react' | ||||
| import { Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import { useMutation, useQueryClient } from 'react-query' | ||||
| import client from '@api/client' | ||||
| import { toast } from '@components/toast' | ||||
|  | ||||
| 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 | ||||
| @@ -43,6 +42,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => { | ||||
|       queryClient.cancelQueries(queryKey) | ||||
|       const oldData = queryClient.getQueryData(queryKey) | ||||
|  | ||||
|       haptics('Success') | ||||
|       queryClient.setQueryData(queryKey, (old: any) => | ||||
|         old.pages.map((paging: any) => ({ | ||||
|           toots: paging.toots.filter( | ||||
| @@ -51,7 +51,6 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => { | ||||
|           pointer: paging.pointer | ||||
|         })) | ||||
|       ) | ||||
|       haptics('Success') | ||||
|  | ||||
|       return oldData | ||||
|     }, | ||||
| @@ -80,26 +79,17 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => { | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <View style={styles.nameAndDate}> | ||||
|         <View style={styles.name}> | ||||
|           {conversation.accounts[0].emojis ? ( | ||||
|             <Emojis | ||||
|         <View style={styles.namdAndAccount}> | ||||
|           <Text numberOfLines={1}> | ||||
|             <ParseEmojis | ||||
|               content={ | ||||
|                 conversation.accounts[0].display_name || | ||||
|                 conversation.accounts[0].username | ||||
|               } | ||||
|               emojis={conversation.accounts[0].emojis} | ||||
|               size='M' | ||||
|               fontBold={true} | ||||
|               fontBold | ||||
|             /> | ||||
|           ) : ( | ||||
|             <Text | ||||
|               numberOfLines={1} | ||||
|               style={[styles.nameWithoutEmoji, { color: theme.primary }]} | ||||
|             > | ||||
|               {conversation.accounts[0].display_name || | ||||
|                 conversation.accounts[0].username} | ||||
|             </Text> | ||||
|           )} | ||||
|           </Text> | ||||
|           <Text | ||||
|             style={[styles.account, { color: theme.secondary }]} | ||||
|             numberOfLines={1} | ||||
| @@ -136,7 +126,7 @@ const styles = StyleSheet.create({ | ||||
|   nameAndDate: { | ||||
|     width: '80%' | ||||
|   }, | ||||
|   name: { | ||||
|   namdAndAccount: { | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center' | ||||
|   }, | ||||
| @@ -144,10 +134,6 @@ const styles = StyleSheet.create({ | ||||
|     flexShrink: 1, | ||||
|     marginLeft: StyleConstants.Spacing.XS | ||||
|   }, | ||||
|   nameWithoutEmoji: { | ||||
|     ...StyleConstants.FontStyle.M, | ||||
|     fontWeight: StyleConstants.Font.Weight.Bold | ||||
|   }, | ||||
|   meta: { | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center', | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import BottomSheet from '@components/BottomSheet' | ||||
| import openLink from '@components/openLink' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import relativeTime from '@components/relativeTime' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { getLocalUrl } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount' | ||||
| import HeaderDefaultActionsDomain from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsDomain' | ||||
| import HeaderDefaultActionsStatus from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus' | ||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react' | ||||
| import { Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import Emojis from '@components/Timelines/Timeline/Shared/Emojis' | ||||
| import relativeTime from '@utils/relativeTime' | ||||
| import { getLocalUrl } from '@utils/slices/instancesSlice' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import BottomSheet from '@components/BottomSheet' | ||||
| import { useSelector } from 'react-redux' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount' | ||||
| import HeaderDefaultActionsStatus from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus' | ||||
| import HeaderDefaultActionsDomain from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsDomain' | ||||
| import openLink from '@root/utils/openLink' | ||||
|  | ||||
| export interface Props { | ||||
|   queryKey?: QueryKey.Timeline | ||||
| @@ -69,21 +69,9 @@ const TimelineHeaderDefault: React.FC<Props> = ({ | ||||
|     <View style={styles.base}> | ||||
|       <View style={queryKey ? { flexBasis: '80%' } : { flexBasis: '100%' }}> | ||||
|         <View style={styles.nameAndAccount}> | ||||
|           {emojis?.length ? ( | ||||
|             <Emojis | ||||
|               content={name} | ||||
|               emojis={emojis} | ||||
|               size='M' | ||||
|               fontBold={true} | ||||
|             /> | ||||
|           ) : ( | ||||
|             <Text | ||||
|               numberOfLines={1} | ||||
|               style={[styles.nameWithoutEmoji, { color: theme.primary }]} | ||||
|             > | ||||
|               {name} | ||||
|             </Text> | ||||
|           )} | ||||
|           <Text numberOfLines={1}> | ||||
|             <ParseEmojis content={name} emojis={emojis} fontBold /> | ||||
|           </Text> | ||||
|           <Text | ||||
|             style={[styles.account, { color: theme.secondary }]} | ||||
|             numberOfLines={1} | ||||
| @@ -163,7 +151,8 @@ const TimelineHeaderDefault: React.FC<Props> = ({ | ||||
| const styles = StyleSheet.create({ | ||||
|   base: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row' | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'baseline' | ||||
|   }, | ||||
|   nameAndMeta: { | ||||
|     flexBasis: '80%' | ||||
| @@ -172,10 +161,6 @@ const styles = StyleSheet.create({ | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center' | ||||
|   }, | ||||
|   nameWithoutEmoji: { | ||||
|     ...StyleConstants.FontStyle.M, | ||||
|     fontWeight: StyleConstants.Font.Weight.Bold | ||||
|   }, | ||||
|   account: { | ||||
|     flex: 1, | ||||
|     marginLeft: StyleConstants.Spacing.XS | ||||
| @@ -199,7 +184,8 @@ const styles = StyleSheet.create({ | ||||
|   action: { | ||||
|     flexBasis: '20%', | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'center' | ||||
|     justifyContent: 'center', | ||||
|     paddingBottom: StyleConstants.Spacing.S | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import client from '@api/client' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import haptics from '@components/haptics' | ||||
| import openLink from '@components/openLink' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import relativeTime from '@components/relativeTime' | ||||
| import { toast } from '@components/toast' | ||||
| import { relationshipFetch } from '@utils/fetches/relationshipFetch' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react' | ||||
| import { Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import { Chase } from 'react-native-animated-spinkit' | ||||
| import { useQuery } from 'react-query' | ||||
| import client from '@api/client' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import Emojis from '@components/Timelines/Timeline/Shared/Emojis' | ||||
| import { toast } from '@components/toast' | ||||
| import openLink from '@root/utils/openLink' | ||||
| 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 | ||||
| @@ -129,16 +129,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => { | ||||
|     <View style={styles.base}> | ||||
|       <View style={styles.nameAndMeta}> | ||||
|         <View style={styles.nameAndAccount}> | ||||
|           {emojis?.length ? ( | ||||
|             <Emojis content={name} emojis={emojis} size='M' fontBold={true} /> | ||||
|           ) : ( | ||||
|             <Text | ||||
|               numberOfLines={1} | ||||
|               style={[styles.nameWithoutEmoji, { color: theme.primary }]} | ||||
|             > | ||||
|               {name} | ||||
|             </Text> | ||||
|           )} | ||||
|           <Text numberOfLines={1}> | ||||
|             <ParseEmojis content={name} emojis={emojis} fontBold /> | ||||
|           </Text> | ||||
|           <Text | ||||
|             style={[styles.account, { color: theme.secondary }]} | ||||
|             numberOfLines={1} | ||||
| @@ -194,10 +187,6 @@ const styles = StyleSheet.create({ | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center' | ||||
|   }, | ||||
|   nameWithoutEmoji: { | ||||
|     ...StyleConstants.FontStyle.M, | ||||
|     fontWeight: StyleConstants.Font.Weight.Bold | ||||
|   }, | ||||
|   account: { | ||||
|     flexShrink: 1, | ||||
|     marginLeft: StyleConstants.Spacing.XS | ||||
|   | ||||
| @@ -1,18 +1,16 @@ | ||||
| import client from '@api/client' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import relativeTime from '@components/relativeTime' | ||||
| import { TimelineData } from '@components/Timelines/Timeline' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { ParseEmojis } from '@root/components/Parse' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { findIndex } from 'lodash' | ||||
| import React, { useCallback, useMemo, useState } from 'react' | ||||
| import { Pressable, StyleSheet, Text, View } from 'react-native' | ||||
| import { useMutation, useQueryClient } from 'react-query' | ||||
| import client from '@api/client' | ||||
| import Button from '@components/Button' | ||||
| import { toast } from '@components/toast' | ||||
| import relativeTime from '@utils/relativeTime' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| 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, | ||||
| @@ -39,11 +37,6 @@ const fireMutation = async ({ | ||||
|   if (res.body.id === id) { | ||||
|     return Promise.resolve(res.body as Mastodon.Poll) | ||||
|   } else { | ||||
|     toast({ | ||||
|       type: 'error', | ||||
|       content: '投票失败,请重试', | ||||
|       autoHide: false | ||||
|     }) | ||||
|     return Promise.reject() | ||||
|   } | ||||
| } | ||||
| @@ -61,7 +54,7 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|   reblog, | ||||
|   sameAccount | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const { mode, theme } = useTheme() | ||||
|   const queryClient = useQueryClient() | ||||
|  | ||||
|   const [allOptions, setAllOptions] = useState( | ||||
| @@ -98,10 +91,13 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|       }) | ||||
|  | ||||
|       haptics('Success') | ||||
|     }, | ||||
|     onError: () => { | ||||
|       haptics('Error') | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   const pollButton = () => { | ||||
|   const pollButton = useMemo(() => { | ||||
|     if (!poll.expired) { | ||||
|       if (!sameAccount && !poll.voted) { | ||||
|         return ( | ||||
| @@ -131,7 +127,7 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   }, [poll.expired, poll.voted, allOptions, mutation.isLoading]) | ||||
|  | ||||
|   const pollExpiration = useMemo(() => { | ||||
|     if (poll.expired) { | ||||
| @@ -147,7 +143,7 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|         </Text> | ||||
|       ) | ||||
|     } | ||||
|   }, []) | ||||
|   }, [mode]) | ||||
|  | ||||
|   const isSelected = useCallback( | ||||
|     (index: number): any => | ||||
| @@ -157,101 +153,93 @@ const TimelinePoll: React.FC<Props> = ({ | ||||
|     [allOptions] | ||||
|   ) | ||||
|  | ||||
|   const pollBodyDisallow = useMemo(() => { | ||||
|     return poll.options.map((option, index) => ( | ||||
|       <View key={index} style={styles.optionContainer}> | ||||
|         <View style={styles.optionContent}> | ||||
|           <Feather | ||||
|             style={styles.optionSelection} | ||||
|             name={ | ||||
|               `${poll.own_votes?.includes(index) ? 'check-' : ''}${ | ||||
|                 poll.multiple ? 'square' : 'circle' | ||||
|               }` as any | ||||
|             } | ||||
|             size={StyleConstants.Font.Size.M} | ||||
|             color={ | ||||
|               poll.own_votes?.includes(index) ? theme.primary : theme.disabled | ||||
|             } | ||||
|           /> | ||||
|           <Text style={styles.optionText}> | ||||
|             <ParseEmojis content={option.title} emojis={poll.emojis} /> | ||||
|           </Text> | ||||
|           <Text style={[styles.optionPercentage, { color: theme.primary }]}> | ||||
|             {poll.votes_count | ||||
|               ? Math.round((option.votes_count / poll.voters_count) * 100) | ||||
|               : 0} | ||||
|             % | ||||
|           </Text> | ||||
|         </View> | ||||
|  | ||||
|         <View | ||||
|           style={[ | ||||
|             styles.background, | ||||
|             { | ||||
|               width: `${Math.round( | ||||
|                 (option.votes_count / poll.voters_count) * 100 | ||||
|               )}%`, | ||||
|               backgroundColor: theme.disabled | ||||
|             } | ||||
|           ]} | ||||
|         /> | ||||
|       </View> | ||||
|     )) | ||||
|   }, [mode, poll.options]) | ||||
|   const pollBodyAllow = useMemo(() => { | ||||
|     return poll.options.map((option, index) => ( | ||||
|       <Pressable | ||||
|         key={index} | ||||
|         style={styles.optionContainer} | ||||
|         onPress={() => { | ||||
|           haptics('Light') | ||||
|           if (poll.multiple) { | ||||
|             setAllOptions(allOptions.map((o, i) => (i === index ? !o : o))) | ||||
|           } else { | ||||
|             { | ||||
|               const otherOptions = | ||||
|                 allOptions[index] === false ? false : undefined | ||||
|               setAllOptions( | ||||
|                 allOptions.map((o, i) => | ||||
|                   i === index | ||||
|                     ? !o | ||||
|                     : otherOptions !== undefined | ||||
|                     ? otherOptions | ||||
|                     : o | ||||
|                 ) | ||||
|               ) | ||||
|             } | ||||
|           } | ||||
|         }} | ||||
|       > | ||||
|         <View style={[styles.optionContent]}> | ||||
|           <Feather | ||||
|             style={styles.optionSelection} | ||||
|             name={isSelected(index)} | ||||
|             size={StyleConstants.Font.Size.L} | ||||
|             color={theme.primary} | ||||
|           /> | ||||
|           <Text style={styles.optionText}> | ||||
|             <ParseEmojis content={option.title} emojis={poll.emojis} /> | ||||
|           </Text> | ||||
|         </View> | ||||
|       </Pressable> | ||||
|     )) | ||||
|   }, [mode, allOptions]) | ||||
|  | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       {poll.options.map((option, index) => | ||||
|         poll.voted ? ( | ||||
|           <View key={index} style={styles.poll}> | ||||
|             <View style={styles.optionSelected}> | ||||
|               <Feather | ||||
|                 style={styles.voted} | ||||
|                 name={ | ||||
|                   `${poll.own_votes!.includes(index) ? 'check-' : ''}${ | ||||
|                     poll.multiple ? 'square' : 'circle' | ||||
|                   }` as any | ||||
|                 } | ||||
|                 size={StyleConstants.Font.Size.M} | ||||
|                 color={ | ||||
|                   poll.own_votes!.includes(index) | ||||
|                     ? theme.primary | ||||
|                     : theme.disabled | ||||
|                 } | ||||
|               /> | ||||
|               <View style={styles.contentSelected}> | ||||
|                 <Emojis | ||||
|                   content={option.title} | ||||
|                   emojis={poll.emojis} | ||||
|                   size='M' | ||||
|                   numberOfLines={2} | ||||
|                 /> | ||||
|               </View> | ||||
|               <Text style={[styles.percentage, { color: theme.primary }]}> | ||||
|                 {poll.votes_count | ||||
|                   ? Math.round((option.votes_count / poll.voters_count) * 100) | ||||
|                   : 0} | ||||
|                 % | ||||
|               </Text> | ||||
|             </View> | ||||
|  | ||||
|             <View | ||||
|               style={[ | ||||
|                 styles.background, | ||||
|                 { | ||||
|                   width: `${Math.round( | ||||
|                     (option.votes_count / poll.voters_count) * 100 | ||||
|                   )}%`, | ||||
|                   backgroundColor: theme.disabled | ||||
|                 } | ||||
|               ]} | ||||
|             /> | ||||
|           </View> | ||||
|         ) : ( | ||||
|           <View key={index} style={styles.poll}> | ||||
|             <Pressable | ||||
|               style={[styles.optionUnselected]} | ||||
|               onPress={() => { | ||||
|                 haptics('Light') | ||||
|                 if (poll.multiple) { | ||||
|                   setAllOptions( | ||||
|                     allOptions.map((o, i) => (i === index ? !o : o)) | ||||
|                   ) | ||||
|                 } else { | ||||
|                   { | ||||
|                     const otherOptions = | ||||
|                       allOptions[index] === false ? false : undefined | ||||
|                     setAllOptions( | ||||
|                       allOptions.map((o, i) => | ||||
|                         i === index | ||||
|                           ? !o | ||||
|                           : otherOptions !== undefined | ||||
|                           ? otherOptions | ||||
|                           : o | ||||
|                       ) | ||||
|                     ) | ||||
|                   } | ||||
|                 } | ||||
|               }} | ||||
|             > | ||||
|               <Feather | ||||
|                 style={styles.votedNot} | ||||
|                 name={isSelected(index)} | ||||
|                 size={StyleConstants.Font.Size.L} | ||||
|                 color={theme.primary} | ||||
|               /> | ||||
|               <View style={styles.contentUnselected}> | ||||
|                 <Emojis | ||||
|                   content={option.title} | ||||
|                   emojis={poll.emojis} | ||||
|                   size='M' | ||||
|                   numberOfLines={2} | ||||
|                 /> | ||||
|               </View> | ||||
|             </Pressable> | ||||
|           </View> | ||||
|         ) | ||||
|       )} | ||||
|       {poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow} | ||||
|       <View style={styles.meta}> | ||||
|         {pollButton()} | ||||
|         {pollButton} | ||||
|         <Text style={[styles.votes, { color: theme.secondary }]}> | ||||
|           已投{poll.voters_count || 0}人{' • '} | ||||
|         </Text> | ||||
| @@ -265,39 +253,24 @@ const styles = StyleSheet.create({ | ||||
|   base: { | ||||
|     marginTop: StyleConstants.Spacing.M | ||||
|   }, | ||||
|   poll: { | ||||
|   optionContainer: { | ||||
|     flex: 1, | ||||
|     minHeight: StyleConstants.Font.LineHeight.M * 2, | ||||
|     paddingVertical: StyleConstants.Spacing.XS | ||||
|     paddingVertical: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   optionSelected: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
|     paddingRight: StyleConstants.Spacing.M | ||||
|   }, | ||||
|   optionUnselected: { | ||||
|   optionContent: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row' | ||||
|   }, | ||||
|   contentSelected: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center', | ||||
|     paddingRight: StyleConstants.Spacing.S | ||||
|   optionText: { | ||||
|     flex: 1 | ||||
|   }, | ||||
|   contentUnselected: { | ||||
|     flexShrink: 1 | ||||
|   }, | ||||
|   voted: { | ||||
|   optionSelection: { | ||||
|     marginRight: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   votedNot: { | ||||
|     paddingRight: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   percentage: { | ||||
|     ...StyleConstants.FontStyle.M | ||||
|   optionPercentage: { | ||||
|     ...StyleConstants.FontStyle.M, | ||||
|     alignSelf: 'center', | ||||
|     marginLeft: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   background: { | ||||
|     height: StyleConstants.Spacing.XS, | ||||
| @@ -314,7 +287,7 @@ const styles = StyleSheet.create({ | ||||
|     marginTop: StyleConstants.Spacing.XS | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: StyleConstants.Spacing.M | ||||
|     marginRight: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   votes: { | ||||
|     ...StyleConstants.FontStyle.S | ||||
|   | ||||
| @@ -2,8 +2,9 @@ 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) | ||||
|   Analytics.logEvent(event, params).catch( | ||||
|     error => {} | ||||
|     // Sentry.Native.captureException(error) | ||||
|   ) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { store } from '@root/store' | ||||
| import { getSettingsBrowser } from '@utils/slices/settingsSlice' | ||||
| import * as Linking from 'expo-linking' | ||||
| import * as WebBrowser from 'expo-web-browser' | ||||
| import { getSettingsBrowser } from './slices/settingsSlice' | ||||
| 
 | ||||
| const openLink = async (url: string) => { | ||||
|   switch (getSettingsBrowser(store.getState())) { | ||||
| @@ -12,7 +12,6 @@ import Logout from '@screens/Me/Root/Logout' | ||||
| import { useScrollToTop } from '@react-navigation/native' | ||||
| import { AccountState } from '../Shared/Account' | ||||
| import AccountNav from '../Shared/Account/Nav' | ||||
| import layoutAnimation from '@root/utils/styles/layoutAnimation' | ||||
|  | ||||
| const ScreenMeRoot: React.FC = () => { | ||||
|   const localRegistered = useSelector(getLocalUrl) | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import ParseContent from '@components/ParseContent' | ||||
| import haptics from '@components/haptics' | ||||
| import { ParseHTML } from '@components/Parse' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import analytics from '@root/components/analytics' | ||||
| import haptics from '@root/components/haptics' | ||||
| import { applicationFetch } from '@utils/fetches/applicationFetch' | ||||
| import { instanceFetch } from '@utils/fetches/instanceFetch' | ||||
| import { loginLocal } from '@utils/slices/instancesSlice' | ||||
| @@ -144,7 +144,12 @@ const Login: React.FC = () => { | ||||
|             height={StyleConstants.Font.Size.M} | ||||
|             shimmerColors={theme.shimmer} | ||||
|           > | ||||
|             <ParseContent content={content!} size={'M'} numberOfLines={5} /> | ||||
|             <ParseHTML | ||||
|               content={content!} | ||||
|               size={'M'} | ||||
|               numberOfLines={5} | ||||
|               expandHint='介绍' | ||||
|             /> | ||||
|           </ShimmerPlaceholder> | ||||
|         </View> | ||||
|       ) | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import BottomSheet from '@root/components/BottomSheet' | ||||
| import { useSelector } from 'react-redux' | ||||
| import { getLocalAccountId } from '@root/utils/slices/instancesSlice' | ||||
| import HeaderDefaultActionsAccount from '@root/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount' | ||||
| import layoutAnimation from '@root/utils/styles/layoutAnimation' | ||||
|  | ||||
| // Moved account example: https://m.cmx.im/web/accounts/27812 | ||||
|  | ||||
| @@ -113,12 +112,12 @@ const ScreenSharedAccount: React.FC<Props> = ({ | ||||
|         /> | ||||
|       ) : null} | ||||
|       <ScrollView | ||||
|         bounces={false} | ||||
|         scrollEventThrottle={16} | ||||
|         showsVerticalScrollIndicator={false} | ||||
|         onScroll={Animated.event( | ||||
|           [{ nativeEvent: { contentOffset: { y: scrollY } } }], | ||||
|           { useNativeDriver: false } | ||||
|         )} | ||||
|         scrollEventThrottle={8} | ||||
|       > | ||||
|         <AccountHeader | ||||
|           accountState={accountState} | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import client from '@root/api/client' | ||||
| import Button from '@root/components/Button' | ||||
| import haptics from '@root/components/haptics' | ||||
| import { toast } from '@root/components/toast' | ||||
| import { relationshipFetch } from '@root/utils/fetches/relationshipFetch' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| @@ -55,8 +56,14 @@ const AccountInformationActions: React.FC<Props> = ({ account }) => { | ||||
|   }, [account]) | ||||
|   const queryClient = useQueryClient() | ||||
|   const mutation = useMutation(fireMutation, { | ||||
|     onSuccess: data => queryClient.setQueryData(relationshipQueryKey, data), | ||||
|     onError: () => toast({ type: 'error', content: '关注失败,请重试' }) | ||||
|     onSuccess: data => { | ||||
|       haptics('Success') | ||||
|       queryClient.setQueryData(relationshipQueryKey, data) | ||||
|     }, | ||||
|     onError: () => { | ||||
|       haptics('Error') | ||||
|       toast({ type: 'error', content: '关注失败,请重试' }) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   const mainAction = useMemo(() => { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { ParseHTML } from '@components/Parse' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import ParseContent from '@root/components/ParseContent' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { useTheme } from '@root/utils/styles/ThemeManager' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import { StyleSheet, View } from 'react-native' | ||||
|  | ||||
| @@ -20,7 +20,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => { | ||||
|           style={[styles.field, { borderBottomColor: theme.border }]} | ||||
|         > | ||||
|           <View style={[styles.fieldLeft, { borderRightColor: theme.border }]}> | ||||
|             <ParseContent | ||||
|             <ParseHTML | ||||
|               content={field.name} | ||||
|               size={'M'} | ||||
|               emojis={account.emojis} | ||||
| @@ -36,7 +36,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => { | ||||
|             ) : null} | ||||
|           </View> | ||||
|           <View style={styles.fieldRight}> | ||||
|             <ParseContent | ||||
|             <ParseHTML | ||||
|               content={field.value} | ||||
|               size={'M'} | ||||
|               emojis={account.emojis} | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import Emojis from '@root/components/Timelines/Timeline/Shared/Emojis' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { useTheme } from '@root/utils/styles/ThemeManager' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { LinearGradient } from 'expo-linear-gradient' | ||||
| import React, { forwardRef } from 'react' | ||||
| import { StyleSheet, Text, View } from 'react-native' | ||||
| import { StyleSheet } from 'react-native' | ||||
| import ShimmerPlaceholder, { | ||||
|   createShimmerPlaceholder | ||||
| } from 'react-native-shimmer-placeholder' | ||||
| @@ -28,26 +28,14 @@ const AccountInformationName = forwardRef<ShimmerPlaceholder, Props>( | ||||
|         style={styles.name} | ||||
|         shimmerColors={theme.shimmer} | ||||
|       > | ||||
|         <View> | ||||
|           {account?.emojis ? ( | ||||
|             <Emojis | ||||
|               content={account?.display_name || account?.username} | ||||
|               emojis={account.emojis} | ||||
|               size='L' | ||||
|               fontBold={true} | ||||
|             /> | ||||
|           ) : ( | ||||
|             <Text | ||||
|               style={{ | ||||
|                 color: theme.primary, | ||||
|                 ...StyleConstants.FontStyle.L, | ||||
|                 fontWeight: StyleConstants.Font.Weight.Bold | ||||
|               }} | ||||
|             > | ||||
|               {account?.display_name || account?.username} | ||||
|             </Text> | ||||
|           )} | ||||
|         </View> | ||||
|         {account ? ( | ||||
|           <ParseEmojis | ||||
|             content={account.display_name || account.username} | ||||
|             emojis={account.emojis} | ||||
|             size='L' | ||||
|             fontBold | ||||
|           /> | ||||
|         ) : null} | ||||
|       </ShimmerPlaceholder> | ||||
|     ) | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import ParseContent from '@root/components/ParseContent' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { ParseHTML } from '@components/Parse' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import React from 'react' | ||||
| import { StyleSheet, View } from 'react-native' | ||||
|  | ||||
| @@ -10,11 +10,7 @@ export interface Props { | ||||
| const AccountInformationNotes: React.FC<Props> = ({ account }) => { | ||||
|   return ( | ||||
|     <View style={styles.note}> | ||||
|       <ParseContent | ||||
|         content={account.note!} | ||||
|         size={'M'} | ||||
|         emojis={account.emojis} | ||||
|       /> | ||||
|       <ParseHTML content={account.note!} size={'M'} emojis={account.emojis} /> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import Emojis from '@root/components/Timelines/Timeline/Shared/Emojis' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { useTheme } from '@root/utils/styles/ThemeManager' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import { AccountState } from '@screens/Shared/Account' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import { Animated, Dimensions, StyleSheet, Text, View } from 'react-native' | ||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||||
| import { AccountState } from '../Account' | ||||
|  | ||||
| export interface Props { | ||||
|   accountState: AccountState | ||||
| @@ -59,24 +59,15 @@ const AccountNav: React.FC<Props> = ({ accountState, scrollY, account }) => { | ||||
|             } | ||||
|           ]} | ||||
|         > | ||||
|           {account?.emojis ? ( | ||||
|             <Emojis | ||||
|               content={account?.display_name || account?.username} | ||||
|               emojis={account.emojis} | ||||
|               size='M' | ||||
|               fontBold={true} | ||||
|             /> | ||||
|           ) : ( | ||||
|             <Text | ||||
|               style={{ | ||||
|                 color: theme.primary, | ||||
|                 ...StyleConstants.FontStyle.M, | ||||
|                 fontWeight: StyleConstants.Font.Weight.Bold | ||||
|               }} | ||||
|             > | ||||
|               {account?.display_name || account?.username} | ||||
|           {account ? ( | ||||
|             <Text numberOfLines={1}> | ||||
|               <ParseEmojis | ||||
|                 content={account.display_name || account.username} | ||||
|                 emojis={account.emojis} | ||||
|                 fontBold | ||||
|               /> | ||||
|             </Text> | ||||
|           )} | ||||
|           ) : null} | ||||
|         </Animated.View> | ||||
|       </View> | ||||
|     </Animated.View> | ||||
|   | ||||
| @@ -19,6 +19,9 @@ const AccountToots: React.FC<Props> = ({ | ||||
|   accountDispatch, | ||||
|   id | ||||
| }) => { | ||||
|   const headerHeight = useSafeAreaInsets().top + 44 | ||||
|   const footerHeight = useSafeAreaInsets().bottom + useBottomTabBarHeight() | ||||
|  | ||||
|   const routes: { key: App.Pages }[] = [ | ||||
|     { key: 'Account_Default' }, | ||||
|     { key: 'Account_All' }, | ||||
| @@ -37,20 +40,11 @@ const AccountToots: React.FC<Props> = ({ | ||||
|     }, | ||||
|     [] | ||||
|   ) | ||||
|   const headerHeight = useSafeAreaInsets().top + 44 | ||||
|   const footerHeight = useSafeAreaInsets().bottom + useBottomTabBarHeight() | ||||
|  | ||||
|   return ( | ||||
|     <TabView | ||||
|       lazy | ||||
|       swipeEnabled | ||||
|       style={[ | ||||
|         styles.base, | ||||
|         { | ||||
|           height: | ||||
|             Dimensions.get('window').height - headerHeight - footerHeight - 33 | ||||
|         } | ||||
|       ]} | ||||
|       renderScene={renderScene} | ||||
|       renderTabBar={() => null} | ||||
|       initialLayout={{ width: Dimensions.get('window').width }} | ||||
| @@ -58,6 +52,16 @@ const AccountToots: React.FC<Props> = ({ | ||||
|       onIndexChange={index => | ||||
|         accountDispatch({ type: 'segmentedIndex', payload: index }) | ||||
|       } | ||||
|       style={[ | ||||
|         styles.base, | ||||
|         { | ||||
|           height: | ||||
|             Dimensions.get('window').height - | ||||
|             headerHeight - | ||||
|             footerHeight - | ||||
|             (33 + StyleConstants.Spacing.Global.PagePadding * 2) | ||||
|         } | ||||
|       ]} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| @@ -68,4 +72,4 @@ const styles = StyleSheet.create({ | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default AccountToots | ||||
| export default React.memo(AccountToots, () => true) | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import client from '@api/client' | ||||
| import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs' | ||||
| import client from '@root/api/client' | ||||
| import Button from '@root/components/Button' | ||||
| import haptics from '@root/components/haptics' | ||||
| import ParseContent from '@root/components/ParseContent' | ||||
| import { announcementFetch } from '@root/utils/fetches/announcementsFetch' | ||||
| import relativeTime from '@root/utils/relativeTime' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { useTheme } from '@root/utils/styles/ThemeManager' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import { ParseHTML } from '@components/Parse' | ||||
| import relativeTime from '@components/relativeTime' | ||||
| import { announcementFetch } from '@utils/fetches/announcementsFetch' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useState } from 'react' | ||||
| import { | ||||
|   Dimensions, | ||||
| @@ -103,7 +103,7 @@ const ScreenSharedAnnouncements: React.FC = ({ | ||||
|             发布于{relativeTime(item.published_at)} | ||||
|           </Text> | ||||
|           <ScrollView style={styles.scrollView} showsVerticalScrollIndicator> | ||||
|             <ParseContent | ||||
|             <ParseHTML | ||||
|               content={item.content} | ||||
|               size='M' | ||||
|               emojis={item.emojis} | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import haptics from '@root/components/haptics' | ||||
| import { toast } from '@root/components/toast' | ||||
| import { store } from '@root/store' | ||||
| import layoutAnimation from '@root/utils/styles/layoutAnimation' | ||||
| import formatText from '@screens/Shared/Compose/formatText' | ||||
| @@ -27,7 +26,6 @@ import { SafeAreaView } from 'react-native-safe-area-context' | ||||
| import { createNativeStackNavigator } from 'react-native-screens/native-stack' | ||||
| import { useQueryClient } from 'react-query' | ||||
| import ComposeEditAttachment from './Compose/EditAttachment' | ||||
| import ComposeEditAttachmentRoot from './Compose/EditAttachment/Root' | ||||
| import composeInitialState from './Compose/utils/initialState' | ||||
| import composeParseState from './Compose/utils/parseState' | ||||
| import composeSend from './Compose/utils/post' | ||||
| @@ -190,7 +188,6 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => { | ||||
|               haptics('Success') | ||||
|               queryClient.invalidateQueries(['Following']) | ||||
|               navigation.goBack() | ||||
|               toast({ type: 'success', content: '发布成功' }) | ||||
|             }) | ||||
|             .catch(() => { | ||||
|               haptics('Error') | ||||
|   | ||||
| @@ -6,7 +6,13 @@ import addAttachment from '@screens/Shared/Compose/addAttachment' | ||||
| import { ExtendedAttachment } from '@screens/Shared/Compose/utils/types' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useContext, useMemo } from 'react' | ||||
| import React, { | ||||
|   useCallback, | ||||
|   useContext, | ||||
|   useEffect, | ||||
|   useMemo, | ||||
|   useRef | ||||
| } from 'react' | ||||
| import { | ||||
|   FlatList, | ||||
|   Image, | ||||
| @@ -26,6 +32,9 @@ const ComposeAttachments: React.FC = () => { | ||||
|   const { theme } = useTheme() | ||||
|   const navigation = useNavigation() | ||||
|  | ||||
|   const flatListRef = useRef<FlatList>(null) | ||||
|   let prevOffsets = useRef<number[]>() | ||||
|  | ||||
|   const sensitiveOnPress = useCallback( | ||||
|     () => | ||||
|       composeDispatch({ | ||||
| @@ -35,35 +44,75 @@ const ComposeAttachments: React.FC = () => { | ||||
|     [composeState.attachments.sensitive] | ||||
|   ) | ||||
|  | ||||
|   const calculateWidth = useCallback(item => { | ||||
|     if (item.local) { | ||||
|       return (item.local.width / item.local.height) * DEFAULT_HEIGHT | ||||
|     } else { | ||||
|       if (item.remote) { | ||||
|         if (item.remote.meta.original.aspect) { | ||||
|           return item.remote.meta.original.aspect * DEFAULT_HEIGHT | ||||
|         } else if ( | ||||
|           item.remote.meta.original.width && | ||||
|           item.remote.meta.original.height | ||||
|         ) { | ||||
|           return ( | ||||
|             (item.remote.meta.original.width / | ||||
|               item.remote.meta.original.height) * | ||||
|             DEFAULT_HEIGHT | ||||
|           ) | ||||
|         } else { | ||||
|           return DEFAULT_HEIGHT | ||||
|         } | ||||
|       } else { | ||||
|         return DEFAULT_HEIGHT | ||||
|       } | ||||
|     } | ||||
|   }, []) | ||||
|  | ||||
|   const snapToOffsets = useMemo(() => { | ||||
|     const attachmentsOffsets = composeState.attachments.uploads.map( | ||||
|       (_, index) => { | ||||
|         let currentOffset = 0 | ||||
|         Array.from(Array(index).keys()).map( | ||||
|           i => | ||||
|             (currentOffset = | ||||
|               currentOffset + | ||||
|               calculateWidth(composeState.attachments.uploads[i]) + | ||||
|               StyleConstants.Spacing.Global.PagePadding) | ||||
|         ) | ||||
|         return currentOffset | ||||
|       } | ||||
|     ) | ||||
|     return attachmentsOffsets.length < 4 | ||||
|       ? [ | ||||
|           ...attachmentsOffsets, | ||||
|           attachmentsOffsets.reduce((a, b) => a + b, 0) + | ||||
|             DEFAULT_HEIGHT + | ||||
|             StyleConstants.Spacing.Global.PagePadding | ||||
|         ] | ||||
|       : attachmentsOffsets | ||||
|   }, [composeState.attachments.uploads.length]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       snapToOffsets.length > | ||||
|       (prevOffsets.current ? prevOffsets.current?.length : 0) | ||||
|     ) { | ||||
|       flatListRef.current?.scrollToOffset({ | ||||
|         offset: | ||||
|           snapToOffsets[snapToOffsets.length - 2] + | ||||
|           snapToOffsets[snapToOffsets.length - 1] | ||||
|       }) | ||||
|     } | ||||
|     prevOffsets.current = snapToOffsets | ||||
|   }, [snapToOffsets, prevOffsets]) | ||||
|  | ||||
|   const renderAttachment = useCallback( | ||||
|     ({ item, index }: { item: ExtendedAttachment; index: number }) => { | ||||
|       let calculatedWidth: number | ||||
|       if (item.local) { | ||||
|         calculatedWidth = | ||||
|           (item.local.width / item.local.height) * DEFAULT_HEIGHT | ||||
|       } else { | ||||
|         if (item.remote) { | ||||
|           if (item.remote.meta.original.aspect) { | ||||
|             calculatedWidth = item.remote.meta.original.aspect * DEFAULT_HEIGHT | ||||
|           } else if ( | ||||
|             item.remote.meta.original.width && | ||||
|             item.remote.meta.original.height | ||||
|           ) { | ||||
|             calculatedWidth = | ||||
|               (item.remote.meta.original.width / | ||||
|                 item.remote.meta.original.height) * | ||||
|               DEFAULT_HEIGHT | ||||
|           } else { | ||||
|             calculatedWidth = DEFAULT_HEIGHT | ||||
|           } | ||||
|         } else { | ||||
|           calculatedWidth = DEFAULT_HEIGHT | ||||
|         } | ||||
|       } | ||||
|       return ( | ||||
|         <View | ||||
|           key={index} | ||||
|           style={[styles.container, { width: calculatedWidth }]} | ||||
|           style={[styles.container, { width: calculateWidth(item) }]} | ||||
|         > | ||||
|           <Image | ||||
|             style={styles.image} | ||||
| @@ -172,7 +221,6 @@ const ComposeAttachments: React.FC = () => { | ||||
|     ), | ||||
|     [] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <Pressable style={styles.sensitive} onPress={sensitiveOnPress}> | ||||
| @@ -187,14 +235,19 @@ const ComposeAttachments: React.FC = () => { | ||||
|       </Pressable> | ||||
|       <FlatList | ||||
|         horizontal | ||||
|         keyExtractor={item => item.local!.uri || item.remote!.url} | ||||
|         data={composeState.attachments.uploads} | ||||
|         ref={flatListRef} | ||||
|         decelerationRate={0} | ||||
|         pagingEnabled={false} | ||||
|         snapToAlignment='center' | ||||
|         renderItem={renderAttachment} | ||||
|         snapToOffsets={snapToOffsets} | ||||
|         keyboardShouldPersistTaps='handled' | ||||
|         showsHorizontalScrollIndicator={false} | ||||
|         data={composeState.attachments.uploads} | ||||
|         keyExtractor={item => item.local!.uri || item.remote!.url} | ||||
|         ListFooterComponent={ | ||||
|           composeState.attachments.uploads.length < 4 ? listFooter : null | ||||
|         } | ||||
|         showsHorizontalScrollIndicator={false} | ||||
|         keyboardShouldPersistTaps='handled' | ||||
|       /> | ||||
|     </View> | ||||
|   ) | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import Emojis from '@components/Timelines/Timeline/Shared/Emojis' | ||||
| import haptics from '@root/components/haptics' | ||||
| import haptics from '@components/haptics' | ||||
| import { ParseEmojis } from '@components/Parse' | ||||
| import { ComposeContext } from '@screens/Shared/Compose' | ||||
| import ComposeActions from '@screens/Shared/Compose/Actions' | ||||
| import ComposeRootFooter from '@screens/Shared/Compose/Root/Footer' | ||||
| @@ -9,7 +9,6 @@ import { emojisFetch } from '@utils/fetches/emojisFetch' | ||||
| import { searchFetch } from '@utils/fetches/searchFetch' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import * as Permissions from 'expo-permissions' | ||||
| import { forEach, groupBy, sortBy } from 'lodash' | ||||
| import React, { | ||||
|   Dispatch, | ||||
| @@ -66,18 +65,20 @@ const ListItem = React.memo( | ||||
|           <View style={[styles.account, { borderBottomColor: theme.border }]}> | ||||
|             <Image source={{ uri: item.avatar }} style={styles.accountAvatar} /> | ||||
|             <View> | ||||
|               <Text style={[styles.accountName, { color: theme.primary }]}> | ||||
|                 {item.emojis?.length ? ( | ||||
|                   <Emojis | ||||
|                     content={item.display_name || item.username} | ||||
|                     emojis={item.emojis} | ||||
|                     size='S' | ||||
|                   /> | ||||
|                 ) : ( | ||||
|                   item.display_name || item.username | ||||
|                 )} | ||||
|               <Text | ||||
|                 style={[styles.accountName, { color: theme.primary }]} | ||||
|                 numberOfLines={1} | ||||
|               > | ||||
|                 <ParseEmojis | ||||
|                   content={item.display_name || item.username} | ||||
|                   emojis={item.emojis} | ||||
|                   size='S' | ||||
|                 /> | ||||
|               </Text> | ||||
|               <Text style={[styles.accountAccount, { color: theme.primary }]}> | ||||
|               <Text | ||||
|                 style={[styles.accountAccount, { color: theme.primary }]} | ||||
|                 numberOfLines={1} | ||||
|               > | ||||
|                 @{item.acct} | ||||
|               </Text> | ||||
|             </View> | ||||
|   | ||||
| @@ -94,8 +94,12 @@ const formatText = ({ | ||||
|         contentLength = contentLength + 23 | ||||
|         break | ||||
|       case 'accounts': | ||||
|         contentLength = | ||||
|           contentLength + main.split(new RegExp('(@.*)@?'))[1].length | ||||
|         if (main.match(/@/g)!.length > 1) { | ||||
|           contentLength = | ||||
|             contentLength + main.split(new RegExp('(@.*?)@'))[1].length | ||||
|         } else { | ||||
|           contentLength = contentLength + main.length | ||||
|         } | ||||
|         break | ||||
|       case 'hashtags': | ||||
|         contentLength = contentLength + main.length | ||||
|   | ||||
| @@ -16,6 +16,9 @@ const composeParseState = ({ | ||||
|     case 'edit': | ||||
|       return { | ||||
|         ...composeInitialState, | ||||
|         ...(incomingStatus.spoiler_text && { | ||||
|           spoiler: { ...composeInitialState.spoiler, active: true } | ||||
|         }), | ||||
|         ...(incomingStatus.poll && { | ||||
|           poll: { | ||||
|             active: true, | ||||
|   | ||||
| @@ -109,8 +109,8 @@ const ScreenSharedImagesViewer: React.FC<Props> = ({ | ||||
|                   { | ||||
|                     url: imageUrls[currentIndex].url | ||||
|                   }, | ||||
|                   () => haptics('Success'), | ||||
|                   () => haptics('Error') | ||||
|                   () => haptics('Error'), | ||||
|                   () => haptics('Success') | ||||
|                 ) | ||||
|               } | ||||
|             /> | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import { HeaderRight } from '@components/Header' | ||||
| import { ParseEmojis, ParseHTML } from '@components/Parse' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { HeaderRight } from '@root/components/Header' | ||||
| import ParseContent from '@root/components/ParseContent' | ||||
| import Emojis from '@root/components/Timelines/Timeline/Shared/Emojis' | ||||
| import { searchFetch } from '@root/utils/fetches/searchFetch' | ||||
| import { StyleConstants } from '@root/utils/styles/constants' | ||||
| import { useTheme } from '@root/utils/styles/ThemeManager' | ||||
| import { searchFetch } from '@utils/fetches/searchFetch' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { debounce } from 'lodash' | ||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react' | ||||
| import { | ||||
| @@ -173,21 +172,16 @@ const ScreenSharedSearch: React.FC = () => { | ||||
|               style={styles.itemAccountAvatar} | ||||
|             /> | ||||
|             <View> | ||||
|               {item.emojis?.length ? ( | ||||
|                 <Emojis | ||||
|               <Text numberOfLines={1}> | ||||
|                 <ParseEmojis | ||||
|                   content={item.display_name || item.username} | ||||
|                   emojis={item.emojis} | ||||
|                   size='S' | ||||
|                   fontBold={true} | ||||
|                   fontBold | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <Text | ||||
|                   style={[styles.nameWithoutEmoji, { color: theme.primary }]} | ||||
|                 > | ||||
|                   {item.display_name || item.username} | ||||
|                 </Text> | ||||
|               )} | ||||
|               </Text> | ||||
|               <Text | ||||
|                 numberOfLines={1} | ||||
|                 style={[styles.itemAccountAcct, { color: theme.secondary }]} | ||||
|               > | ||||
|                 @{item.acct} | ||||
| @@ -229,28 +223,23 @@ const ScreenSharedSearch: React.FC = () => { | ||||
|               style={styles.itemAccountAvatar} | ||||
|             /> | ||||
|             <View> | ||||
|               {item.account.emojis?.length ? ( | ||||
|                 <Emojis | ||||
|               <Text numberOfLines={1}> | ||||
|                 <ParseEmojis | ||||
|                   content={item.account.display_name || item.account.username} | ||||
|                   emojis={item.account.emojis} | ||||
|                   size='S' | ||||
|                   fontBold={true} | ||||
|                   fontBold | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <Text | ||||
|                   style={[styles.nameWithoutEmoji, { color: theme.primary }]} | ||||
|                 > | ||||
|                   {item.account.display_name || item.account.username} | ||||
|                 </Text> | ||||
|               )} | ||||
|               </Text> | ||||
|               <Text | ||||
|                 numberOfLines={1} | ||||
|                 style={[styles.itemAccountAcct, { color: theme.secondary }]} | ||||
|               > | ||||
|                 @{item.account.acct} | ||||
|               </Text> | ||||
|               {item.content && ( | ||||
|                 <View style={styles.itemStatus}> | ||||
|                   <ParseContent | ||||
|                   <ParseHTML | ||||
|                     content={item.content} | ||||
|                     size='M' | ||||
|                     emojis={item.emojis} | ||||
| @@ -405,10 +394,6 @@ const styles = StyleSheet.create({ | ||||
|     borderRadius: 6, | ||||
|     marginRight: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   nameWithoutEmoji: { | ||||
|     ...StyleConstants.FontStyle.S, | ||||
|     fontWeight: StyleConstants.Font.Weight.Bold | ||||
|   }, | ||||
|   itemAccountAcct: { marginTop: StyleConstants.Spacing.XS }, | ||||
|   itemHashtag: { | ||||
|     ...StyleConstants.FontStyle.M | ||||
|   | ||||
| @@ -15,7 +15,7 @@ const ScreenSharedToot: React.FC<Props> = ({ | ||||
|     params: { toot } | ||||
|   } | ||||
| }) => { | ||||
|   return <Timeline page='Toot' toot={toot} disableRefresh /> | ||||
|   return <Timeline page='Toot' toot={toot} disableRefresh disableInfinity /> | ||||
| } | ||||
|  | ||||
| export default ScreenSharedToot | ||||
|   | ||||
		Reference in New Issue
	
	Block a user