mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Basic account page works
This commit is contained in:
		
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -6570,6 +6570,14 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "react-native-collapsible": { | ||||
|       "version": "1.5.3", | ||||
|       "resolved": "https://registry.npmjs.org/react-native-collapsible/-/react-native-collapsible-1.5.3.tgz", | ||||
|       "integrity": "sha512-Ciuy3yF8wcHeLxXFtx9vxoyrq8l+9EJqLM/ADw54gVwzTQc1/e2ojhQbVFPlxhDA+Pba2qezkaKfv5ifxg8hoQ==", | ||||
|       "requires": { | ||||
|         "prop-types": "^15.6.2" | ||||
|       } | ||||
|     }, | ||||
|     "react-native-gesture-handler": { | ||||
|       "version": "1.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.7.0.tgz", | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
|     "react": "16.13.1", | ||||
|     "react-dom": "16.13.1", | ||||
|     "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz", | ||||
|     "react-native-collapsible": "^1.5.3", | ||||
|     "react-native-gesture-handler": "~1.7.0", | ||||
|     "react-native-htmlview": "^0.16.0", | ||||
|     "react-native-image-zoom-viewer": "^3.0.1", | ||||
|   | ||||
| @@ -14,9 +14,7 @@ export async function client (url, query, { body, ...customConfig } = {}) { | ||||
|   } | ||||
|  | ||||
|   const queryString = query | ||||
|     ? `?${Object.keys(query) | ||||
|         .map(key => `${key}=${query[key]}`) | ||||
|         .join('&')}` | ||||
|     ? `?${query.map(({ key, value }) => `${key}=${value}`).join('&')}` | ||||
|     : '' | ||||
|  | ||||
|   if (body) { | ||||
|   | ||||
							
								
								
									
										119
									
								
								src/components/ParseContent.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/components/ParseContent.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| import React from 'react' | ||||
| import PropTypes from 'prop-types' | ||||
| import { StyleSheet, Text } from 'react-native' | ||||
| import HTMLView from 'react-native-htmlview' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
|  | ||||
| import Emojis from 'src/components/TootTimeline/Emojis' | ||||
|  | ||||
| function renderNode ({ node, index, navigation, mentions, showFullLink }) { | ||||
|   if (node.name == 'a') { | ||||
|     const classes = node.attribs.class | ||||
|     const href = node.attribs.href | ||||
|     if (classes) { | ||||
|       if (classes.includes('hashtag')) { | ||||
|         return ( | ||||
|           <Text | ||||
|             key={index} | ||||
|             style={styles.a} | ||||
|             onPress={() => { | ||||
|               const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) | ||||
|               navigation.navigate('Hashtag', { | ||||
|                 hashtag: tag[1] || tag[2] | ||||
|               }) | ||||
|             }} | ||||
|           > | ||||
|             {node.children[0].data} | ||||
|             {node.children[1]?.children[0].data} | ||||
|           </Text> | ||||
|         ) | ||||
|       } else if (classes.includes('mention')) { | ||||
|         return ( | ||||
|           <Text | ||||
|             key={index} | ||||
|             style={styles.a} | ||||
|             onPress={() => { | ||||
|               const username = href.split(new RegExp(/@(.*)/)) | ||||
|               const usernameIndex = mentions.findIndex( | ||||
|                 m => m.username === username[1] | ||||
|               ) | ||||
|               navigation.navigate('Account', { | ||||
|                 id: mentions[usernameIndex].id | ||||
|               }) | ||||
|             }} | ||||
|           > | ||||
|             {node.children[0].data} | ||||
|             {node.children[1]?.children[0].data} | ||||
|           </Text> | ||||
|         ) | ||||
|       } | ||||
|     } else { | ||||
|       const domain = href.split(new RegExp(/:\/\/(.*?)\//)) | ||||
|       return ( | ||||
|         <Text | ||||
|           key={index} | ||||
|           style={styles.a} | ||||
|           onPress={() => { | ||||
|             navigation.navigate('Webview', { | ||||
|               uri: href, | ||||
|               domain: domain[1] | ||||
|             }) | ||||
|           }} | ||||
|         > | ||||
|           {showFullLink ? href : domain[1]} | ||||
|         </Text> | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default function ParseContent ({ | ||||
|   content, | ||||
|   emojis, | ||||
|   emojiSize = 14, | ||||
|   mentions, | ||||
|   showFullLink = false | ||||
| }) { | ||||
|   const navigation = useNavigation() | ||||
|  | ||||
|   return ( | ||||
|     <HTMLView | ||||
|       value={content} | ||||
|       stylesheet={HTMLstyles} | ||||
|       addLineBreaks={null} | ||||
|       renderNode={(node, index) => | ||||
|         renderNode({ node, index, navigation, mentions, showFullLink }) | ||||
|       } | ||||
|       TextComponent={({ children }) => ( | ||||
|         <Emojis content={children} emojis={emojis} dimension={emojiSize} /> | ||||
|       )} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   a: { | ||||
|     color: 'blue' | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const HTMLstyles = StyleSheet.create({ | ||||
|   p: { | ||||
|     marginBottom: 12 | ||||
|   } | ||||
| }) | ||||
|  | ||||
| ParseContent.propTypes = { | ||||
|   content: PropTypes.string.isRequired, | ||||
|   emojis: Emojis.propTypes.emojis, | ||||
|   emojiSize: PropTypes.number, | ||||
|   mentions: PropTypes.arrayOf( | ||||
|     PropTypes.exact({ | ||||
|       id: PropTypes.string.isRequired, | ||||
|       username: PropTypes.string.isRequired, | ||||
|       url: PropTypes.string.isRequired, | ||||
|       acct: PropTypes.string.isRequired | ||||
|     }) | ||||
|   ), | ||||
|   showFullLink: PropTypes.bool | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| import React, { useState } from 'react' | ||||
| import PropTypes from 'prop-types' | ||||
| import { StyleSheet, View } from 'react-native' | ||||
| import { Dimensions, StyleSheet, View } from 'react-native' | ||||
|  | ||||
| import Reblog from './TootTimeline/Reblog' | ||||
| import Avatar from './TootTimeline/Avatar' | ||||
| @@ -11,8 +11,6 @@ import Actions from './TootTimeline/Actions' | ||||
| // Maybe break away notification types? https://docs.joinmastodon.org/entities/notification/ | ||||
|  | ||||
| export default function TootTimeline ({ item, notification }) { | ||||
|   const [viewWidth, setViewWidth] = useState() | ||||
|  | ||||
|   let contentAggregated = {} | ||||
|   let actualContent | ||||
|   if (notification && item.status) { | ||||
| @@ -41,11 +39,11 @@ export default function TootTimeline ({ item, notification }) { | ||||
|         /> | ||||
|       )} | ||||
|       <View style={styles.toot}> | ||||
|         <Avatar uri={item.reblog?.account.avatar || item.account.avatar} /> | ||||
|         <View | ||||
|           style={styles.details} | ||||
|           onLayout={e => setViewWidth(e.nativeEvent.layout.width)} | ||||
|         > | ||||
|         <Avatar | ||||
|           uri={item.reblog?.account.avatar || item.account.avatar} | ||||
|           id={item.reblog?.account.id || item.account.id} | ||||
|         /> | ||||
|         <View style={styles.details}> | ||||
|           <Header | ||||
|             name={ | ||||
|               (item.reblog?.account.display_name | ||||
| @@ -60,7 +58,11 @@ export default function TootTimeline ({ item, notification }) { | ||||
|             created_at={item.created_at} | ||||
|             application={item.application || null} | ||||
|           /> | ||||
|           <Content {...contentAggregated} width={viewWidth} /> | ||||
|           <Content | ||||
|             {...contentAggregated} | ||||
|             style={{ flex: 1 }} | ||||
|             width={Dimensions.get('window').width - 24 - 50 - 8} | ||||
|           /> | ||||
|         </View> | ||||
|       </View> | ||||
|       <Actions /> | ||||
|   | ||||
| @@ -1,9 +1,22 @@ | ||||
| import React from 'react' | ||||
| import PropTypes from 'prop-types' | ||||
| import { Image, StyleSheet, View } from 'react-native' | ||||
| import { Image, Pressable, StyleSheet } from 'react-native' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
|  | ||||
| export default function Avatar ({ uri }) { | ||||
|   return <Image source={{ uri: uri }} style={styles.avatar} /> | ||||
| export default function Avatar ({ uri, id }) { | ||||
|   const navigation = useNavigation() | ||||
|   return ( | ||||
|     <Pressable | ||||
|       style={styles.avatar} | ||||
|       onPress={() => { | ||||
|         navigation.navigate('Account', { | ||||
|           id: id | ||||
|         }) | ||||
|       }} | ||||
|     > | ||||
|       <Image source={{ uri: uri }} style={styles.image} /> | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
| @@ -11,9 +24,14 @@ const styles = StyleSheet.create({ | ||||
|     width: 50, | ||||
|     height: 50, | ||||
|     marginRight: 8 | ||||
|   }, | ||||
|   image: { | ||||
|     width: '100%', | ||||
|     height: '100%' | ||||
|   } | ||||
| }) | ||||
|  | ||||
| Avatar.propTypes = { | ||||
|   uri: PropTypes.string.isRequired | ||||
|   uri: PropTypes.string.isRequired, | ||||
|   id: PropTypes.string.isRequired | ||||
| } | ||||
|   | ||||
| @@ -6,75 +6,13 @@ import { | ||||
|   Modal, | ||||
|   StyleSheet, | ||||
|   Text, | ||||
|   TouchableHighlight, | ||||
|   Pressable, | ||||
|   View | ||||
| } from 'react-native' | ||||
| import HTMLView from 'react-native-htmlview' | ||||
| import Collapsible from 'react-native-collapsible' | ||||
| import ImageViewer from 'react-native-image-zoom-viewer' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
|  | ||||
| import Emojis from './Emojis' | ||||
|  | ||||
| function renderNode (navigation, node, index, mentions) { | ||||
|   if (node.name == 'a') { | ||||
|     const classes = node.attribs.class | ||||
|     const href = node.attribs.href | ||||
|     if (classes) { | ||||
|       if (classes.includes('hashtag')) { | ||||
|         return ( | ||||
|           <Text | ||||
|             key={index} | ||||
|             style={styles.a} | ||||
|             onPress={() => { | ||||
|               const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) | ||||
|               navigation.navigate('Hashtag', { | ||||
|                 hashtag: tag[1] || tag[2] | ||||
|               }) | ||||
|             }} | ||||
|           > | ||||
|             {node.children[0].data} | ||||
|             {node.children[1]?.children[0].data} | ||||
|           </Text> | ||||
|         ) | ||||
|       } else if (classes.includes('mention')) { | ||||
|         return ( | ||||
|           <Text | ||||
|             key={index} | ||||
|             style={styles.a} | ||||
|             onPress={() => { | ||||
|               const username = href.split(new RegExp(/@(.*)/)) | ||||
|               const usernameIndex = mentions.findIndex( | ||||
|                 m => m.username === username[1] | ||||
|               ) | ||||
|               navigation.navigate('Account', { | ||||
|                 id: mentions[usernameIndex].id | ||||
|               }) | ||||
|             }} | ||||
|           > | ||||
|             {node.children[0].data} | ||||
|             {node.children[1]?.children[0].data} | ||||
|           </Text> | ||||
|         ) | ||||
|       } | ||||
|     } else { | ||||
|       const domain = href.split(new RegExp(/:\/\/(.*?)\//)) | ||||
|       return ( | ||||
|         <Text | ||||
|           key={index} | ||||
|           style={styles.a} | ||||
|           onPress={() => { | ||||
|             navigation.navigate('Webview', { | ||||
|               uri: href, | ||||
|               domain: domain[1] | ||||
|             }) | ||||
|           }} | ||||
|         > | ||||
|           {domain[1]} | ||||
|         </Text> | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| import ParseContent from 'src/components/ParseContent' | ||||
|  | ||||
| function Media ({ media_attachments, sensitive, width }) { | ||||
|   const [mediaSensitive, setMediaSensitive] = useState(sensitive) | ||||
| @@ -90,16 +28,6 @@ function Media ({ media_attachments, sensitive, width }) { | ||||
|  | ||||
|   let images = [] | ||||
|   if (width) { | ||||
|     const calWidth = i => { | ||||
|       if (media_attachments.length === 1) { | ||||
|         return { flexGrow: 1, aspectRatio: 16 / 9 } | ||||
|       } else if (media_attachments.length === 3 && i === 2) { | ||||
|         return { flexGrow: 1, aspectRatio: 16 / 9 } | ||||
|       } else { | ||||
|         return { flexBasis: width / 2 - 4, aspectRatio: 16 / 9 } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     media_attachments = media_attachments.map((m, i) => { | ||||
|       switch (m.type) { | ||||
|         case 'unknown': | ||||
| @@ -111,9 +39,9 @@ function Media ({ media_attachments, sensitive, width }) { | ||||
|             height: m.meta.original.height | ||||
|           }) | ||||
|           return ( | ||||
|             <TouchableHighlight | ||||
|             <Pressable | ||||
|               key={i} | ||||
|               style={calWidth(i)} | ||||
|               style={{ flexGrow: 1, height: width / 5, margin: 4 }} | ||||
|               onPress={() => { | ||||
|                 setImageModalIndex(i) | ||||
|                 setImageModalVisible(true) | ||||
| @@ -122,9 +50,9 @@ function Media ({ media_attachments, sensitive, width }) { | ||||
|               <Image | ||||
|                 source={{ uri: m.preview_url }} | ||||
|                 style={styles.image} | ||||
|                 blurRadius={mediaSensitive ? 50 : 0} | ||||
|                 blurRadius={mediaSensitive ? width / 5 : 0} | ||||
|               /> | ||||
|             </TouchableHighlight> | ||||
|             </Pressable> | ||||
|           ) | ||||
|       } | ||||
|     }) | ||||
| @@ -181,54 +109,79 @@ export default function Content ({ | ||||
|   mentions, | ||||
|   sensitive, | ||||
|   spoiler_text, | ||||
|   tags, | ||||
|   width | ||||
| }) { | ||||
|   const navigation = useNavigation() | ||||
|   const [spoilerCollapsed, setSpoilerCollapsed] = useState(true) | ||||
|  | ||||
|   let fullContent = [] | ||||
|   if (content) { | ||||
|     fullContent.push( | ||||
|       <HTMLView | ||||
|         key='content' | ||||
|         value={content} | ||||
|         renderNode={(node, index) => | ||||
|           renderNode(navigation, node, index, mentions) | ||||
|         } | ||||
|         TextComponent={({ children }) => ( | ||||
|           <Emojis content={children} emojis={emojis} dimension={14} /> | ||||
|         )} | ||||
|   return ( | ||||
|     <> | ||||
|       {content && | ||||
|         (spoiler_text ? ( | ||||
|           <> | ||||
|             <Text> | ||||
|               {spoiler_text}{' '} | ||||
|               <Text onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}> | ||||
|                 点击展开 | ||||
|               </Text> | ||||
|             </Text> | ||||
|             <Collapsible collapsed={spoilerCollapsed}> | ||||
|               <ParseContent | ||||
|                 content={content} | ||||
|                 emojis={emojis} | ||||
|                 emojiSize={14} | ||||
|                 mentions={mentions} | ||||
|               /> | ||||
|     ) | ||||
|   } | ||||
|   if (media_attachments) { | ||||
|     fullContent.push( | ||||
|             </Collapsible> | ||||
|           </> | ||||
|         ) : ( | ||||
|           <ParseContent | ||||
|             content={content} | ||||
|             emojis={emojis} | ||||
|             emojiSize={14} | ||||
|             mentions={mentions} | ||||
|           /> | ||||
|         ))} | ||||
|       {media_attachments.length > 0 && ( | ||||
|         <View | ||||
|           style={{ | ||||
|             width: width + 8, | ||||
|             height: width / 2, | ||||
|             marginTop: 4, | ||||
|             marginLeft: -4 | ||||
|           }} | ||||
|         > | ||||
|           <Media | ||||
|         key='media' | ||||
|             media_attachments={media_attachments} | ||||
|             sensitive={sensitive} | ||||
|             width={width} | ||||
|           /> | ||||
|         </View> | ||||
|       )} | ||||
|     </> | ||||
|   ) | ||||
|   } | ||||
|  | ||||
|   return fullContent | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   media: { | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'space-between' | ||||
|     flex: 1, | ||||
|     flexDirection: 'column', | ||||
|     flexWrap: 'wrap', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'stretch', | ||||
|     alignContent: 'stretch' | ||||
|   }, | ||||
|   image: { | ||||
|     width: '100%', | ||||
|     height: '100%' | ||||
|   }, | ||||
|   a: { | ||||
|     color: 'blue' | ||||
|   } | ||||
| }) | ||||
|  | ||||
| Content.propTypes = { | ||||
|   content: PropTypes.string | ||||
|   content: ParseContent.propTypes.content, | ||||
|   emojis: ParseContent.propTypes.emojis, | ||||
|   // media_attachments | ||||
|   mentions: ParseContent.propTypes.mentions, | ||||
|   sensitive: PropTypes.bool.isRequired, | ||||
|   spoiler_text: PropTypes.string, | ||||
|   width: PropTypes.number.isRequired | ||||
| } | ||||
|   | ||||
| @@ -2,17 +2,16 @@ import React from 'react' | ||||
| import PropTypes from 'prop-types' | ||||
| import { Image, Text } from 'react-native' | ||||
|  | ||||
| const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/g) | ||||
| const regexEmojiSelect = new RegExp(/:([a-z0-9_]+):/) | ||||
| const regexEmoji = new RegExp(/(:.*?:)/g) | ||||
|  | ||||
| export default function Emojis ({ content, emojis, dimension }) { | ||||
|   const hasEmojis = content.match(regexEmoji) | ||||
|   return hasEmojis ? ( | ||||
|     content.split(regexEmoji).map((str, i) => { | ||||
|       if (str.match(regexEmoji)) { | ||||
|         const emojiShortcode = str.split(regexEmojiSelect)[1] | ||||
|         const emojiShortcode = str.split(regexEmoji)[1] | ||||
|         const emojiIndex = emojis.findIndex(emoji => { | ||||
|           return emoji.shortcode === emojiShortcode | ||||
|           return emojiShortcode === `:${emoji.shortcode}:` | ||||
|         }) | ||||
|         return ( | ||||
|           <Image | ||||
|   | ||||
| @@ -14,11 +14,12 @@ export default function Header ({ | ||||
| }) { | ||||
|   const [since, setSince] = useState(relativeTime(created_at)) | ||||
|  | ||||
|   // causing full re-render | ||||
|   useEffect(() => { | ||||
|     setTimeout(() => { | ||||
|       setSince(relativeTime(created_at)) | ||||
|     }, 1000) | ||||
|   }) | ||||
|   }, [since]) | ||||
|  | ||||
|   return ( | ||||
|     <View> | ||||
|   | ||||
| @@ -1,11 +1,89 @@ | ||||
| import React, { useEffect } from 'react' | ||||
| import { SafeAreaView, ScrollView, Text } from 'react-native' | ||||
| import React, { useEffect, useState } from 'react' | ||||
| import { | ||||
|   Dimensions, | ||||
|   Image, | ||||
|   ScrollView, | ||||
|   StyleSheet, | ||||
|   Text, | ||||
|   View | ||||
| } from 'react-native' | ||||
| import HTMLView from 'react-native-htmlview' | ||||
| import { useDispatch, useSelector } from 'react-redux' | ||||
| import { useFocusEffect } from '@react-navigation/native' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
|  | ||||
| import { fetch, getState, reset } from 'src/stacks/common/accountSlice' | ||||
| import * as accountSlice from 'src/stacks/common/accountSlice' | ||||
| import * as relationshipsSlice from 'src/stacks/common/relationshipsSlice' | ||||
|  | ||||
| // Show remote hashtag? Only when private, show local version? | ||||
| import ParseContent from 'src/components/ParseContent' | ||||
|  | ||||
| // Moved account example: https://m.cmx.im/web/accounts/27812 | ||||
|  | ||||
| function Header ({ uri, size }) { | ||||
|   if (uri) { | ||||
|     return ( | ||||
|       <Image | ||||
|         source={{ uri: uri }} | ||||
|         style={styles.header(size ? size.height / size.width : 1 / 2)} | ||||
|       /> | ||||
|     ) | ||||
|   } else { | ||||
|     return <View style={styles.header(1 / 3)} /> | ||||
|   } | ||||
| } | ||||
|  | ||||
| function Information ({ account, emojis }) { | ||||
|   return ( | ||||
|     <View style={styles.information}> | ||||
|       {/* <Text>Moved or not: {account.moved}</Text> */} | ||||
|       <Image source={{ uri: account.avatar }} style={styles.avatar} /> | ||||
|  | ||||
|       <Text style={styles.display_name}> | ||||
|         {account.display_name || account.username} | ||||
|         {account.bot && ( | ||||
|           <Feather name='hard-drive' style={styles.display_name} /> | ||||
|         )} | ||||
|       </Text> | ||||
|       <Text style={styles.account}> | ||||
|         {account.acct} | ||||
|         {account.locked && <Feather name='lock' />} | ||||
|       </Text> | ||||
|  | ||||
|       {account.fields && | ||||
|         account.fields.map((field, index) => ( | ||||
|           <View key={index} style={{ flex: 1, flexDirection: 'row' }}> | ||||
|             <Text style={{ width: '30%', alignSelf: 'center' }}> | ||||
|               <ParseContent content={field.name} emojis={emojis} showFullLink />{' '} | ||||
|               {field.verified_at && <Feather name='check-circle' />} | ||||
|             </Text> | ||||
|             <Text style={{ width: '70%' }}> | ||||
|               <ParseContent | ||||
|                 content={field.value} | ||||
|                 emojis={emojis} | ||||
|                 showFullLink | ||||
|               /> | ||||
|             </Text> | ||||
|           </View> | ||||
|         ))} | ||||
|  | ||||
|       <Text> | ||||
|         <ParseContent content={account.note} emojis={emojis} /> | ||||
|       </Text> | ||||
|       <Text> | ||||
|         加入时间{' '} | ||||
|         {new Date(account.created_at).toLocaleDateString('zh-CN', { | ||||
|           year: 'numeric', | ||||
|           month: 'long', | ||||
|           day: 'numeric' | ||||
|         })} | ||||
|       </Text> | ||||
|  | ||||
|       <Text>Toots: {account.statuses_count}</Text> | ||||
|       <Text>Followers: {account.followers_count}</Text> | ||||
|       <Text>Following: {account.following_count}</Text> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default function Account ({ | ||||
|   route: { | ||||
| @@ -13,29 +91,74 @@ export default function Account ({ | ||||
|   } | ||||
| }) { | ||||
|   const dispatch = useDispatch() | ||||
|   const state = useSelector(getState) | ||||
|   const accountState = useSelector(accountSlice.retrive) | ||||
|   // const stateRelationships = useSelector(relationshipsState) | ||||
|   const [loaded, setLoaded] = useState(false) | ||||
|   const [headerImageSize, setHeaderImageSize] = useState() | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (state.status === 'idle') { | ||||
|       dispatch(fetch({ id })) | ||||
|     if (accountState.status === 'idle') { | ||||
|       dispatch(accountSlice.fetch({ id })) | ||||
|     } | ||||
|   }, [state, dispatch]) | ||||
|  | ||||
|     if (accountState.account.header) { | ||||
|       Image.getSize(accountState.account.header, (width, height) => { | ||||
|         setHeaderImageSize({ width, height }) | ||||
|         setLoaded(true) | ||||
|       }) | ||||
|     } else { | ||||
|       setLoaded(true) | ||||
|     } | ||||
|   }, [accountState, dispatch]) | ||||
|  | ||||
|   useFocusEffect( | ||||
|     React.useCallback(() => { | ||||
|       // Do something when the screen is focused | ||||
|  | ||||
|       return () => { | ||||
|         dispatch(reset()) | ||||
|         dispatch(accountSlice.reset()) | ||||
|       } | ||||
|     }, []) | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <SafeAreaView> | ||||
|   // add emoji support | ||||
|   return loaded ? ( | ||||
|     <ScrollView> | ||||
|         <Text>{state.account.acct}</Text> | ||||
|       <Header | ||||
|         uri={accountState.account.header} | ||||
|         size={ | ||||
|           headerImageSize && { | ||||
|             width: headerImageSize.width, | ||||
|             height: headerImageSize.height | ||||
|           } | ||||
|         } | ||||
|       /> | ||||
|       <Information | ||||
|         account={accountState.account} | ||||
|         emojis={accountState.emojis} | ||||
|       /> | ||||
|     </ScrollView> | ||||
|     </SafeAreaView> | ||||
|   ) : ( | ||||
|     <></> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   header: ratio => ({ | ||||
|     width: '100%', | ||||
|     height: Dimensions.get('window').width * ratio, | ||||
|     backgroundColor: 'gray' | ||||
|   }), | ||||
|   information: { marginTop: -30, paddingLeft: 12, paddingRight: 12 }, | ||||
|   avatar: { | ||||
|     width: 90, | ||||
|     height: 90 | ||||
|   }, | ||||
|   display_name: { | ||||
|     fontSize: 18, | ||||
|     fontWeight: 'bold', | ||||
|     marginTop: 12 | ||||
|   }, | ||||
|   account: { | ||||
|     marginTop: 4 | ||||
|   } | ||||
| }) | ||||
|   | ||||
| @@ -33,7 +33,12 @@ export default function Timeline ({ page, hashtag, list }) { | ||||
|             <TootTimeline key={item.key} item={item} /> | ||||
|           )} | ||||
|           onRefresh={() => | ||||
|             dispatch(fetch({ page, query: { since_id: state.toots[0].id } })) | ||||
|             dispatch( | ||||
|               fetch({ | ||||
|                 page, | ||||
|                 query: [{ key: 'since_id', value: state.toots[0].id }] | ||||
|               }) | ||||
|             ) | ||||
|           } | ||||
|           refreshing={state.status === 'loading'} | ||||
|           onEndReached={() => { | ||||
| @@ -41,9 +46,12 @@ export default function Timeline ({ page, hashtag, list }) { | ||||
|               dispatch( | ||||
|                 fetch({ | ||||
|                   page, | ||||
|                   query: { | ||||
|                     max_id: state.toots[state.toots.length - 1].id | ||||
|                   query: [ | ||||
|                     { | ||||
|                       key: 'max_id', | ||||
|                       value: state.toots[state.toots.length - 1].id | ||||
|                     } | ||||
|                   ] | ||||
|                 }) | ||||
|               ) | ||||
|               setTimelineReady(true) | ||||
|   | ||||
| @@ -74,9 +74,11 @@ export default function TimelinesCombined ({ name, content }) { | ||||
|       <Stack.Screen | ||||
|         name='Account' | ||||
|         component={Account} | ||||
|         options={({ route }) => ({ | ||||
|           title: `${route.params.id}` | ||||
|         })} | ||||
|         options={{ | ||||
|           headerTranslucent: true, | ||||
|           headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' }, | ||||
|           headerCenter: () => {} | ||||
|         }} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Hashtag' | ||||
|   | ||||
| @@ -16,18 +16,13 @@ const accountInitState = { | ||||
|   status: 'idle' | ||||
| } | ||||
|  | ||||
| export const getState = state => state.account | ||||
| export const retrive = state => state.account | ||||
|  | ||||
| export const accountSlice = createSlice({ | ||||
|   name: 'account', | ||||
|   initialState: { | ||||
|     account: {}, | ||||
|     status: 'idle' | ||||
|   }, | ||||
|   initialState: accountInitState, | ||||
|   reducers: { | ||||
|     reset: state => { | ||||
|       state.account = accountInitState | ||||
|     } | ||||
|     reset: () => accountInitState | ||||
|   }, | ||||
|   extraReducers: { | ||||
|     [fetch.pending]: state => { | ||||
|   | ||||
							
								
								
									
										0
									
								
								src/stacks/common/interfaceSlice.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/stacks/common/interfaceSlice.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										60
									
								
								src/stacks/common/relationshipsSlice.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/stacks/common/relationshipsSlice.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' | ||||
|  | ||||
| import { client } from 'src/api/client' | ||||
|  | ||||
| export const fetch = createAsyncThunk( | ||||
|   'relationships/fetch', | ||||
|   async ({ ids }, { getState }) => { | ||||
|     if (!ids.length) console.error('Relationships empty') | ||||
|     const instanceLocal = getState().instanceInfo.local + '/api/v1/' | ||||
|     const query = ids.map(id => ({ | ||||
|       key: 'id[]', | ||||
|       value: id | ||||
|     })) | ||||
|     const header = { | ||||
|       headers: { | ||||
|         Authorization: `Bearer ${getState().instanceInfo.localToken}` | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return await client.get( | ||||
|       `${instanceLocal}accounts/relationships`, | ||||
|       query, | ||||
|       header | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
|  | ||||
| const relationshipsInitState = { | ||||
|   relationships: [], | ||||
|   status: 'idle' | ||||
| } | ||||
|  | ||||
| export const retrive = state => state.relationships | ||||
|  | ||||
| export const relationshipSlice = createSlice({ | ||||
|   name: 'relationships', | ||||
|   initialState: { | ||||
|     relationships: [], | ||||
|     status: 'idle' | ||||
|   }, | ||||
|   reducers: { | ||||
|     reset: () => relationshipsInitState | ||||
|   }, | ||||
|   extraReducers: { | ||||
|     [fetch.pending]: state => { | ||||
|       state.status = 'loading' | ||||
|     }, | ||||
|     [fetch.fulfilled]: (state, action) => { | ||||
|       state.status = 'succeeded' | ||||
|       state.relationships = action.payload | ||||
|     }, | ||||
|     [fetch.rejected]: (state, action) => { | ||||
|       state.status = 'failed' | ||||
|       console.error(action.error.message) | ||||
|     } | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export const { reset } = relationshipSlice.actions | ||||
| export default relationshipSlice.reducer | ||||
| @@ -3,6 +3,7 @@ import { configureStore } from '@reduxjs/toolkit' | ||||
| import instanceInfoSlice from 'src/stacks/common/instanceInfoSlice' | ||||
| import timelineSlice from 'src/stacks/common/timelineSlice' | ||||
| import accountSlice from 'src/stacks/common/accountSlice' | ||||
| import relationshipsSlice from 'src/stacks/common/relationshipsSlice' | ||||
|  | ||||
| // get site information from local storage and pass to reducers | ||||
| const preloadedState = { | ||||
| @@ -16,7 +17,8 @@ const preloadedState = { | ||||
| const reducer = { | ||||
|   instanceInfo: instanceInfoSlice, | ||||
|   timelines: timelineSlice, | ||||
|   account: accountSlice | ||||
|   account: accountSlice, | ||||
|   relationships: relationshipsSlice | ||||
| } | ||||
|  | ||||
| export default configureStore({ | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import { client } from 'src/api/client' | ||||
|  | ||||
| export const fetch = createAsyncThunk( | ||||
|   'timeline/fetch', | ||||
|   async ({ page, query, hashtag, list }, { getState }) => { | ||||
|   async ({ page, query = [], hashtag, list }, { getState }) => { | ||||
|     const instanceLocal = getState().instanceInfo.local + '/api/v1/' | ||||
|     const instanceRemote = getState().instanceInfo.remote + '/api/v1/' | ||||
|     const header = { | ||||
| @@ -26,7 +26,7 @@ export const fetch = createAsyncThunk( | ||||
|       case 'Following': | ||||
|         return await client.get(`${instanceLocal}timelines/home`, query, header) | ||||
|       case 'Local': | ||||
|         query ? (query.local = true) : (query = { local: 'true' }) | ||||
|         query.push({ key: 'local', value: 'true' }) | ||||
|         return await client.get( | ||||
|           `${instanceLocal}timelines/public`, | ||||
|           query, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user