1
0
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:
Zhiyuan Zheng
2020-10-28 00:02:37 +01:00
parent 3f8e451099
commit a6e33d8b0a
17 changed files with 458 additions and 169 deletions

View 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
}

View File

@ -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 />

View File

@ -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
}

View File

@ -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} />
)}
/>
)
}
if (media_attachments) {
fullContent.push(
<Media
key='media'
media_attachments={media_attachments}
sensitive={sensitive}
width={width}
/>
)
}
return fullContent
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}
/>
</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
media_attachments={media_attachments}
sensitive={sensitive}
width={width}
/>
</View>
)}
</>
)
}
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
}

View File

@ -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

View File

@ -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>