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:
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} />
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user