mirror of
https://github.com/tooot-app/app
synced 2025-04-27 08:28:55 +02:00
Basic account page works
This commit is contained in:
parent
3f8e451099
commit
a6e33d8b0a
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": {
|
"react-native-gesture-handler": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.7.0.tgz",
|
"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": "16.13.1",
|
||||||
"react-dom": "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": "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-gesture-handler": "~1.7.0",
|
||||||
"react-native-htmlview": "^0.16.0",
|
"react-native-htmlview": "^0.16.0",
|
||||||
"react-native-image-zoom-viewer": "^3.0.1",
|
"react-native-image-zoom-viewer": "^3.0.1",
|
||||||
|
@ -14,9 +14,7 @@ export async function client (url, query, { body, ...customConfig } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryString = query
|
const queryString = query
|
||||||
? `?${Object.keys(query)
|
? `?${query.map(({ key, value }) => `${key}=${value}`).join('&')}`
|
||||||
.map(key => `${key}=${query[key]}`)
|
|
||||||
.join('&')}`
|
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
if (body) {
|
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 React, { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||||
|
|
||||||
import Reblog from './TootTimeline/Reblog'
|
import Reblog from './TootTimeline/Reblog'
|
||||||
import Avatar from './TootTimeline/Avatar'
|
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/
|
// Maybe break away notification types? https://docs.joinmastodon.org/entities/notification/
|
||||||
|
|
||||||
export default function TootTimeline ({ item, notification }) {
|
export default function TootTimeline ({ item, notification }) {
|
||||||
const [viewWidth, setViewWidth] = useState()
|
|
||||||
|
|
||||||
let contentAggregated = {}
|
let contentAggregated = {}
|
||||||
let actualContent
|
let actualContent
|
||||||
if (notification && item.status) {
|
if (notification && item.status) {
|
||||||
@ -41,11 +39,11 @@ export default function TootTimeline ({ item, notification }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={styles.toot}>
|
<View style={styles.toot}>
|
||||||
<Avatar uri={item.reblog?.account.avatar || item.account.avatar} />
|
<Avatar
|
||||||
<View
|
uri={item.reblog?.account.avatar || item.account.avatar}
|
||||||
style={styles.details}
|
id={item.reblog?.account.id || item.account.id}
|
||||||
onLayout={e => setViewWidth(e.nativeEvent.layout.width)}
|
/>
|
||||||
>
|
<View style={styles.details}>
|
||||||
<Header
|
<Header
|
||||||
name={
|
name={
|
||||||
(item.reblog?.account.display_name
|
(item.reblog?.account.display_name
|
||||||
@ -60,7 +58,11 @@ export default function TootTimeline ({ item, notification }) {
|
|||||||
created_at={item.created_at}
|
created_at={item.created_at}
|
||||||
application={item.application || null}
|
application={item.application || null}
|
||||||
/>
|
/>
|
||||||
<Content {...contentAggregated} width={viewWidth} />
|
<Content
|
||||||
|
{...contentAggregated}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
width={Dimensions.get('window').width - 24 - 50 - 8}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Actions />
|
<Actions />
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
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 }) {
|
export default function Avatar ({ uri, id }) {
|
||||||
return <Image source={{ uri: uri }} style={styles.avatar} />
|
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({
|
const styles = StyleSheet.create({
|
||||||
@ -11,9 +24,14 @@ const styles = StyleSheet.create({
|
|||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
marginRight: 8
|
marginRight: 8
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Avatar.propTypes = {
|
Avatar.propTypes = {
|
||||||
uri: PropTypes.string.isRequired
|
uri: PropTypes.string.isRequired,
|
||||||
|
id: PropTypes.string.isRequired
|
||||||
}
|
}
|
||||||
|
@ -6,75 +6,13 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableHighlight,
|
Pressable,
|
||||||
View
|
View
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import HTMLView from 'react-native-htmlview'
|
import Collapsible from 'react-native-collapsible'
|
||||||
import ImageViewer from 'react-native-image-zoom-viewer'
|
import ImageViewer from 'react-native-image-zoom-viewer'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
|
|
||||||
import Emojis from './Emojis'
|
import ParseContent from 'src/components/ParseContent'
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Media ({ media_attachments, sensitive, width }) {
|
function Media ({ media_attachments, sensitive, width }) {
|
||||||
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
|
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
|
||||||
@ -90,16 +28,6 @@ function Media ({ media_attachments, sensitive, width }) {
|
|||||||
|
|
||||||
let images = []
|
let images = []
|
||||||
if (width) {
|
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) => {
|
media_attachments = media_attachments.map((m, i) => {
|
||||||
switch (m.type) {
|
switch (m.type) {
|
||||||
case 'unknown':
|
case 'unknown':
|
||||||
@ -111,9 +39,9 @@ function Media ({ media_attachments, sensitive, width }) {
|
|||||||
height: m.meta.original.height
|
height: m.meta.original.height
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<TouchableHighlight
|
<Pressable
|
||||||
key={i}
|
key={i}
|
||||||
style={calWidth(i)}
|
style={{ flexGrow: 1, height: width / 5, margin: 4 }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setImageModalIndex(i)
|
setImageModalIndex(i)
|
||||||
setImageModalVisible(true)
|
setImageModalVisible(true)
|
||||||
@ -122,9 +50,9 @@ function Media ({ media_attachments, sensitive, width }) {
|
|||||||
<Image
|
<Image
|
||||||
source={{ uri: m.preview_url }}
|
source={{ uri: m.preview_url }}
|
||||||
style={styles.image}
|
style={styles.image}
|
||||||
blurRadius={mediaSensitive ? 50 : 0}
|
blurRadius={mediaSensitive ? width / 5 : 0}
|
||||||
/>
|
/>
|
||||||
</TouchableHighlight>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -181,54 +109,79 @@ export default function Content ({
|
|||||||
mentions,
|
mentions,
|
||||||
sensitive,
|
sensitive,
|
||||||
spoiler_text,
|
spoiler_text,
|
||||||
tags,
|
|
||||||
width
|
width
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation()
|
const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
|
||||||
|
|
||||||
let fullContent = []
|
return (
|
||||||
if (content) {
|
<>
|
||||||
fullContent.push(
|
{content &&
|
||||||
<HTMLView
|
(spoiler_text ? (
|
||||||
key='content'
|
<>
|
||||||
value={content}
|
<Text>
|
||||||
renderNode={(node, index) =>
|
{spoiler_text}{' '}
|
||||||
renderNode(navigation, node, index, mentions)
|
<Text onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}>
|
||||||
}
|
点击展开
|
||||||
TextComponent={({ children }) => (
|
</Text>
|
||||||
<Emojis content={children} emojis={emojis} dimension={14} />
|
</Text>
|
||||||
)}
|
<Collapsible collapsed={spoilerCollapsed}>
|
||||||
/>
|
<ParseContent
|
||||||
)
|
content={content}
|
||||||
}
|
emojis={emojis}
|
||||||
if (media_attachments) {
|
emojiSize={14}
|
||||||
fullContent.push(
|
mentions={mentions}
|
||||||
<Media
|
/>
|
||||||
key='media'
|
</Collapsible>
|
||||||
media_attachments={media_attachments}
|
</>
|
||||||
sensitive={sensitive}
|
) : (
|
||||||
width={width}
|
<ParseContent
|
||||||
/>
|
content={content}
|
||||||
)
|
emojis={emojis}
|
||||||
}
|
emojiSize={14}
|
||||||
|
mentions={mentions}
|
||||||
return fullContent
|
/>
|
||||||
|
))}
|
||||||
|
{media_attachments.length > 0 && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: width + 8,
|
||||||
|
height: width / 2,
|
||||||
|
marginTop: 4,
|
||||||
|
marginLeft: -4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Media
|
||||||
|
media_attachments={media_attachments}
|
||||||
|
sensitive={sensitive}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
media: {
|
media: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
justifyContent: 'space-between'
|
flexDirection: 'column',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
alignContent: 'stretch'
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%'
|
height: '100%'
|
||||||
},
|
|
||||||
a: {
|
|
||||||
color: 'blue'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Content.propTypes = {
|
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 PropTypes from 'prop-types'
|
||||||
import { Image, Text } from 'react-native'
|
import { Image, Text } from 'react-native'
|
||||||
|
|
||||||
const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/g)
|
const regexEmoji = new RegExp(/(:.*?:)/g)
|
||||||
const regexEmojiSelect = new RegExp(/:([a-z0-9_]+):/)
|
|
||||||
|
|
||||||
export default function Emojis ({ content, emojis, dimension }) {
|
export default function Emojis ({ content, emojis, dimension }) {
|
||||||
const hasEmojis = content.match(regexEmoji)
|
const hasEmojis = content.match(regexEmoji)
|
||||||
return hasEmojis ? (
|
return hasEmojis ? (
|
||||||
content.split(regexEmoji).map((str, i) => {
|
content.split(regexEmoji).map((str, i) => {
|
||||||
if (str.match(regexEmoji)) {
|
if (str.match(regexEmoji)) {
|
||||||
const emojiShortcode = str.split(regexEmojiSelect)[1]
|
const emojiShortcode = str.split(regexEmoji)[1]
|
||||||
const emojiIndex = emojis.findIndex(emoji => {
|
const emojiIndex = emojis.findIndex(emoji => {
|
||||||
return emoji.shortcode === emojiShortcode
|
return emojiShortcode === `:${emoji.shortcode}:`
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
|
@ -14,11 +14,12 @@ export default function Header ({
|
|||||||
}) {
|
}) {
|
||||||
const [since, setSince] = useState(relativeTime(created_at))
|
const [since, setSince] = useState(relativeTime(created_at))
|
||||||
|
|
||||||
|
// causing full re-render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSince(relativeTime(created_at))
|
setSince(relativeTime(created_at))
|
||||||
}, 1000)
|
}, 1000)
|
||||||
})
|
}, [since])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
@ -1,11 +1,89 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { SafeAreaView, ScrollView, Text } from 'react-native'
|
import {
|
||||||
|
Dimensions,
|
||||||
|
Image,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
|
import HTMLView from 'react-native-htmlview'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { useFocusEffect } from '@react-navigation/native'
|
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 ({
|
export default function Account ({
|
||||||
route: {
|
route: {
|
||||||
@ -13,29 +91,74 @@ export default function Account ({
|
|||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useDispatch()
|
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(() => {
|
useEffect(() => {
|
||||||
if (state.status === 'idle') {
|
if (accountState.status === 'idle') {
|
||||||
dispatch(fetch({ id }))
|
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(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
// Do something when the screen is focused
|
// Do something when the screen is focused
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dispatch(reset())
|
dispatch(accountSlice.reset())
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
)
|
)
|
||||||
|
// add emoji support
|
||||||
return (
|
return loaded ? (
|
||||||
<SafeAreaView>
|
<ScrollView>
|
||||||
<ScrollView>
|
<Header
|
||||||
<Text>{state.account.acct}</Text>
|
uri={accountState.account.header}
|
||||||
</ScrollView>
|
size={
|
||||||
</SafeAreaView>
|
headerImageSize && {
|
||||||
|
width: headerImageSize.width,
|
||||||
|
height: headerImageSize.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Information
|
||||||
|
account={accountState.account}
|
||||||
|
emojis={accountState.emojis}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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} />
|
<TootTimeline key={item.key} item={item} />
|
||||||
)}
|
)}
|
||||||
onRefresh={() =>
|
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'}
|
refreshing={state.status === 'loading'}
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
@ -41,9 +46,12 @@ export default function Timeline ({ page, hashtag, list }) {
|
|||||||
dispatch(
|
dispatch(
|
||||||
fetch({
|
fetch({
|
||||||
page,
|
page,
|
||||||
query: {
|
query: [
|
||||||
max_id: state.toots[state.toots.length - 1].id
|
{
|
||||||
}
|
key: 'max_id',
|
||||||
|
value: state.toots[state.toots.length - 1].id
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
setTimelineReady(true)
|
setTimelineReady(true)
|
||||||
|
@ -74,9 +74,11 @@ export default function TimelinesCombined ({ name, content }) {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Account'
|
name='Account'
|
||||||
component={Account}
|
component={Account}
|
||||||
options={({ route }) => ({
|
options={{
|
||||||
title: `${route.params.id}`
|
headerTranslucent: true,
|
||||||
})}
|
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
|
||||||
|
headerCenter: () => {}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Hashtag'
|
name='Hashtag'
|
||||||
|
@ -16,18 +16,13 @@ const accountInitState = {
|
|||||||
status: 'idle'
|
status: 'idle'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getState = state => state.account
|
export const retrive = state => state.account
|
||||||
|
|
||||||
export const accountSlice = createSlice({
|
export const accountSlice = createSlice({
|
||||||
name: 'account',
|
name: 'account',
|
||||||
initialState: {
|
initialState: accountInitState,
|
||||||
account: {},
|
|
||||||
status: 'idle'
|
|
||||||
},
|
|
||||||
reducers: {
|
reducers: {
|
||||||
reset: state => {
|
reset: () => accountInitState
|
||||||
state.account = accountInitState
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
extraReducers: {
|
extraReducers: {
|
||||||
[fetch.pending]: state => {
|
[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 instanceInfoSlice from 'src/stacks/common/instanceInfoSlice'
|
||||||
import timelineSlice from 'src/stacks/common/timelineSlice'
|
import timelineSlice from 'src/stacks/common/timelineSlice'
|
||||||
import accountSlice from 'src/stacks/common/accountSlice'
|
import accountSlice from 'src/stacks/common/accountSlice'
|
||||||
|
import relationshipsSlice from 'src/stacks/common/relationshipsSlice'
|
||||||
|
|
||||||
// get site information from local storage and pass to reducers
|
// get site information from local storage and pass to reducers
|
||||||
const preloadedState = {
|
const preloadedState = {
|
||||||
@ -16,7 +17,8 @@ const preloadedState = {
|
|||||||
const reducer = {
|
const reducer = {
|
||||||
instanceInfo: instanceInfoSlice,
|
instanceInfo: instanceInfoSlice,
|
||||||
timelines: timelineSlice,
|
timelines: timelineSlice,
|
||||||
account: accountSlice
|
account: accountSlice,
|
||||||
|
relationships: relationshipsSlice
|
||||||
}
|
}
|
||||||
|
|
||||||
export default configureStore({
|
export default configureStore({
|
||||||
|
@ -13,7 +13,7 @@ import { client } from 'src/api/client'
|
|||||||
|
|
||||||
export const fetch = createAsyncThunk(
|
export const fetch = createAsyncThunk(
|
||||||
'timeline/fetch',
|
'timeline/fetch',
|
||||||
async ({ page, query, hashtag, list }, { getState }) => {
|
async ({ page, query = [], hashtag, list }, { getState }) => {
|
||||||
const instanceLocal = getState().instanceInfo.local + '/api/v1/'
|
const instanceLocal = getState().instanceInfo.local + '/api/v1/'
|
||||||
const instanceRemote = getState().instanceInfo.remote + '/api/v1/'
|
const instanceRemote = getState().instanceInfo.remote + '/api/v1/'
|
||||||
const header = {
|
const header = {
|
||||||
@ -26,7 +26,7 @@ export const fetch = createAsyncThunk(
|
|||||||
case 'Following':
|
case 'Following':
|
||||||
return await client.get(`${instanceLocal}timelines/home`, query, header)
|
return await client.get(`${instanceLocal}timelines/home`, query, header)
|
||||||
case 'Local':
|
case 'Local':
|
||||||
query ? (query.local = true) : (query = { local: 'true' })
|
query.push({ key: 'local', value: 'true' })
|
||||||
return await client.get(
|
return await client.get(
|
||||||
`${instanceLocal}timelines/public`,
|
`${instanceLocal}timelines/public`,
|
||||||
query,
|
query,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user