Transform into TypeScript

This commit is contained in:
Zhiyuan Zheng 2020-10-31 21:04:46 +01:00
parent 698b54868e
commit d2cc643b9c
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
57 changed files with 935 additions and 646 deletions

View File

@ -1,6 +0,0 @@
import React from 'react'
import Index from './src/Index'
const App = () => <Index />
export default App

6
App.tsx Normal file
View File

@ -0,0 +1,6 @@
import React from 'react'
import { Index } from 'src/Index'
const App: React.FC = () => <Index />
export default App

View File

@ -4,6 +4,7 @@ module.exports = function (api) {
presets: ['babel-preset-expo'],
plugins: [
['@babel/plugin-proposal-optional-chaining'],
['babel-plugin-typescript-to-proptypes'],
[
'module-resolver',
{

121
package-lock.json generated
View File

@ -1762,6 +1762,27 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz",
"integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ=="
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"requires": {
"react-is": "^16.7.0"
}
}
}
},
"@types/istanbul-lib-coverage": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@ -1784,6 +1805,82 @@
"@types/istanbul-lib-report": "*"
}
},
"@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
"dev": true
},
"@types/react": {
"version": "16.9.55",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.55.tgz",
"integrity": "sha512-6KLe6lkILeRwyyy7yG9rULKJ0sXplUsl98MGoCfpteXf9sPWFWWMknDcsvubcpaTdBuxtsLF6HDUwdApZL/xIg==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"@types/react-dom": {
"version": "16.9.9",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.9.tgz",
"integrity": "sha512-jE16FNWO3Logq/Lf+yvEAjKzhpST/Eac8EMd1i4dgZdMczfgqC8EjpxwNgEe3SExHYLliabXDh9DEhhqnlXJhg==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-native": {
"version": "0.63.30",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.30.tgz",
"integrity": "sha512-8/PrOjuUaPTCfMeW12ubseZPUGdbRhxYDa/aT+0D0KWVTe60b4H/gJrcfJmBXC6EcCFcimuTzQCv8/S03slYqA==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-native-htmlview": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/react-native-htmlview/-/react-native-htmlview-0.12.2.tgz",
"integrity": "sha512-r5lWdZcZmcxLrfhIAAzBCEpDUuDFRiB5V9d0QvCqhTRh9vorlEjXgyZ5K8/HzbIOuvGb9/mQJPK0rItEAQk0dw==",
"dev": true,
"requires": {
"@types/react": "*",
"@types/react-native": "*"
}
},
"@types/react-navigation": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/react-navigation/-/react-navigation-3.4.0.tgz",
"integrity": "sha512-Y7F5zU8BTBK8tEOvUqgvwvPZ7+9vnc2UI1vHwJ/9ZJG98TntNv04GWa6lrn4MA4149pqw6cyNw/V49Yd2osAFQ==",
"dev": true,
"requires": {
"react-navigation": "*"
}
},
"@types/react-redux": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz",
"integrity": "sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"requires": {
"react-is": "^16.7.0"
}
}
}
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@ -2111,6 +2208,18 @@
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz",
"integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ=="
},
"babel-plugin-typescript-to-proptypes": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/babel-plugin-typescript-to-proptypes/-/babel-plugin-typescript-to-proptypes-1.4.1.tgz",
"integrity": "sha512-CxTjlgiB/qjcC8lMTnmgt/6FsviyDbK/m08ImhnY+M45ZFnDL37zU68n5Kaxh1YWmjNex5R3HMzz0cPwthzHgQ==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.10.4",
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-typescript": "^7.10.4",
"@babel/types": "^7.11.5"
}
},
"babel-preset-expo": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-8.3.0.tgz",
@ -2767,6 +2876,12 @@
"isobject": "^3.0.1"
}
},
"csstype": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz",
"integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA==",
"dev": true
},
"dayjs": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.4.tgz",
@ -7743,6 +7858,12 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz",
"integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==",
"dev": true
},
"ua-parser-js": {
"version": "0.7.22",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz",

View File

@ -24,7 +24,6 @@
"expo-splash-screen": "~0.6.1",
"expo-status-bar": "~1.0.2",
"ky": "^0.24.0",
"prop-types": "^15.7.2",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz",
@ -44,7 +43,15 @@
"devDependencies": {
"@babel/core": "~7.9.0",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"babel-plugin-module-resolver": "^4.0.0"
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@types/react-native": "^0.63.30",
"@types/react-native-htmlview": "^0.12.2",
"@types/react-navigation": "^3.4.0",
"@types/react-redux": "^7.1.9",
"babel-plugin-module-resolver": "^4.0.0",
"babel-plugin-typescript-to-proptypes": "^1.4.1",
"typescript": "^4.0.5"
},
"private": true
}

