1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Transform into TypeScript

This commit is contained in:
Zhiyuan Zheng
2020-10-31 21:04:46 +01:00
parent 698b54868e
commit d2cc643b9c
57 changed files with 935 additions and 646 deletions

View File

@ -1,14 +1,23 @@
import React from 'react'
import PropTypes from 'prop-types'
import propTypesEmoji from 'src/prop-types/emoji'
import propTypesMention from 'src/prop-types/mention'
import { StyleSheet, Text } from 'react-native'
import HTMLView from 'react-native-htmlview'
import HTMLView, { HTMLViewNode } from 'react-native-htmlview'
import { useNavigation } from '@react-navigation/native'
import Emojis from 'src/components/Toot/Emojis'
function renderNode ({ node, index, navigation, mentions, showFullLink }) {
const renderNode = ({
node,
index,
navigation,
mentions,
showFullLink
}: {
node: HTMLViewNode
index: number
navigation: object
mentions?: mastodon.Mention[]
showFullLink: boolean
}) => {
if (node.name == 'a') {
const classes = node.attribs.class
const href = node.attribs.href
@ -69,27 +78,40 @@ function renderNode ({ node, index, navigation, mentions, showFullLink }) {
}
}
export default function ParseContent ({
export interface Props {
content: string
emojis?: mastodon.Emoji[]
emojiSize?: number
mentions?: mastodon.Mention[]
showFullLink?: boolean
linesTruncated?: number
}
const ParseContent: React.FC<Props> = ({
content,
emojis,
emojiSize = 14,
mentions,
showFullLink = false,
linesTruncated = 10
}) {
}) => {
const navigation = useNavigation()
return (
<HTMLView
value={content}
stylesheet={HTMLstyles}
paragraphBreak={null}
paragraphBreak=''
renderNode={(node, index) =>
renderNode({ node, index, navigation, mentions, showFullLink })
}
TextComponent={({ children }) => (
<Emojis content={children} emojis={emojis} dimension={emojiSize} />
)}
TextComponent={({ children }) =>
emojis ? (
<Emojis content={children} emojis={emojis} dimension={emojiSize} />
) : (
<Text>{children}</Text>
)
}
RootComponent={({ children }) => {
return <Text numberOfLines={linesTruncated}>{children}</Text>
}}
@ -109,11 +131,4 @@ const HTMLstyles = StyleSheet.create({
}
})
ParseContent.propTypes = {
content: PropTypes.string.isRequired,
emojis: PropTypes.arrayOf(propTypesEmoji),
emojiSize: PropTypes.number,
mentions: PropTypes.arrayOf(propTypesMention),
showFullLink: PropTypes.bool,
linesTruncated: PropTypes.number
}
export default ParseContent

View File

@ -1,17 +1,22 @@
import React from 'react'
import PropTypes from 'prop-types'
import propTypesEmoji from 'src/prop-types/emoji'
import { StyleSheet, Text, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import Emojis from './Emojis'
export default function Actioned ({
export interface Props {
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog'
name?: string
emojis?: mastodon.Emoji[]
notification?: boolean
}
const Actioned: React.FC<Props> = ({
action,
name,
emojis,
notification = false
}) {
}) => {
let icon
let content
switch (action) {
@ -51,7 +56,11 @@ export default function Actioned ({
{icon}
{content ? (
<View style={styles.content}>
<Emojis content={content} emojis={emojis} dimension={12} />
{emojis ? (
<Emojis content={content} emojis={emojis} dimension={12} />
) : (
<Text>{content}</Text>
)}
</View>
) : (
<></>
@ -74,10 +83,4 @@ const styles = StyleSheet.create({
}
})
Actioned.propTypes = {
action: PropTypes.oneOf(['favourite', 'follow', 'mention', 'poll', 'reblog'])
.isRequired,
name: PropTypes.string,
emojis: PropTypes.arrayOf(propTypesEmoji),
notification: PropTypes.bool
}
export default Actioned

View File

@ -1,18 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import action from 'src/components/action'
export default function Actions ({
export interface Props {
id: string
replies_count: number
reblogs_count: number
reblogged?: boolean
favourites_count: number
favourited?: boolean
}
const Actions: React.FC<Props> = ({
id,
replies_count,
reblogs_count,
reblogged,
favourites_count,
favourited
}) {
}) => {
return (
<View style={styles.actions}>
<Pressable style={styles.action}>
@ -23,7 +31,17 @@ export default function Actions ({
<Feather name='repeat' />
<Text>{reblogs_count}</Text>
</Pressable>
<Pressable style={styles.action} onPress={() => action('favourite', id)}>
<Pressable
style={styles.action}
onPress={() =>
action({
id,
type: 'favourite',
stateKey: 'favourited',
statePrev: favourited || false
})
}
>
<Feather name='heart' />
<Text>{favourites_count}</Text>
</Pressable>
@ -49,11 +67,4 @@ const styles = StyleSheet.create({
}
})
Actions.propTypes = {
id: PropTypes.string.isRequired,
replies_count: PropTypes.number.isRequired,
reblogs_count: PropTypes.number.isRequired,
reblogged: PropTypes.bool.isRequired,
favourites_count: PropTypes.number.isRequired,
favourited: PropTypes.bool.isRequired
}
export default Actions

View File

@ -1,12 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import propTypesAttachment from 'src/prop-types/attachment'
import { Text, View } from 'react-native'
import AttachmentImage from './Attachment/AttachmentImage'
import AttachmentVideo from './Attachment/AttachmentVideo'
export default function Attachment ({ media_attachments, sensitive, width }) {
export interface Props {
media_attachments: mastodon.Attachment[]
sensitive: boolean
width: number
}
const Attachment: React.FC<Props> = ({
media_attachments,
sensitive,
width
}) => {
let attachment
let attachmentHeight
// if (width) {}
@ -74,8 +82,4 @@ export default function Attachment ({ media_attachments, sensitive, width }) {
)
}
Attachment.propTypes = {
media_attachments: PropTypes.arrayOf(propTypesAttachment),
sensitive: PropTypes.bool.isRequired,
width: PropTypes.number.isRequired
}
export default Attachment

View File

@ -1,10 +1,18 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import propTypesAttachment from 'src/prop-types/attachment'
import { Button, Image, Modal, StyleSheet, Pressable, View } from 'react-native'
import ImageViewer from 'react-native-image-zoom-viewer'
export default function AttachmentImage ({ media_attachments, sensitive, width }) {
export interface Props {
media_attachments: mastodon.Attachment[]
sensitive: boolean
width: number
}
const AttachmentImage: React.FC<Props> = ({
media_attachments,
sensitive,
width
}) => {
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
const [imageModalVisible, setImageModalVisible] = useState(false)
const [imageModalIndex, setImageModalIndex] = useState(0)
@ -16,8 +24,8 @@ export default function AttachmentImage ({ media_attachments, sensitive, width }
}
}, [mediaSensitive])
let images = []
media_attachments = media_attachments.map((m, i) => {
let images: { url: string; width: number; height: number }[] = []
const imagesNode = media_attachments.map((m, i) => {
images.push({
url: m.url,
width: m.meta.original.width,
@ -44,7 +52,7 @@ export default function AttachmentImage ({ media_attachments, sensitive, width }
return (
<>
<View style={styles.media}>
{media_attachments}
{imagesNode}
{mediaSensitive && (
<View
style={{
@ -95,8 +103,4 @@ const styles = StyleSheet.create({
}
})
AttachmentImage.propTypes = {
media_attachments: PropTypes.arrayOf(propTypesAttachment),
sensitive: PropTypes.bool.isRequired,
width: PropTypes.number.isRequired
}
export default AttachmentImage

View File

@ -1,15 +1,19 @@
import React, { useRef, useState } from 'react'
import PropTypes from 'prop-types'
import propTypesAttachment from 'src/prop-types/attachment'
import { Pressable, View } from 'react-native'
import { Video } from 'expo-av'
import { Feather } from '@expo/vector-icons'
export default function AttachmentVideo ({
export interface Props {
media_attachments: mastodon.Attachment[]
sensitive: boolean
width: number
}
const AttachmentVideo: React.FC<Props> = ({
media_attachments,
sensitive,
width
}) {
}) => {
const videoPlayer = useRef()
const [mediaSensitive, setMediaSensitive] = useState(sensitive)
const [videoPlay, setVideoPlay] = useState(false)
@ -28,7 +32,7 @@ export default function AttachmentVideo ({
>
<Video
ref={videoPlayer}
source={{ uri: video.remote_url }}
source={{ uri: video.remote_url || video.url }}
style={{
width: videoWidth,
height: videoHeight
@ -65,8 +69,4 @@ export default function AttachmentVideo ({
)
}
AttachmentVideo.propTypes = {
media_attachments: PropTypes.arrayOf(propTypesAttachment),
sensitive: PropTypes.bool.isRequired,
width: PropTypes.number.isRequired
}
export default AttachmentVideo

View File

@ -1,9 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Image, Pressable, StyleSheet } from 'react-native'
import { useNavigation } from '@react-navigation/native'
export default function Avatar ({ uri, id }) {
export interface Props {
uri: string
id: string
}
const Avatar: React.FC<Props> = ({ uri, id }) => {
const navigation = useNavigation()
// Need to fix go back root
return (
@ -32,7 +36,4 @@ const styles = StyleSheet.create({
}
})
Avatar.propTypes = {
uri: PropTypes.string.isRequired,
id: PropTypes.string.isRequired
}
export default Avatar

View File

@ -1,9 +1,12 @@
import React from 'react'
import propTypesCard from 'src/prop-types/card'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
export default function Card ({ card }) {
export interface Props {
card: mastodon.Card
}
const Card: React.FC<Props> = ({ card }) => {
const navigation = useNavigation()
return (
card && (
@ -53,6 +56,4 @@ const styles = StyleSheet.create({
}
})
Card.propTypes = {
card: propTypesCard
}
export default Card

View File

@ -1,11 +1,22 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { Text } from 'react-native'
import Collapsible from 'react-native-collapsible'
import ParseContent from 'src/components/ParseContent'
export default function Content ({ content, emojis, mentions, spoiler_text }) {
export interface Props {
content: string
emojis: mastodon.Emoji[]
mentions: mastodon.Mention[]
spoiler_text?: string
}
const Content: React.FC<Props> = ({
content,
emojis,
mentions,
spoiler_text
}) => {
const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
return (
@ -40,9 +51,4 @@ export default function Content ({ content, emojis, mentions, spoiler_text }) {
)
}
Content.propTypes = {
content: ParseContent.propTypes.content,
emojis: ParseContent.propTypes.emojis,
mentions: ParseContent.propTypes.mentions,
spoiler_text: PropTypes.string
}
export default Content

View File

@ -1,49 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import propTypesEmoji from 'src/prop-types/emoji'
import { Image, Text } from 'react-native'
const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/)
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(regexEmoji)[1]
const emojiIndex = emojis.findIndex(emoji => {
return emojiShortcode === `:${emoji.shortcode}:`
})
return emojiIndex === -1 ? (
<Text key={i} style={{ color: 'red' }}>
Something wrong with emoji!
</Text>
) : (
<Image
key={i}
source={{ uri: emojis[emojiIndex].url }}
style={{ width: dimension, height: dimension }}
/>
)
} else {
return (
<Text
key={i}
style={{ fontSize: dimension, lineHeight: dimension + 1 }}
>
{str}
</Text>
)
}
})
) : (
<Text style={{ fontSize: dimension, lineHeight: dimension + 1 }}>
{content}
</Text>
)
}
Emojis.propTypes = {
content: PropTypes.string.isRequired,
emojis: PropTypes.arrayOf(propTypesEmoji)
}

View File

@ -0,0 +1,52 @@
import React from 'react'
import { Image, Text } from 'react-native'
const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/)
export interface Props {
content: string
emojis: mastodon.Emoji[]
dimension: number
}
const Emojis: React.FC<Props> = ({ content, emojis, dimension }) => {
const hasEmojis = content.match(regexEmoji)
return hasEmojis ? (
<>
{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={{ color: 'red' }}>
Something wrong with emoji!
</Text>
) : (
<Image
key={i}
source={{ uri: emojis[emojiIndex].url }}
style={{ width: dimension, height: dimension }}
/>
)
} else {
return (
<Text
key={i}
style={{ fontSize: dimension, lineHeight: dimension + 1 }}
>
{str}
</Text>
)
}
})}
</>
) : (
<Text style={{ fontSize: dimension, lineHeight: dimension + 1 }}>
{content}
</Text>
)
}
export default Emojis

View File

@ -1,17 +1,26 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import Emojis from './Emojis'
import relativeTime from 'src/utils/relativeTime'
export default function Header ({
export interface Props {
name: string
emojis?: mastodon.Emoji[]
account: string
created_at: string
application?: mastodon.Application
}
const Header: React.FC<Props> = ({
name,
emojis,
account,
created_at,
application
}) {
}) => {
const navigation = useNavigation()
const [since, setSince] = useState(relativeTime(created_at))
// causing full re-render
@ -25,7 +34,11 @@ export default function Header ({
<View>
<View style={styles.names}>
<View style={styles.name}>
<Emojis content={name} emojis={emojis} dimension={14} />
{emojis ? (
<Emojis content={name} emojis={emojis} dimension={14} />
) : (
<Text>{name}</Text>
)}
</View>
<Text style={styles.account} numberOfLines={1}>
@{account}
@ -38,7 +51,11 @@ export default function Header ({
{application && application.name !== 'Web' && (
<View>
<Text
onPress={() => Linking.openURL(application.website)}
onPress={() => {
navigation.navigate('Webview', {
uri: application.website
})
}}
style={styles.application}
>
{application.name}
@ -78,13 +95,4 @@ const styles = StyleSheet.create({
}
})
Header.propTypes = {
name: PropTypes.string.isRequired,
emojis: Emojis.propTypes.emojis,
account: PropTypes.string.isRequired,
created_at: PropTypes.string.isRequired,
application: PropTypes.exact({
name: PropTypes.string.isRequired,
website: PropTypes.string
})
}
export default Header

View File

@ -1,10 +1,13 @@
import React from 'react'
import propTypesPoll from 'src/prop-types/poll'
import { StyleSheet, Text, View } from 'react-native'
import Emojis from './Emojis'
export default function Poll ({ poll }) {
export interface Props {
poll: mastodon.Poll
}
const Poll: React.FC<Props> = ({ poll }) => {
return (
<View>
{poll.options.map((option, index) => (
@ -13,7 +16,11 @@ export default function Poll ({ poll }) {
<Text>
{Math.round((option.votes_count / poll.votes_count) * 100)}%
</Text>
<Text>{option.title}</Text>
<Emojis
content={option.title}
emojis={poll.emojis}
dimension={14}
/>
</View>
<View
style={{
@ -42,6 +49,4 @@ const styles = StyleSheet.create({
}
})
Poll.propTypes = {
poll: propTypesPoll
}
export default Poll

View File

@ -1,5 +1,4 @@
import React, { useMemo } from 'react'
import propTypesNotification from 'src/prop-types/notification'
import { Dimensions, Pressable, StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
@ -12,7 +11,11 @@ import Attachment from './Toot/Attachment'
import Card from './Toot/Card'
import Actions from './Toot/Actions'
export default function TootNotification ({ toot }) {
export interface Props {
toot: mastodon.Notification
}
const TootNotification: React.FC<Props> = ({ toot }) => {
const navigation = useNavigation()
const actualAccount = toot.status ? toot.status.account : toot.account
@ -46,8 +49,8 @@ export default function TootNotification ({ toot }) {
emojis={toot.status.emojis}
mentions={toot.status.mentions}
spoiler_text={toot.status.spoiler_text}
tags={toot.status.tags}
style={{ flex: 1 }}
// tags={toot.status.tags}
// style={{ flex: 1 }}
/>
)}
{toot.status.poll && <Poll poll={toot.status.poll} />}
@ -99,6 +102,4 @@ const styles = StyleSheet.create({
}
})
TootNotification.propTypes = {
toot: propTypesNotification
}
export default TootNotification

View File

@ -1,5 +1,4 @@
import React, { useMemo } from 'react'
import propTypesStatus from 'src/prop-types/status'
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
@ -12,15 +11,14 @@ import Attachment from './Toot/Attachment'
import Card from './Toot/Card'
import Actions from './Toot/Actions'
export default function TootTimeline ({ toot }) {
export interface Props {
toot: mastodon.Status
}
const TootTimeline: React.FC<Props> = ({ toot }) => {
const navigation = useNavigation()
let actualContent
if (toot.reblog) {
actualContent = toot.reblog
} else {
actualContent = toot
}
let actualContent = toot.reblog ? toot.reblog : toot
const tootView = useMemo(() => {
return (
@ -60,8 +58,8 @@ export default function TootTimeline ({ toot }) {
emojis={actualContent.emojis}
mentions={actualContent.mentions}
spoiler_text={actualContent.spoiler_text}
tags={actualContent.tags}
style={{ flex: 1 }}
// tags={actualContent.tags}
// style={{ flex: 1 }}
/>
) : (
<></>
@ -88,7 +86,7 @@ export default function TootTimeline ({ toot }) {
</View>
</View>
)
})
}, [toot])
return tootView
}
@ -109,6 +107,4 @@ const styles = StyleSheet.create({
}
})
TootTimeline.propTypes = {
toot: propTypesStatus
}
export default TootTimeline

View File

@ -1,63 +0,0 @@
import { Alert } from 'react-native'
import { useSelector } from 'react-redux'
import { client } from 'src/api/client'
export default async function action (type, id) {
// If header if needed for remote server
const header = {
headers: {
Authorization: `Bearer ${useSelector(
state => state.instanceInfo.localToken
)}`
}
}
const instance = `https://${useSelector(
state => state.instanceInfo.local
)}/api/v1/`
let endpoint
switch (type) {
case 'favourite':
endpoint = `${instance}statuses/${id}/favourite`
break
case 'unfavourite':
endpoint = `${instance}statuses/${id}/unfavourite`
break
case 'reblog':
endpoint = `${instance}statuses/${id}/reblog`
break
case 'unreblog':
endpoint = `${instance}statuses/${id}/unreblog`
break
case 'bookmark':
endpoint = `${instance}statuses/${id}/bookmark`
break
case 'unbookmark':
endpoint = `${instance}statuses/${id}/unbookmark`
break
case 'mute':
endpoint = `${instance}statuses/${id}/mute`
break
case 'unmute':
endpoint = `${instance}statuses/${id}/unmute`
break
case 'pin':
endpoint = `${instance}statuses/${id}/pin`
break
case 'unpin':
endpoint = `${instance}statuses/${id}/unpin`
break
}
const res = await client.post(endpoint, [], header)
console.log(res)
const alert = {
title: 'This is a title',
message: 'This is a message'
}
Alert.alert(alert.title, alert.message, [
{ text: 'OK', onPress: () => console.log('OK Pressed') }
])
}

41
src/components/action.ts Normal file
View File

@ -0,0 +1,41 @@
import { Alert } from 'react-native'
import client from 'src/api/client'
export interface params {
id: string
}
const action = async ({
id,
type,
stateKey,
statePrev
}: {
id: string
type: 'favourite' | 'reblog' | 'bookmark' | 'mute' | 'pin'
stateKey: 'favourited' | 'reblogged' | 'bookmarked' | 'muted' | 'pinned'
statePrev: boolean
}): Promise<void> => {
const alert = {
title: 'This is a title',
message: 'This is a message'
}
const res = await client({
method: 'post',
instance: 'local',
endpoint: `statuses/${id}/${statePrev ? 'un' : ''}${type}`
})
if (!res.body[stateKey] === statePrev) {
// Update redux
console.log('OK!!!')
} else {
Alert.alert(alert.title, alert.message, [
{ text: 'OK', onPress: () => console.log('OK Pressed') }
])
}
}
export default action