mirror of
https://github.com/tooot-app/app
synced 2025-02-18 04:40:57 +01:00
Transform into TypeScript
This commit is contained in:
parent
698b54868e
commit
d2cc643b9c
6
App.jsx
6
App.jsx
@ -1,6 +0,0 @@
|
||||
import React from 'react'
|
||||
import Index from './src/Index'
|
||||
|
||||
const App = () => <Index />
|
||||
|
||||
export default App
|
6
App.tsx
Normal file
6
App.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Index } from 'src/Index'
|
||||
|
||||
const App: React.FC = () => <Index />
|
||||
|
||||
export default App
|
@ -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
121
package-lock.json
generated
@ -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",
|
||||
|
11
package.json
11
package.json
@ -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
244
src/@types/mastodon.d.ts
vendored
Normal 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
47
src/@types/store.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
@ -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} />
|
||||
}
|
@ -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 })
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
||||
}
|
52
src/components/Toot/Emojis.tsx
Normal file
52
src/components/Toot/Emojis.tsx
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
41
src/components/action.ts
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -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>
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -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 }
|
@ -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
16
tsconfig.json
Normal 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/*"]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user