244
src/@types/mastodon.d.ts vendored Normal file
View File

@ -0,0 +1,244 @@
declare namespace mastodon {
type Account = {
// Base
id: string
username: string
acct: string
url: string
// Attributes
display_name: string
note?: string
avatar: string
avatar_static: string
header: string
header_static: string
locked: boolean
emojis?: Emoji[]
discoverable: boolean
// Statistics
created_at: string
last_status_at: string
statuses_count: number
followers_count: number
following_count: number
// Others
moved?: Status
fields: Field[]
bot: boolean
source: Source
}
type Application = {
// Base
name: string
website?: string
vapid_key?: string
}
type Attachment = {
// Base
id: string
type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio'
url: string
preview_url?: string
// Others
remote_url?: string
text_url?: string
meta: {
original: { width: number; height: number; size: string; aspect: number }
small: { width: number; height: number; size: string; aspect: number }
focus:
| { x: number; y: number }
| {
length: string
duration: number
fps: number
size: string
width: number
height: number
aspect: number
audio_encode: string
audio_bitrate: string
audio_channels: string
original: {
width: number
height: number
frame_rate: string
duration: number
bitrate: number
}
small: {
width: number
height: number
size: string
aspect: number
}
}
| {
length: string
duration: number
fps: number
size: string
width: number
height: number
aspect: number
original: {
width: number
height: number
frame_rate: string
duration: number
bitrate: number
}
small: {
width: number
height: number
size: string
aspect: number
}
}
| {
length: string
duration: number
audio_encode: string
audio_bitrate: string
audio_channels: string
original: {
duration: number
bitrate: number
}
}
}
description?: string
blurhash?: string
}
type Card = {
// Base
url: string
title: string
description: string
type: 'link' | 'photo' | 'video' | 'rich'
// Attributes
author_name: string
author_url: string
provider_name: string
provider_url: string
html: string
width: number
height: number
image: string
embed_url: string
blurhash: string
}
type Emoji = {
// Base
shortcode: string
url: string
static_url: string
visible_in_picker: boolean
category?: string
}
type Field = {
name: string
value: string
verified_at?: string
}
type Mention = {
// Base
id: string
username: string
acct: string
url: string
}
type Notification = {
// Base
id: string
type: 'follow' | 'mention' | 'reblog' | 'favourite' | 'poll'
created_at: string
account: Account
// Others
status?: Status
}
type Poll = {
// Base
id: string
expires_at: string
expired: boolean
multiple: bool
votes_count: number
voters_count: number
voted?: boolean
own_votes?: number[]
options: { title: string; votes_count: number }[]
emojis: Emoji[]
}
type Status = {
// Base
id: string
urk: string
created_at: string
account: Account
content: string
visibility: 'public' | 'unlisted' | 'private' | 'direct'
sensitive: boolean
spoiler_text?: string
media_attachments: Attachment[]
application: Application
// Attributes
mentions: Mention[]
tags: Tag[]
emojis: Emoji[]
// Interaction
reblogs_count: number
favourites_count: number
replies_count: number
favourited: boolean
reblogged: boolean
muted: boolean
bookmarked: boolean
pinned: boolean
// Others
url?: string
in_reply_to_id?: string
in_reply_to_account_id?: string
reblog: Status
poll: Poll
card: Card
language?: string
text?: string
}
type Source = {
// Base
note: string
fields: Field[]
// Others
privacy?: 'public' | 'unlisted' | 'private' | 'direct'
sensitive?: boolean
language?: string
follow_requests_count?: number
}
type Tag = {
// Base
name: string
url: string
// history: types
}
}

47
src/@types/store.d.ts vendored Normal file
View File

@ -0,0 +1,47 @@
declare namespace store {
type AsyncStatus = 'idle' | 'loading' | 'succeeded' | 'failed'
type InstanceInfoState = {
local: string
localToken: string
remote: string
}
type TimelinePage =
| 'Following'
| 'Local'
| 'LocalPublic'
| 'RemotePublic'
| 'Notifications'
| 'Hashtag'
| 'List'
| 'Toot'
| 'Account_Default'
| 'Account_All'
| 'Account_Media'
type TimelineState = {
toots: mastodon.Status[] | []
pointer?: string
status: AsyncStatus
}
type TimelinesState = {
Following: TimelineState
Local: TimelineState
LocalPublic: TimelineState
RemotePublic: TimelineState
Notifications: TimelineState
Hashtag: TimelineState
List: TimelineState
Toot: TimelineState
Account_Default: TimelineState
Account_All: TimelineState
Account_Media: TimelineState
}
type AccountState = {
account: mastodon.Account | {}
status: AsyncStatus
}
}

View File

@ -18,7 +18,7 @@ import Me from 'src/stacks/Me'
enableScreens()
const Tab = createBottomTabNavigator()
export default function Index () {
export const Index: React.FC = () => {
return (
<Provider store={store}>
<StatusBar style='auto' />
@ -26,7 +26,7 @@ export default function Index () {
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let name
let name: string
switch (route.name) {
case 'Local':
name = 'home'
@ -43,6 +43,9 @@ export default function Index () {
case 'Me':
name = focused ? 'smile' : 'meh'
break
default:
name = 'alert-octagon'
break
}
return <Feather name={name} size={size} color={color} />
}

View File

@ -1,61 +0,0 @@
import store from 'src/stacks/common/store'
export async function client (instance, query, { body, ...customConfig } = {}) {
const state = store.getState().instanceInfo
let url
let authHeader
switch (instance.type) {
case 'local':
url = `https://${state.local}/${instance.endpoint}`
authHeader = {
Authorization: `Bearer ${state.localToken}`
}
break
case 'remote':
url = `https://${state.remote}/${instance.endpoint}`
authHeader = {}
break
default:
return Promise.reject('Instance type is not defined.')
}
const headers = { 'Content-Type': 'application/json', ...authHeader }
const config = {
method: body ? 'POST' : 'GET',
...customConfig,
headers: {
...headers,
...customConfig.headers
}
}
const queryString = query
? `?${query.map(({ key, value }) => `${key}=${value}`).join('&')}`
: ''
if (body) {
config.body = JSON.stringify(body)
}
let data
try {
const response = await fetch(`${url}${queryString}`, config)
data = await response.json()
if (response.ok) {
return { headers: response.headers, body: data }
}
throw new Error(response.statusText)
} catch (err) {
return Promise.reject(err.message ? err.message : data)
}
}
client.get = function (instance, endpoint, query, customConfig = {}) {
return client(instance, endpoint, query, { ...customConfig, method: 'GET' })
}
client.post = function (instance, endpoint, query, body, customConfig = {}) {
return client(instance, endpoint, query, { ...customConfig, body })
}

View File

@ -1,14 +1,22 @@
import store from 'src/stacks/common/store'
import store, { RootState } from 'src/stacks/common/store'
import ky from 'ky'
export default async function client ({
method, // * get / post
instance, // * local / remote
endpoint, // * if url is empty
query, // object
body // object
}) {
const state = store.getState().instanceInfo
const client = async ({
method,
instance,
endpoint,
query,
body
}: {
method: 'get' | 'post'
instance: 'local' | 'remote'
endpoint: string
query?: {
[key: string]: string | number | boolean
}
body?: object
}): Promise<any> => {
const state: RootState['instanceInfo'] = store.getState().instanceInfo
let response
try {
@ -38,3 +46,5 @@ export default async function client ({
return Promise.reject({ body: response.error_message })
}
}
export default client

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

View File

@ -1,37 +0,0 @@
import PropTypes from 'prop-types'
import propTypesEmoji from './emoji'
import propTypesStatus from './status'
const propTypesAccount = PropTypes.shape({
// Base
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
// Attributes
display_name: PropTypes.string.isRequired,
note: PropTypes.string,
avatar: PropTypes.string.isRequired,
avatar_static: PropTypes.string.isRequired,
header: PropTypes.string.isRequired,
header_static: PropTypes.string.isRequired,
locked: PropTypes.bool.isRequired,
emojis: PropTypes.arrayOf(propTypesEmoji),
discoverable: PropTypes.bool.isRequired,
// Statistics
created_at: PropTypes.string.isRequired,
last_status_at: PropTypes.string.isRequired,
statuses_count: PropTypes.number.isRequired,
followers_count: PropTypes.number.isRequired,
following_count: PropTypes.number.isRequired,
// Others
moved: propTypesStatus,
// fields prop-types
bot: PropTypes.bool.isRequired
// source prop-types
})
export default propTypesAccount

View File

@ -1,10 +0,0 @@
import PropTypes from 'prop-types'
const propTypesApplication = PropTypes.shape({
// Base
name: PropTypes.string.isRequired,
website: PropTypes.string,
vapid_key: PropTypes.string
})
export default propTypesApplication

View File

@ -1,19 +0,0 @@
import PropTypes from 'prop-types'
const propTypesAttachment = PropTypes.shape({
// Base
id: PropTypes.string.isRequired,
type: PropTypes.oneOf(['unknown', 'image', 'gifv', 'video', 'audio'])
.isRequired,
url: PropTypes.string.isRequired,
preview_url: PropTypes.string,
// Others
remote_url: PropTypes.string,
text_url: PropTypes.string,
meta: PropTypes.object,
description: PropTypes.string,
blurhash: PropTypes.string
})
export default propTypesAttachment

View File

@ -1,23 +0,0 @@
import PropTypes from 'prop-types'
const propTypesCard = PropTypes.shape({
// Base
url: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
type: PropTypes.oneOf(['link', 'photo', 'video', 'rich']).isRequired,
// Attributes
author_name: PropTypes.string,
author_url: PropTypes.string,
provider_name: PropTypes.string,
provider_url: PropTypes.string,
html: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
image: PropTypes.string,
embed_url: PropTypes.string,
blurhash: PropTypes.string
})
export default propTypesCard

View File

@ -1,12 +0,0 @@
import PropTypes from 'prop-types'
const propTypesEmoji = PropTypes.shape({
// Base
shortcode: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
static_url: PropTypes.string.isRequired,
visible_in_picker: PropTypes.bool.isRequired,
category: PropTypes.string
})
export default propTypesEmoji

View File

@ -1,11 +0,0 @@
import PropTypes from 'prop-types'
const propTypesMention = PropTypes.shape({
// Base
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
})
export default propTypesMention

View File

@ -1,17 +0,0 @@
import PropTypes from 'prop-types'
import propTypesAccount from './account'
import propTypesStatus from './status'
const propTypesNotification = PropTypes.shape({
// Base
id: PropTypes.string.isRequired,
type: PropTypes.oneOf(['follow', 'mention', 'reblog', 'favourite', 'poll'])
.isRequired,
created_at: PropTypes.string.isRequired,
account: propTypesAccount,
// Others
status: propTypesStatus
})
export default propTypesNotification

View File

@ -1,23 +0,0 @@
import PropTypes from 'prop-types'
import propTypesEmoji from './emoji'
const propTypesPoll = PropTypes.shape({
// Base
id: PropTypes.string.isRequired,
expires_at: PropTypes.string.isRequired,
expired: PropTypes.bool.isRequired,
multiple: PropTypes.bool.isRequired,
votes_count: PropTypes.number.isRequired,
voters_count: PropTypes.number.isRequired,
voted: PropTypes.bool,
own_votes: PropTypes.arrayOf(PropTypes.number),
options: PropTypes.arrayOf(
PropTypes.exact({
title: PropTypes.string.isRequired,
votes_count: PropTypes.number.isRequired
})
).isRequired,
emojis: PropTypes.arrayOf(propTypesEmoji)
})
export default propTypesPoll

View File

@ -1,51 +0,0 @@
import PropTypes from 'prop-types'
import propTypesAccount from './account'
import propTypesAttachment from './attachment'
import propTypesApplication from './application'
import propTypesMention from './mention'
import propTypesTag from './tag'
import propTypesEmoji from './emoji'
import propTypesPoll from './poll'
import propTypesCard from './card'
const propTypesStatus = PropTypes.shape({
// Base
id: PropTypes.string.isRequired,
uri: PropTypes.string.isRequired,
created_at: PropTypes.string.isRequired,
account: propTypesAccount,
content: PropTypes.string.isRequired, // Might not be required
visibility: PropTypes.oneOf(['public', 'unlisted', 'private', 'direct'])
.isRequired,
sensitive: PropTypes.bool.isRequired,
spoiler_text: PropTypes.string,
media_attachments: PropTypes.arrayOf(propTypesAttachment),
application: propTypesApplication,
// Attributes
mentions: PropTypes.arrayOf(propTypesMention),
tags: PropTypes.arrayOf(propTypesTag),
emojis: PropTypes.arrayOf(propTypesEmoji),
// Interaction
reblogs_count: PropTypes.number.isRequired,
favourites_count: PropTypes.number.isRequired,
replies_count: PropTypes.number.isRequired,
favourited: PropTypes.bool,
reblogged: PropTypes.bool,
muted: PropTypes.bool,
bookmarked: PropTypes.bool,
pinned: PropTypes.bool,
// Others
url: PropTypes.string,
in_reply_to_id: PropTypes.string,
in_reply_to_account_id: PropTypes.string,
reblog: propTypesStatus,
poll: propTypesPoll,
card: propTypesCard,
language: PropTypes.string,
text: PropTypes.string
})
export default propTypesStatus

View File

@ -1,10 +0,0 @@
import PropTypes from 'prop-types'
const propTypesTag = PropTypes.shape({
// Base
name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
// history prop-types
})
export default propTypesTag

View File

@ -2,7 +2,7 @@ import React from 'react'
import TimelinesCombined from 'src/stacks/common/TimelinesCombined'
export default function Local () {
const Local: React.FC = () => {
return (
<TimelinesCombined
name='Local'
@ -13,3 +13,5 @@ export default function Local () {
/>
)
}
export default Local

View File

@ -6,7 +6,7 @@ import Authentication from 'src/stacks/Me/Authentication'
const Stack = createNativeStackNavigator()
export default function Me () {
const Me: React.FC = () => {
return (
<Stack.Navigator>
<Stack.Screen name='Me-Base' component={Base} />
@ -20,3 +20,5 @@ export default function Me () {
</Stack.Navigator>
)
}
export default Me

View File

@ -7,7 +7,7 @@ import sharedScreens from 'src/stacks/Shared/sharedScreens'
const Stack = createNativeStackNavigator()
export default function Notifications () {
const Notifications: React.FC = () => {
const [renderHeader, setRenderHeader] = useState(false)
useEffect(() => {
@ -18,7 +18,6 @@ export default function Notifications () {
return (
<Stack.Navigator
screenOptions={{
statusBarAnimation: 'none',
headerRight: () =>
renderHeader ? (
<Feather name='search' size={24} color='black' />
@ -34,3 +33,5 @@ export default function Notifications () {
</Stack.Navigator>
)
}
export default Notifications

View File

@ -2,12 +2,9 @@ import React from 'react'
import { View } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import Base from './Me/Base'
import Authentication from 'src/stacks/Me/Authentication'
const Stack = createNativeStackNavigator()
export default function Post () {
const Post: React.FC = () => {
return (
// <Stack.Navigator>
// <Stack.Screen name='Me-Base' component={Base} />
@ -22,3 +19,5 @@ export default function Post () {
<View></View>
)
}
export default Post

View File

@ -2,7 +2,7 @@ import React from 'react'
import TimelinesCombined from 'src/stacks/common/TimelinesCombined'
export default function Public () {
const Public: React.FC = () => {
return (
<TimelinesCombined
name='Public'
@ -13,3 +13,5 @@ export default function Public () {
/>
)
}
export default Public

View File

@ -22,7 +22,13 @@ import Timeline from 'src/stacks/common/Timeline'
// Moved account example: https://m.cmx.im/web/accounts/27812
function Header ({ uri, size }) {
const Header = ({
uri,
size
}: {
uri: string
size: { width: number; height: number }
}) => {
if (uri) {
return (
<Image
@ -35,7 +41,13 @@ function Header ({ uri, size }) {
}
}
function Information ({ account, emojis }) {
const Information = ({
account,
emojis
}: {
account: mastodon.Account
emojis: mastodon.Emoji[]
}) => {
return (
<View style={styles.information}>
{/* <Text>Moved or not: {account.moved}</Text> */}
@ -85,7 +97,7 @@ function Information ({ account, emojis }) {
)
}
function Toots ({ account }) {
const Toots = ({ account }: { account: string }) => {
const [segment, setSegment] = useState(0)
const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
false
@ -161,11 +173,19 @@ function Toots ({ account }) {
)
}
export default function Account ({
export interface Props {
route: {
params: {
id: string
}
}
}
const Account: React.FC<Props> = ({
route: {
params: { id }
}
}) {
}) => {
const dispatch = useDispatch()
const accountState = useSelector(state => state.account)
// const stateRelationships = useSelector(relationshipsState)
@ -241,3 +261,5 @@ const styles = StyleSheet.create({
marginTop: 4
}
})
export default Account

View File

@ -7,11 +7,19 @@ import { reset } from 'src/stacks/common/timelineSlice'
// Show remote hashtag? Only when private, show local version?
export default function Hashtag ({
export interface Props {
route: {
params: {
hashtag: string
}
}
}
const Hashtag: React.FC<Props> = ({
route: {
params: { hashtag }
}
}) {
}) => {
const dispatch = useDispatch()
useFocusEffect(
@ -26,3 +34,5 @@ export default function Hashtag ({
return <Timeline page='Hashtag' hashtag={hashtag} />
}
export default Hashtag

View File

@ -7,11 +7,19 @@ import { reset } from 'src/stacks/common/timelineSlice'
// Show remote hashtag? Only when private, show local version?
export default function Toot ({
export interface Props {
route: {
params: {
toot: string
}
}
}
const Toot: React.FC<Props> = ({
route: {
params: { toot }
}
}) {
}) => {
const dispatch = useDispatch()
useFocusEffect(
@ -26,3 +34,5 @@ export default function Toot ({
return <Timeline page='Toot' toot={toot} disableRefresh />
}
export default Toot

View File

@ -1,17 +1,22 @@
import React from 'react'
import PropTypes from 'prop-types'
import { WebView } from 'react-native-webview'
// Update page title
export default function Webview ({
export interface Props {
route: {
params: {
uri: string
}
}
}
const Webview: React.FC<Props> = ({
route: {
params: { uri }
}
}) {
}) => {
return <WebView source={{ uri: uri }} />
}
Webview.propTypes = {
uri: PropTypes.string.isRequired
}
export default Webview

View File

@ -5,7 +5,11 @@ import Hashtag from 'src/stacks/Shared/Hashtag'
import Toot from 'src/stacks/Shared/Toot'
import Webview from 'src/stacks/Shared/Webview'
export default function sharedScreens (Stack) {
export interface Props {
Stack: any
}
const sharedScreens = Stack => {
return [
<Stack.Screen
key='Account'
@ -43,3 +47,5 @@ export default function sharedScreens (Stack) {
/>
]
}
export default sharedScreens

View File

@ -1,24 +1,24 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { ActivityIndicator, FlatList, Text, View } from 'react-native'
import { useSelector, useDispatch } from 'react-redux'
import TootNotification from 'src/components/TootNotification'
import TootTimeline from 'src/components/TootTimeline'
import { RootState } from 'src/stacks/common/store'
import { fetch } from './timelineSlice'
// Opening nesting hashtag pages
export default function Timeline ({
page,
hashtag,
list,
toot,
account,
disableRefresh = false
}) {
const Timeline: React.FC<{
page: store.TimelinePage
hashtag?: string
list?: string
toot?: string
account?: string
disableRefresh?: boolean
}> = ({ page, hashtag, list, toot, account, disableRefresh = false }) => {
const dispatch = useDispatch()
const state = useSelector(state => state.timelines[page])
const state = useSelector((state: RootState) => state.timelines[page])
const [timelineReady, setTimelineReady] = useState(false)
useEffect(() => {
@ -27,7 +27,9 @@ export default function Timeline ({
dispatch(fetch({ page, hashtag, list, toot, account }))
setTimelineReady(true)
}
return () => (mounted = false)
return () => {
mounted = false
}
}, [state, dispatch])
let content
@ -42,12 +44,12 @@ export default function Timeline ({
keyExtractor={({ id }) => id}
renderItem={({ item, index, separators }) =>
page === 'Notifications' ? (
<TootNotification key={item.key} toot={item} />
<TootNotification key={index} toot={item} />
) : (
<TootTimeline key={item.key} toot={item} />
<TootTimeline key={index} toot={item} />
)
}
{...(state.pointer && { initialScrollIndex: state.pointer })}
// {...(state.pointer && { initialScrollIndex: state.pointer })}
{...(!disableRefresh && {
onRefresh: () =>
dispatch(
@ -84,10 +86,4 @@ export default function Timeline ({
return <View>{content}</View>
}
Timeline.propTypes = {
page: PropTypes.string.isRequired,
hashtag: PropTypes.string,
list: PropTypes.string,
toot: PropTypes.string,
disableRefresh: PropTypes.bool
}
export default Timeline

View File

@ -1,5 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { Dimensions, FlatList, View } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import SegmentedControl from '@react-native-community/segmented-control'
@ -10,7 +9,7 @@ import sharedScreens from 'src/stacks/Shared/sharedScreens'
const Stack = createNativeStackNavigator()
function Page ({ item: { page } }) {
const Page = ({ item: { page } }: { item: { page: store.TimelinePage } }) => {
return (
<View style={{ width: Dimensions.get('window').width }}>
<Timeline page={page} />
@ -18,7 +17,12 @@ function Page ({ item: { page } }) {
)
}
export default function TimelinesCombined ({ name, content }) {
export interface Props {
name: string
content: { title: string; page: string }[]
}
const TimelinesCombined: React.FC<Props> = ({ name, content }) => {
const [segment, setSegment] = useState(0)
const [renderHeader, setRenderHeader] = useState(false)
const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
@ -30,14 +34,13 @@ export default function TimelinesCombined ({ name, content }) {
return
}, [])
const horizontalPaging = useRef()
const horizontalPaging = useRef(null!)
return (
<Stack.Navigator>
<Stack.Screen
name={name}
options={{
statusBarAnimation: 'none',
headerRight: () =>
renderHeader ? (
<Feather name='search' size={24} color='black' />
@ -96,13 +99,4 @@ export default function TimelinesCombined ({ name, content }) {
)
}
TimelinesCombined.propTypes = {
name: PropTypes.string.isRequired,
content: PropTypes.arrayOf(
PropTypes.exact({
title: PropTypes.string.isRequired,
page: Timeline.propTypes.page,
instance: PropTypes.oneOf(['local', 'remote'])
})
).isRequired
}
export default TimelinesCombined

View File

@ -4,7 +4,7 @@ import client from 'src/api/client'
export const fetch = createAsyncThunk(
'account/fetch',
async ({ id }, { getState }) => {
async ({ id }: { id: string }) => {
const res = await client({
method: 'get',
instance: 'local',
@ -25,18 +25,20 @@ export const accountSlice = createSlice({
reducers: {
reset: () => accountInitState
},
extraReducers: {
[fetch.pending]: state => {
extraReducers: builder => {
builder.addCase(fetch.pending, state => {
state.status = 'loading'
},
[fetch.fulfilled]: (state, action) => {
})
builder.addCase(fetch.fulfilled, (state, action) => {
state.status = 'succeeded'
state.account = action.payload
},
[fetch.rejected]: (state, action) => {
})
builder.addCase(fetch.rejected, (state, action) => {
state.status = 'failed'
console.error(action.error.message)
}
})
}
})

View File

@ -3,7 +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'
// import relationshipsSlice from 'src/stacks/common/relationshipsSlice'
// get site information from local storage and pass to reducers
const preloadedState = {
@ -18,10 +18,14 @@ const reducer = {
instanceInfo: instanceInfoSlice,
timelines: timelineSlice,
account: accountSlice,
relationships: relationshipsSlice
// relationships: relationshipsSlice
}
export default configureStore({
const store = configureStore({
preloadedState,
reducer
})
export default store
export type RootState = ReturnType<typeof store.getState>

View File

@ -1,25 +1,40 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import {
AnyAction,
createAsyncThunk,
createSlice,
PayloadAction
} from '@reduxjs/toolkit'
import client from 'src/api/client'
// Naming convention
// Following: timelines/home
// Local: timelines/public/local
// LocalPublic: timelines/public
// RemotePublic: remote/timelines/public
// Notifications: notifications
// Hashtag: hastag
// List: list
export const fetch = createAsyncThunk(
'timeline/fetch',
async (
{ page, paginationDirection, query = {}, account, hashtag, list, toot },
{
page,
paginationDirection,
query = {},
account,
hashtag,
list,
toot
}: {
page: string
paginationDirection?: 'prev' | 'next'
query?: {
[key: string]: string | number | boolean
}
account?: string
hashtag?: string
list?: string
toot?: string
},
{ getState }
) => {
let res
if (paginationDirection) {
//@ts-ignore
const allToots = getState().timelines[page].toots
switch (paginationDirection) {
case 'prev':
@ -177,7 +192,7 @@ export const fetch = createAsyncThunk(
}
)
const timelineInitState = {
const timelineInitState: store.TimelineState = {
toots: [],
pointer: undefined,
status: 'idle'
@ -199,31 +214,41 @@ export const timelineSlice = createSlice({
Account_Media: timelineInitState
},
reducers: {
reset (state, action) {
reset: (state, action: PayloadAction<store.TimelinePage>) => {
state[action.payload] = timelineInitState
}
},
extraReducers: {
[fetch.pending]: (state, action) => {
extraReducers: builder => {
builder.addCase(fetch.pending, (state, action: AnyAction) => {
//@ts-ignore
state[action.meta.arg.page].status = 'loading'
},
[fetch.fulfilled]: (state, action) => {
})
builder.addCase(fetch.fulfilled, (state, action) => {
//@ts-ignore
state[action.meta.arg.page].status = 'succeeded'
if (action.meta.arg.paginationDirection === 'prev') {
state[action.meta.arg.page].toots.unshift(...action.payload.toots)
} else {
state[action.meta.arg.page].toots.push(...action.payload.toots)
if (action.payload?.toots) {
if (action.meta.arg.paginationDirection === 'prev') {
//@ts-ignore
state[action.meta.arg.page].toots.unshift(...action.payload.toots)
} else {
//@ts-ignore
state[action.meta.arg.page].toots.push(...action.payload.toots)
}
}
if (action.payload.pointer) {
if (action.payload?.pointer) {
//@ts-ignore
state[action.meta.arg.page].pointer = action.payload.pointer
}
},
[fetch.rejected]: (state, action) => {
})
builder.addCase(fetch.rejected, (state, action) => {
//@ts-ignore
state[action.meta.arg.page].status = 'failed'
console.error(action.error.message)
}
})
}
})

View File

@ -1,6 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
export async function getItem () {
const getItem = async () => {
try {
const value = await AsyncStorage.getItem('@social.xmflsct.com')
if (!value) {
@ -15,10 +15,12 @@ export async function getItem () {
}
}
export async function getAllKeys () {
const getAllKeys = async () => {
try {
return await AsyncStorage.getAllKeys()
} catch (e) {
console.error('Get all keys error')
}
}
export default { getItem, getAllKeys }

View File

@ -1,7 +1,5 @@
import PropTypes from 'prop-types'
export default function relativeTime (date) {
var units = {
const relativeTime = (date: string) => {
let units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
@ -10,9 +8,9 @@ export default function relativeTime (date) {
second: 1000
}
var rtf = new Intl.RelativeTimeFormat('zh', { numeric: 'auto' })
let rtf = new Intl.RelativeTimeFormat('zh', { numeric: 'auto' })
var elapsed = new Date(date) - new Date()
let elapsed: number = new Date(date) - new Date()
// "Math.abs" accounts for both "past" & "future" scenarios
for (var u in units)
@ -20,6 +18,4 @@ export default function relativeTime (date) {
return rtf.format(Math.round(elapsed / units[u]), u)
}
relativeTime.propTypes = {
date: PropTypes.string.isRequired
}
export default relativeTime

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"jsx": "react-native",
"lib": ["dom", "esnext"],
"moduleResolution": "node",
"noEmit": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"strict": true,
"baseUrl": ".",
"paths": {
"src/*": ["src/*"]
}
}
}