mirror of
https://github.com/tooot-app/app
synced 2025-02-20 13:50:49 +01:00
Fix bugs
This commit is contained in:
parent
31b2f67feb
commit
7c6aba77ba
1
README.md
Normal file
1
README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
<a rel="me" href="https://social.xmflsct.com/@tooot">@tooot@xmflsct.com</a>
|
@ -17,26 +17,10 @@ export default (): ExpoConfig => ({
|
|||||||
image: './assets/splash.png'
|
image: './assets/splash.png'
|
||||||
},
|
},
|
||||||
scheme: 'tooot',
|
scheme: 'tooot',
|
||||||
ios: {
|
|
||||||
buildNumber: '1.0',
|
|
||||||
config: { usesNonExemptEncryption: false },
|
|
||||||
bundleIdentifier: 'com.xmflsct.app.tooot',
|
|
||||||
googleServicesFile: './configs/GoogleService-Info.plist',
|
|
||||||
infoPlist: {
|
|
||||||
CFBundleAllowMixedLocalizations: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
android: {
|
|
||||||
versionCode: 1,
|
|
||||||
package: 'com.xmflsct.app.tooot',
|
|
||||||
googleServicesFile: './configs/google-services.json',
|
|
||||||
permissions: ['CAMERA', 'VIBRATE']
|
|
||||||
},
|
|
||||||
locales: {
|
|
||||||
en: './src/i18n/en/system.json',
|
|
||||||
zh: './src/i18n/zh-Hans/system.json'
|
|
||||||
},
|
|
||||||
assetBundlePatterns: ['assets/*'],
|
assetBundlePatterns: ['assets/*'],
|
||||||
|
extra: {
|
||||||
|
sentryDSN: process.env.SENTRY_DSN
|
||||||
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
postPublish: [
|
postPublish: [
|
||||||
{
|
{
|
||||||
@ -51,8 +35,24 @@ export default (): ExpoConfig => ({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
extra: {
|
ios: {
|
||||||
sentryDSN: process.env.SENTRY_DSN
|
buildNumber: '2',
|
||||||
|
config: { usesNonExemptEncryption: false },
|
||||||
|
bundleIdentifier: 'com.xmflsct.app.tooot',
|
||||||
|
googleServicesFile: './configs/GoogleService-Info.plist',
|
||||||
|
infoPlist: {
|
||||||
|
CFBundleAllowMixedLocalizations: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
locales: {
|
||||||
|
en: './src/i18n/en/system.json',
|
||||||
|
zh: './src/i18n/zh-Hans/system.json'
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
versionCode: 2,
|
||||||
|
package: 'com.xmflsct.app.tooot',
|
||||||
|
googleServicesFile: './configs/google-services.json',
|
||||||
|
permissions: ['CAMERA', 'VIBRATE']
|
||||||
},
|
},
|
||||||
web: {
|
web: {
|
||||||
config: {
|
config: {
|
||||||
|
97
demo/statuses.json
Normal file
97
demo/statuses.json
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"pageParams": [],
|
||||||
|
"pages": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"created_at": "2021-01-22T03:48:33.901Z",
|
||||||
|
"sensitive": false,
|
||||||
|
"visibility": "public",
|
||||||
|
"replies_count": 9,
|
||||||
|
"reblogs_count": 15,
|
||||||
|
"favourites_count": 8,
|
||||||
|
"favourited": false,
|
||||||
|
"reblogged": true,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": false,
|
||||||
|
"content": "<p>Would you like to try out this simple and open-source mobile app for Mastodon? 😊</p>",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "tooot",
|
||||||
|
"website": "https://tooot.app"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "1",
|
||||||
|
"username": "tooot📱",
|
||||||
|
"acct": "tooot@xmflsct.com",
|
||||||
|
"display_name": "tooot📱",
|
||||||
|
"avatar_static": "https://avatars.githubusercontent.com/u/77554750?s=200&v=4"
|
||||||
|
},
|
||||||
|
"media_attachments": [],
|
||||||
|
"poll": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"created_at": "2021-01-22T03:48:33.901Z",
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"visibility": "public",
|
||||||
|
"replies_count": 5,
|
||||||
|
"reblogs_count": 6,
|
||||||
|
"favourites_count": 11,
|
||||||
|
"favourited": true,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": false,
|
||||||
|
"content": "<p>Mastodon is a free and open-source self-hosted social networking service. It allows anyone to host their own server node in the network, and its various separately operated user bases are federated across many different servers. These nodes are referred to as \"instances\" by Mastodon users.</p>",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "Web",
|
||||||
|
"website": null
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "2",
|
||||||
|
"username": "Mastodon",
|
||||||
|
"acct": "mastodon",
|
||||||
|
"display_name": "Mastodon",
|
||||||
|
"avatar_static": "https://cdn.dnaindia.com/sites/default/files/styles/full/public/2017/04/06/563120-123.jpg"
|
||||||
|
},
|
||||||
|
"media_attachments": [],
|
||||||
|
"card": {
|
||||||
|
"url": "https://joinmastodon.org/",
|
||||||
|
"title": "Giving social networking back to you - Mastodon",
|
||||||
|
"description": "Mastodon is an open source decentralized social network - by the people for the people. Join the federation and take back control of your social media!",
|
||||||
|
"type": "link",
|
||||||
|
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Mastodon_Logotype_%28Simple%29.svg/1200px-Mastodon_Logotype_%28Simple%29.svg.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"created_at": "2021-01-22T03:48:33.901Z",
|
||||||
|
"spoiler_text": "",
|
||||||
|
"visibility": "public",
|
||||||
|
"replies_count": 2,
|
||||||
|
"reblogs_count": null,
|
||||||
|
"favourites_count": 3,
|
||||||
|
"favourited": false,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": true,
|
||||||
|
"content": "<p>These servers are connected as a federated social network, allowing users from different servers to interact with each other seamlessly. Once a Mastodon server knows another Mastodon server, it \"federates\" with the other Mastodon server. Mastodon is a part of the wider Fediverse, allowing its users to also interact with users on different open platforms that support the same protocol, such as PeerTube and Friendica.</p>",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "Web",
|
||||||
|
"website": null
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "3",
|
||||||
|
"username": "Fediverse",
|
||||||
|
"acct": "fediverse",
|
||||||
|
"display_name": "Fediverse",
|
||||||
|
"avatar_static": "https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png"
|
||||||
|
},
|
||||||
|
"media_attachments": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
@ -35,11 +35,13 @@ const client = async <T = unknown>({
|
|||||||
localIndex !== undefined ? localIndex : state.local.activeIndex
|
localIndex !== undefined ? localIndex : state.local.activeIndex
|
||||||
|
|
||||||
let domain = null
|
let domain = null
|
||||||
|
let token = null
|
||||||
if (instance === 'remote') {
|
if (instance === 'remote') {
|
||||||
domain = instanceDomain || state.remote.url
|
domain = instanceDomain || state.remote.url
|
||||||
} else {
|
} else {
|
||||||
if (theLocalIndex !== null && state.local.instances[theLocalIndex]) {
|
if (theLocalIndex !== null && state.local.instances[theLocalIndex]) {
|
||||||
domain = state.local.instances[theLocalIndex].url
|
domain = state.local.instances[theLocalIndex].url
|
||||||
|
token = state.local.instances[theLocalIndex].token
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
|
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
|
||||||
@ -69,8 +71,8 @@ const client = async <T = unknown>({
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers,
|
...headers,
|
||||||
...(instance === 'local' && {
|
...(token && {
|
||||||
Authorization: `Bearer ${state.local!.instances[theLocalIndex!].token}`
|
Authorization: `Bearer ${token}`
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
...(body && { data: body }),
|
...(body && { data: body }),
|
||||||
|
@ -10,7 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, Image, StyleSheet, Text, TextInput, View } from 'react-native'
|
import { Alert, Image, StyleSheet, Text, TextInput, View } from 'react-native'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
@ -42,6 +42,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
|
|
||||||
const instanceQuery = useInstanceQuery({
|
const instanceQuery = useInstanceQuery({
|
||||||
instanceDomain,
|
instanceDomain,
|
||||||
|
checkPublic: type === 'remote',
|
||||||
options: { enabled: false, retry: false }
|
options: { enabled: false, retry: false }
|
||||||
})
|
})
|
||||||
const appsQuery = useAppsQuery({
|
const appsQuery = useAppsQuery({
|
||||||
@ -170,7 +171,12 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
styles.textInput,
|
styles.textInput,
|
||||||
{
|
{
|
||||||
color: theme.primary,
|
color: theme.primary,
|
||||||
borderBottomColor: theme.border
|
borderBottomColor:
|
||||||
|
type === 'remote' &&
|
||||||
|
instanceQuery.data &&
|
||||||
|
!instanceQuery.data.publicAllow
|
||||||
|
? theme.red
|
||||||
|
: theme.border
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
@ -188,10 +194,20 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
type='text'
|
type='text'
|
||||||
content={buttonContent}
|
content={buttonContent}
|
||||||
onPress={processUpdate}
|
onPress={processUpdate}
|
||||||
disabled={!instanceQuery.data?.uri}
|
disabled={
|
||||||
|
!instanceQuery.data?.uri ||
|
||||||
|
(type === 'remote' && !instanceQuery.data.publicAllow)
|
||||||
|
}
|
||||||
loading={instanceQuery.isFetching || appsQuery.isFetching}
|
loading={instanceQuery.isFetching || appsQuery.isFetching}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
{type === 'remote' &&
|
||||||
|
instanceQuery.data &&
|
||||||
|
!instanceQuery.data.publicAllow ? (
|
||||||
|
<Text style={[styles.privateInstance, { color: theme.red }]}>
|
||||||
|
{t('server.privateInstance')}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
<View>
|
<View>
|
||||||
<InstanceInfo
|
<InstanceInfo
|
||||||
visible={instanceQuery.data?.title !== undefined}
|
visible={instanceQuery.data?.title !== undefined}
|
||||||
@ -278,6 +294,12 @@ const styles = StyleSheet.create({
|
|||||||
...StyleConstants.FontStyle.M,
|
...StyleConstants.FontStyle.M,
|
||||||
marginRight: StyleConstants.Spacing.M
|
marginRight: StyleConstants.Spacing.M
|
||||||
},
|
},
|
||||||
|
privateInstance: {
|
||||||
|
...StyleConstants.FontStyle.S,
|
||||||
|
fontWeight: StyleConstants.Font.Weight.Bold,
|
||||||
|
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginTop: StyleConstants.Spacing.XS
|
||||||
|
},
|
||||||
instanceStats: {
|
instanceStats: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { StyleSheet, Text } from 'react-native'
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
import { Image } from 'react-native-expo-image-cache'
|
import { Image } from 'react-native-expo-image-cache'
|
||||||
|
|
||||||
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
|
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
|
||||||
@ -19,18 +19,26 @@ const ParseEmojis: React.FC<Props> = ({
|
|||||||
size = 'M',
|
size = 'M',
|
||||||
fontBold = false
|
fontBold = false
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme()
|
const { mode, theme } = useTheme()
|
||||||
const styles = StyleSheet.create({
|
const styles = useMemo(() => {
|
||||||
text: {
|
return StyleSheet.create({
|
||||||
color: theme.primary,
|
text: {
|
||||||
...StyleConstants.FontStyle[size],
|
color: theme.primary,
|
||||||
...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold })
|
...StyleConstants.FontStyle[size],
|
||||||
},
|
...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold })
|
||||||
image: {
|
},
|
||||||
width: StyleConstants.Font.Size[size],
|
imageContainer: {
|
||||||
height: StyleConstants.Font.Size[size]
|
paddingVertical:
|
||||||
}
|
(StyleConstants.Font.LineHeight[size] -
|
||||||
})
|
StyleConstants.Font.Size[size]) /
|
||||||
|
3
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: StyleConstants.Font.Size[size],
|
||||||
|
height: StyleConstants.Font.Size[size]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [mode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text style={styles.text}>
|
<Text style={styles.text}>
|
||||||
@ -50,7 +58,13 @@ const ParseEmojis: React.FC<Props> = ({
|
|||||||
<Text key={i}>
|
<Text key={i}>
|
||||||
{/* When emoji starts a paragraph, lineHeight will break */}
|
{/* When emoji starts a paragraph, lineHeight will break */}
|
||||||
{i === 0 ? <Text> </Text> : null}
|
{i === 0 ? <Text> </Text> : null}
|
||||||
<Image uri={emojis[emojiIndex].url} style={[styles.image]} />
|
<View style={styles.imageContainer}>
|
||||||
|
<Image
|
||||||
|
transitionDuration={0}
|
||||||
|
uri={emojis[emojiIndex].url}
|
||||||
|
style={[styles.image]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -154,12 +154,16 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
tags,
|
tags,
|
||||||
showFullLink = false,
|
showFullLink = false,
|
||||||
numberOfLines = 10,
|
numberOfLines = 10,
|
||||||
expandHint = '全文',
|
expandHint,
|
||||||
disableDetails = false
|
disableDetails = false
|
||||||
}) => {
|
}) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const { t, i18n } = useTranslation('componentParse')
|
||||||
|
if (!expandHint) {
|
||||||
|
expandHint = t('HTML.defaultHint')
|
||||||
|
}
|
||||||
|
|
||||||
const renderNodeCallback = useCallback(
|
const renderNodeCallback = useCallback(
|
||||||
(node, index) =>
|
(node, index) =>
|
||||||
@ -261,7 +265,7 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[theme]
|
[theme, i18n.language]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -6,7 +6,8 @@ import sharedScreens from '@screens/Shared/sharedScreens'
|
|||||||
import { getLocalActiveIndex, getRemoteUrl } from '@utils/slices/instancesSlice'
|
import { getLocalActiveIndex, getRemoteUrl } from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Dimensions, Platform, StyleSheet, View } from 'react-native'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Dimensions, Platform, StyleSheet } from 'react-native'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import { TabView } from 'react-native-tab-view'
|
import { TabView } from 'react-native-tab-view'
|
||||||
import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter'
|
import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter'
|
||||||
@ -18,22 +19,32 @@ const Stack = createNativeStackNavigator<
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
name: 'Local' | 'Public'
|
name: 'Local' | 'Public'
|
||||||
content: { title: string; page: App.Pages; remote?: boolean }[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Timelines: React.FC<Props> = ({ name, content }) => {
|
const Timelines: React.FC<Props> = ({ name }) => {
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
const remoteUrl = useSelector(getRemoteUrl)
|
const remoteUrl = useSelector(getRemoteUrl)
|
||||||
|
const mapNameToContent: {
|
||||||
|
[key: string]: { title: string; page: App.Pages }[]
|
||||||
|
} = {
|
||||||
|
Local: [
|
||||||
|
{ title: t('local:heading.segments.left'), page: 'Following' },
|
||||||
|
{ title: t('local:heading.segments.right'), page: 'Local' }
|
||||||
|
],
|
||||||
|
Public: [
|
||||||
|
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
|
||||||
|
{ title: remoteUrl, page: 'RemotePublic' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { mode } = useTheme()
|
|
||||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
const publicDomain = useSelector(getRemoteUrl)
|
|
||||||
const [segment, setSegment] = useState(0)
|
|
||||||
|
|
||||||
const onPressSearch = useCallback(() => {
|
const onPressSearch = useCallback(() => {
|
||||||
navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' })
|
navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const routes = content
|
const routes = mapNameToContent[name]
|
||||||
.filter(p => (localActiveIndex !== null ? true : p.page === 'RemotePublic'))
|
.filter(p => (localActiveIndex !== null ? true : p.page === 'RemotePublic'))
|
||||||
.map(p => ({ key: p.page }))
|
.map(p => ({ key: p.page }))
|
||||||
|
|
||||||
@ -54,39 +65,37 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
[localActiveIndex]
|
[localActiveIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { mode } = useTheme()
|
||||||
|
const [segment, setSegment] = useState(0)
|
||||||
const screenOptions = useMemo(() => {
|
const screenOptions = useMemo(() => {
|
||||||
if (localActiveIndex === null) {
|
if (localActiveIndex === null) {
|
||||||
if (name === 'Public') {
|
if (name === 'Public') {
|
||||||
return {
|
return {
|
||||||
headerTitle: publicDomain,
|
headerTitle: remoteUrl,
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
headerCenter: () => <HeaderCenter content={publicDomain} />
|
headerCenter: () => <HeaderCenter content={remoteUrl} />
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
headerCenter: () => (
|
headerCenter: () => (
|
||||||
<View style={styles.segmentsContainer}>
|
<SegmentedControl
|
||||||
<SegmentedControl
|
appearance={mode}
|
||||||
appearance={mode}
|
values={mapNameToContent[name].map(p => p.title)}
|
||||||
values={[
|
selectedIndex={segment}
|
||||||
content[0].title,
|
onChange={({ nativeEvent }) =>
|
||||||
content[1].remote ? remoteUrl : content[1].title
|
setSegment(nativeEvent.selectedSegmentIndex)
|
||||||
]}
|
}
|
||||||
selectedIndex={segment}
|
style={styles.segmentsContainer}
|
||||||
onChange={({ nativeEvent }) =>
|
/>
|
||||||
setSegment(nativeEvent.selectedSegmentIndex)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
),
|
),
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<HeaderRight content='Search' onPress={onPressSearch} />
|
<HeaderRight content='Search' onPress={onPressSearch} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [localActiveIndex, mode, segment])
|
}, [localActiveIndex, mode, segment, i18n.language])
|
||||||
|
|
||||||
const renderPager = useCallback(props => <ViewPagerAdapter {...props} />, [])
|
const renderPager = useCallback(props => <ViewPagerAdapter {...props} />, [])
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import TimelineEmpty from '@components/Timelines/Timeline/Empty'
|
|||||||
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
||||||
import TimelineHeader from '@components/Timelines/Timeline/Header'
|
import TimelineHeader from '@components/Timelines/Timeline/Header'
|
||||||
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
|
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
|
||||||
import { useScrollToTop } from '@react-navigation/native'
|
import { useNavigation, useScrollToTop } from '@react-navigation/native'
|
||||||
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
@ -19,7 +19,6 @@ import { FlatList } from 'react-native-gesture-handler'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import { InfiniteData, useQueryClient } from 'react-query'
|
|
||||||
import { getPublicRemoteNotice } from '@utils/slices/contextsSlice'
|
import { getPublicRemoteNotice } from '@utils/slices/contextsSlice'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -58,25 +57,12 @@ const Timeline: React.FC<Props> = ({
|
|||||||
isSuccess,
|
isSuccess,
|
||||||
isFetching,
|
isFetching,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasPreviousPage,
|
|
||||||
fetchPreviousPage,
|
|
||||||
isFetchingPreviousPage,
|
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
isFetchingNextPage
|
isFetchingNextPage
|
||||||
} = useTimelineQuery({
|
} = useTimelineQuery({
|
||||||
...queryKeyParams,
|
...queryKeyParams,
|
||||||
options: {
|
options: {
|
||||||
getPreviousPageParam: firstPage => {
|
|
||||||
return Array.isArray(firstPage) && firstPage.length
|
|
||||||
? {
|
|
||||||
direction: 'prev',
|
|
||||||
id: firstPage[0].last_status
|
|
||||||
? firstPage[0].last_status.id
|
|
||||||
: firstPage[0].id
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
},
|
|
||||||
getNextPageParam: lastPage => {
|
getNextPageParam: lastPage => {
|
||||||
return Array.isArray(lastPage) && lastPage.length
|
return Array.isArray(lastPage) && lastPage.length
|
||||||
? {
|
? {
|
||||||
@ -94,16 +80,23 @@ const Timeline: React.FC<Props> = ({
|
|||||||
|
|
||||||
// Clear unread notification badge
|
// Clear unread notification badge
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const navigation = useNavigation()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page === 'Notifications' && flattenData.length) {
|
const unsubscribe = navigation.addListener('focus', props => {
|
||||||
dispatch(
|
if (props.target && props.target.includes('Screen-Notifications-Root')) {
|
||||||
localUpdateNotification({
|
if (flattenData.length) {
|
||||||
unread: false,
|
dispatch(
|
||||||
latestTime: (flattenData[0] as Mastodon.Notification).created_at
|
localUpdateNotification({
|
||||||
})
|
unread: false,
|
||||||
)
|
latestTime: (flattenData[0] as Mastodon.Notification).created_at
|
||||||
}
|
})
|
||||||
}, [flattenData])
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return unsubscribe
|
||||||
|
}, [navigation, flattenData])
|
||||||
|
|
||||||
const flRef = useRef<FlatList<any>>(null)
|
const flRef = useRef<FlatList<any>>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -166,43 +159,29 @@ const Timeline: React.FC<Props> = ({
|
|||||||
[hasNextPage]
|
[hasNextPage]
|
||||||
)
|
)
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const isSwipeDown = useRef(false)
|
||||||
const refreshControl = useMemo(
|
const refreshControl = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
{...(Platform.OS === 'android' && { enabled: true })}
|
{...(Platform.OS === 'android' && { enabled: true })}
|
||||||
refreshing={
|
refreshing={
|
||||||
isFetchingPreviousPage ||
|
isSwipeDown.current && isFetching && !isFetchingNextPage && !isLoading
|
||||||
(isFetching && !isFetchingNextPage && !isLoading)
|
|
||||||
}
|
}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
// if (hasPreviousPage) {
|
isSwipeDown.current = true
|
||||||
// fetchPreviousPage()
|
|
||||||
// } else {
|
|
||||||
// queryClient.setQueryData<InfiniteData<any> | undefined>(
|
|
||||||
// queryKey,
|
|
||||||
// data => {
|
|
||||||
// if (data) {
|
|
||||||
// return {
|
|
||||||
// pages: data.pages.slice(1),
|
|
||||||
// pageParams: data.pageParams.slice(1)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
refetch()
|
refetch()
|
||||||
// }
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[
|
[isSwipeDown.current, isFetching, isFetchingNextPage, isLoading]
|
||||||
hasPreviousPage,
|
|
||||||
isFetchingPreviousPage,
|
|
||||||
isFetching,
|
|
||||||
isFetchingNextPage,
|
|
||||||
isLoading
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFetching) {
|
||||||
|
isSwipeDown.current = false
|
||||||
|
}
|
||||||
|
}, [isFetching])
|
||||||
|
|
||||||
const onScrollToIndexFailed = useCallback(error => {
|
const onScrollToIndexFailed = useCallback(error => {
|
||||||
const offset = error.averageItemLength * error.index
|
const offset = error.averageItemLength * error.index
|
||||||
flRef.current?.scrollToOffset({ offset })
|
flRef.current?.scrollToOffset({ offset })
|
||||||
|
@ -79,43 +79,44 @@ const TimelineConversation: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{conversation.last_status ? (
|
{conversation.last_status ? (
|
||||||
<View
|
<>
|
||||||
style={{
|
<View
|
||||||
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
style={{
|
||||||
paddingLeft: highlighted
|
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
||||||
? 0
|
paddingLeft: highlighted
|
||||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
? 0
|
||||||
}}
|
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||||
>
|
}}
|
||||||
<TimelineContent
|
>
|
||||||
status={conversation.last_status}
|
<TimelineContent
|
||||||
highlighted={highlighted}
|
status={conversation.last_status}
|
||||||
/>
|
highlighted={highlighted}
|
||||||
{conversation.last_status.poll && (
|
|
||||||
<TimelinePoll
|
|
||||||
queryKey={queryKey}
|
|
||||||
statusId={conversation.last_status.id}
|
|
||||||
poll={conversation.last_status.poll}
|
|
||||||
reblog={false}
|
|
||||||
sameAccount={conversation.last_status.id === localAccount?.id}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{conversation.last_status.poll && (
|
||||||
</View>
|
<TimelinePoll
|
||||||
|
queryKey={queryKey}
|
||||||
|
statusId={conversation.last_status.id}
|
||||||
|
poll={conversation.last_status.poll}
|
||||||
|
reblog={false}
|
||||||
|
sameAccount={conversation.last_status.id === localAccount?.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
paddingLeft: highlighted
|
||||||
|
? 0
|
||||||
|
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TimelineActions
|
||||||
|
queryKey={queryKey}
|
||||||
|
status={conversation.last_status}
|
||||||
|
reblog={false}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
paddingLeft: highlighted
|
|
||||||
? 0
|
|
||||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TimelineActions
|
|
||||||
queryKey={queryKey}
|
|
||||||
status={conversation.last_status!}
|
|
||||||
reblog={false}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -111,12 +111,10 @@ const AttachmentAudio: React.FC<Props> = ({
|
|||||||
minimumTrackTintColor={theme.secondary}
|
minimumTrackTintColor={theme.secondary}
|
||||||
maximumTrackTintColor={theme.disabled}
|
maximumTrackTintColor={theme.disabled}
|
||||||
// onSlidingStart={() => {
|
// onSlidingStart={() => {
|
||||||
// console.log('yes!!!')
|
|
||||||
// audioPlayer?.pauseAsync()
|
// audioPlayer?.pauseAsync()
|
||||||
// setAudioPlaying(false)
|
// setAudioPlaying(false)
|
||||||
// }}
|
// }}
|
||||||
// onSlidingComplete={value => {
|
// onSlidingComplete={value => {
|
||||||
// console.log('no!!!')
|
|
||||||
// setAudioPosition(value)
|
// setAudioPosition(value)
|
||||||
// }}
|
// }}
|
||||||
enabled={false} // Bug in above sliding actions
|
enabled={false} // Bug in above sliding actions
|
||||||
|
@ -59,7 +59,9 @@ const AttachmentUnsupported: React.FC<Props> = ({
|
|||||||
content={t('shared.attachment.unsupported.button')}
|
content={t('shared.attachment.unsupported.button')}
|
||||||
size='S'
|
size='S'
|
||||||
overlay
|
overlay
|
||||||
onPress={async () => await openLink(attachment.remote_url!)}
|
onPress={async () =>
|
||||||
|
attachment.remote_url && (await openLink(attachment.remote_url))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
server: {
|
server: {
|
||||||
textInput: { placeholder: "Instance' domain" },
|
textInput: { placeholder: "Instance' domain" },
|
||||||
|
privateInstance: 'Private instance, peeping not allowed',
|
||||||
button: {
|
button: {
|
||||||
local: 'Login',
|
local: 'Login',
|
||||||
remote: 'Peep'
|
remote: 'Peep'
|
||||||
|
@ -3,6 +3,7 @@ export default {
|
|||||||
expanded: {
|
expanded: {
|
||||||
true: 'Fold {{hint}}',
|
true: 'Fold {{hint}}',
|
||||||
false: 'Expand {{hint}}'
|
false: 'Expand {{hint}}'
|
||||||
}
|
},
|
||||||
|
defaultHint: 'article'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export default {
|
|||||||
heading: '$t(sharedAnnouncements:heading)',
|
heading: '$t(sharedAnnouncements:heading)',
|
||||||
content: {
|
content: {
|
||||||
unread: '{{amount}} unread',
|
unread: '{{amount}} unread',
|
||||||
read: 'all read'
|
read: 'All read'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
server: {
|
server: {
|
||||||
textInput: { placeholder: '输入社区服务器地址' },
|
textInput: { placeholder: '输入社区服务器地址' },
|
||||||
|
privateInstance: '非公开社区, 不能围观',
|
||||||
button: {
|
button: {
|
||||||
local: '登录',
|
local: '登录',
|
||||||
remote: '围观'
|
remote: '围观'
|
||||||
|
@ -3,6 +3,7 @@ export default {
|
|||||||
expanded: {
|
expanded: {
|
||||||
true: '折叠{{hint}}',
|
true: '折叠{{hint}}',
|
||||||
false: '展开{{hint}}'
|
false: '展开{{hint}}'
|
||||||
}
|
},
|
||||||
|
defaultHint: '全文'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
import Timelines from '@components/Timelines'
|
import Timelines from '@components/Timelines'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const ScreenLocal: React.FC = () => {
|
const ScreenLocal = React.memo(
|
||||||
const { t } = useTranslation()
|
() => {
|
||||||
|
return <Timelines name='Local' />
|
||||||
return (
|
},
|
||||||
<Timelines
|
() => true
|
||||||
name='Local'
|
)
|
||||||
content={[
|
|
||||||
{ title: t('local:heading.segments.left'), page: 'Following' },
|
|
||||||
{ title: t('local:heading.segments.right'), page: 'Local' }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScreenLocal
|
export default ScreenLocal
|
||||||
|
@ -5,7 +5,7 @@ import React, { useMemo } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const Collections: React.FC = () => {
|
const Collections: React.FC = () => {
|
||||||
const { t } = useTranslation('meRoot')
|
const { t, i18n } = useTranslation('meRoot')
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const { data, isFetching } = useAnnouncementQuery({ showAll: true })
|
const { data, isFetching } = useAnnouncementQuery({ showAll: true })
|
||||||
@ -19,7 +19,7 @@ const Collections: React.FC = () => {
|
|||||||
return t('content.collections.announcements.content.read')
|
return t('content.collections.announcements.content.read')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data, i18n.language])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
|
@ -125,10 +125,10 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: t('content.language.heading'),
|
title: t('content.language.heading'),
|
||||||
options,
|
options,
|
||||||
cancelButtonIndex: i18n.languages.length
|
cancelButtonIndex: options.length - 1
|
||||||
},
|
},
|
||||||
buttonIndex => {
|
buttonIndex => {
|
||||||
if (buttonIndex < i18n.languages.length) {
|
if (buttonIndex < options.length) {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
dispatch(changeLanguage(availableLanguages[buttonIndex]))
|
dispatch(changeLanguage(availableLanguages[buttonIndex]))
|
||||||
i18n.changeLanguage(availableLanguages[buttonIndex])
|
i18n.changeLanguage(availableLanguages[buttonIndex])
|
||||||
|
@ -50,7 +50,7 @@ const AccountButton: React.FC<Props> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
content={`@${data?.acct || '...'}@${instance.url}`}
|
content={`@${data?.acct || '...'}@${instance.uri}${disabled ? ' ✓' : ''}`}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
dispatch(localUpdateActiveIndex(index))
|
dispatch(localUpdateActiveIndex(index))
|
||||||
queryClient.clear()
|
queryClient.clear()
|
||||||
@ -125,7 +125,8 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: StyleConstants.Spacing.M
|
marginTop: StyleConstants.Spacing.M
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
marginBottom: StyleConstants.Spacing.M
|
marginBottom: StyleConstants.Spacing.M,
|
||||||
|
marginRight: StyleConstants.Spacing.M
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,23 +1,11 @@
|
|||||||
import Timelines from '@components/Timelines'
|
import Timelines from '@components/Timelines'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const ScreenPublic: React.FC = () => {
|
const ScreenPublic = React.memo(
|
||||||
const { t } = useTranslation()
|
() => {
|
||||||
|
return <Timelines name='Public' />
|
||||||
return (
|
},
|
||||||
<Timelines
|
() => true
|
||||||
name='Public'
|
)
|
||||||
content={[
|
|
||||||
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
|
|
||||||
{
|
|
||||||
title: t('public:heading.segments.right'),
|
|
||||||
page: 'RemotePublic',
|
|
||||||
remote: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScreenPublic
|
export default ScreenPublic
|
||||||
|
@ -47,7 +47,7 @@ const AccountInformationCreated = forwardRef<ShimmerPlaceholder, Props>(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('content.created_at', {
|
{t('content.created_at', {
|
||||||
date: new Date(account?.created_at!).toLocaleDateString(
|
date: new Date(account?.created_at || '').toLocaleDateString(
|
||||||
i18n.language,
|
i18n.language,
|
||||||
{
|
{
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
@ -22,6 +22,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
|
|||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
import * as Sentry from 'sentry-expo'
|
||||||
import ComposeEditAttachment from './Compose/EditAttachment'
|
import ComposeEditAttachment from './Compose/EditAttachment'
|
||||||
import ComposeContext from './Compose/utils/createContext'
|
import ComposeContext from './Compose/utils/createContext'
|
||||||
import composeInitialState from './Compose/utils/initialState'
|
import composeInitialState from './Compose/utils/initialState'
|
||||||
@ -145,7 +146,7 @@ const Compose: React.FC<SharedComposeProp> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[totalTextCount]
|
[totalTextCount, composeState]
|
||||||
)
|
)
|
||||||
const headerCenter = useCallback(
|
const headerCenter = useCallback(
|
||||||
() => (
|
() => (
|
||||||
@ -190,7 +191,8 @@ const Compose: React.FC<SharedComposeProp> = ({
|
|||||||
}
|
}
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(error => {
|
||||||
|
Sentry.Native.captureException(error)
|
||||||
haptics('Error')
|
haptics('Error')
|
||||||
composeDispatch({ type: 'posting', payload: false })
|
composeDispatch({ type: 'posting', payload: false })
|
||||||
Alert.alert(t('heading.right.alert.title'), undefined, [
|
Alert.alert(t('heading.right.alert.title'), undefined, [
|
||||||
@ -201,7 +203,13 @@ const Compose: React.FC<SharedComposeProp> = ({
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
loading={composeState.posting}
|
loading={composeState.posting}
|
||||||
disabled={composeState.text.raw.length < 1 || totalTextCount > 500}
|
disabled={
|
||||||
|
composeState.text.raw.length < 1 ||
|
||||||
|
totalTextCount > 500 ||
|
||||||
|
(composeState.attachments.uploads.length > 0 &&
|
||||||
|
composeState.attachments.uploads.filter(upload => upload.uploading)
|
||||||
|
.length > 0)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[totalTextCount, composeState]
|
[totalTextCount, composeState]
|
||||||
|
@ -54,10 +54,11 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (theAttachment.type === 'image') {
|
if (theAttachment.type === 'image') {
|
||||||
if (focus.current.x !== 0 || focus.current.y !== 0) {
|
if (focus.current.x !== 0 || focus.current.y !== 0) {
|
||||||
theAttachment.meta!.focus = {
|
theAttachment.meta &&
|
||||||
x: focus.current.x > 1 ? 1 : focus.current.x,
|
(theAttachment.meta.focus = {
|
||||||
y: focus.current.y > 1 ? 1 : focus.current.y
|
x: focus.current.x > 1 ? 1 : focus.current.x,
|
||||||
}
|
y: focus.current.y > 1 ? 1 : focus.current.y
|
||||||
|
})
|
||||||
needUpdate = true
|
needUpdate = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,20 +28,21 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const { composeState } = useContext(ComposeContext)
|
const { composeState } = useContext(ComposeContext)
|
||||||
const theAttachmentRemote = composeState.attachments.uploads[index].remote!
|
const theAttachmentRemote = composeState.attachments.uploads[index].remote
|
||||||
const theAttachmentLocal = composeState.attachments.uploads[index].local!
|
const theAttachmentLocal = composeState.attachments.uploads[index].local
|
||||||
|
|
||||||
const imageWidthBase =
|
const imageWidthBase =
|
||||||
theAttachmentRemote.meta?.original?.aspect < 1
|
theAttachmentRemote?.meta?.original?.aspect < 1
|
||||||
? Dimensions.get('screen').width *
|
? Dimensions.get('screen').width *
|
||||||
theAttachmentRemote.meta?.original?.aspect
|
theAttachmentRemote?.meta?.original?.aspect
|
||||||
: Dimensions.get('screen').width
|
: Dimensions.get('screen').width
|
||||||
const padding = (Dimensions.get('screen').width - imageWidthBase) / 2
|
const padding = (Dimensions.get('screen').width - imageWidthBase) / 2
|
||||||
const imageDimensionis = {
|
const imageDimensionis = {
|
||||||
width: imageWidthBase,
|
width: imageWidthBase,
|
||||||
height:
|
height:
|
||||||
imageWidthBase /
|
imageWidthBase /
|
||||||
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.original?.aspect!
|
((theAttachmentRemote as Mastodon.AttachmentImage).meta?.original
|
||||||
|
?.aspect || 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const panX = useSharedValue(
|
const panX = useSharedValue(
|
||||||
@ -115,7 +116,7 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
|
|||||||
height: imageDimensionis.height
|
height: imageDimensionis.height
|
||||||
}}
|
}}
|
||||||
source={{
|
source={{
|
||||||
uri: theAttachmentLocal.uri || theAttachmentRemote.preview_url
|
uri: theAttachmentLocal?.uri || theAttachmentRemote?.preview_url
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||||
|
@ -32,31 +32,33 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({
|
|||||||
const { t } = useTranslation('sharedCompose')
|
const { t } = useTranslation('sharedCompose')
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { composeState } = useContext(ComposeContext)
|
const { composeState } = useContext(ComposeContext)
|
||||||
const theAttachment = composeState.attachments.uploads[index].remote!
|
const theAttachment = composeState.attachments.uploads[index].remote
|
||||||
|
|
||||||
const mediaDisplay = useMemo(() => {
|
const mediaDisplay = useMemo(() => {
|
||||||
switch (theAttachment.type) {
|
if (theAttachment) {
|
||||||
case 'image':
|
switch (theAttachment.type) {
|
||||||
return <ComposeEditAttachmentImage index={index} focus={focus} />
|
case 'image':
|
||||||
case 'video':
|
return <ComposeEditAttachmentImage index={index} focus={focus} />
|
||||||
case 'gifv':
|
case 'video':
|
||||||
const video = composeState.attachments.uploads[index]
|
case 'gifv':
|
||||||
return (
|
const video = composeState.attachments.uploads[index]
|
||||||
<AttachmentVideo
|
return (
|
||||||
total={1}
|
<AttachmentVideo
|
||||||
index={0}
|
total={1}
|
||||||
sensitiveShown={false}
|
index={0}
|
||||||
video={
|
sensitiveShown={false}
|
||||||
video.local
|
video={
|
||||||
? ({
|
video.local
|
||||||
url: video.local.uri,
|
? ({
|
||||||
preview_url: video.local.local_thumbnail,
|
url: video.local.uri,
|
||||||
blurhash: video.remote?.blurhash
|
preview_url: video.local.local_thumbnail,
|
||||||
} as Mastodon.AttachmentVideo)
|
blurhash: video.remote?.blurhash
|
||||||
: (video.remote! as Mastodon.AttachmentVideo)
|
} as Mastodon.AttachmentVideo)
|
||||||
}
|
: (video.remote as Mastodon.AttachmentVideo)
|
||||||
/>
|
}
|
||||||
)
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Modal, StyleSheet, View } from 'react-native'
|
import { Modal, View } from 'react-native'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
import { Chase } from 'react-native-animated-spinkit'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
|
|
||||||
const ComposePosting = React.memo(
|
const ComposePosting = React.memo(
|
||||||
() => {
|
() => {
|
||||||
@ -16,15 +14,7 @@ const ComposePosting = React.memo(
|
|||||||
animationType='fade'
|
animationType='fade'
|
||||||
visible={composeState.posting}
|
visible={composeState.posting}
|
||||||
children={
|
children={
|
||||||
<View
|
<View style={{ flex: 1, backgroundColor: theme.backgroundOverlay }} />
|
||||||
style={[styles.base, { backgroundColor: theme.backgroundOverlay }]}
|
|
||||||
children={
|
|
||||||
<Chase
|
|
||||||
size={StyleConstants.Font.Size.L * 2}
|
|
||||||
color={theme.primaryOverlay}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -32,12 +22,4 @@ const ComposePosting = React.memo(
|
|||||||
() => true
|
() => true
|
||||||
)
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
base: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ComposePosting
|
export default ComposePosting
|
||||||
|
@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { forEach, groupBy, sortBy } from 'lodash'
|
import { forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||||
import { View, FlatList, StyleSheet } from 'react-native'
|
import { FlatList, Image, StyleSheet, View } from 'react-native'
|
||||||
import { Chase } from 'react-native-animated-spinkit'
|
import { Chase } from 'react-native-animated-spinkit'
|
||||||
import ComposeActions from './Root/Actions'
|
import ComposeActions from './Root/Actions'
|
||||||
import ComposePosting from './Posting'
|
import ComposePosting from './Posting'
|
||||||
@ -53,6 +53,14 @@ const ComposeRoot: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [emojisData])
|
}, [emojisData])
|
||||||
|
useEffect(() => {
|
||||||
|
if (emojisData && emojisData.length) {
|
||||||
|
// Prefetch first batch of emojis for faster loading experience
|
||||||
|
emojisData.slice(0, 40).forEach(emoji => {
|
||||||
|
Image.prefetch(emoji.url)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [emojisData])
|
||||||
|
|
||||||
const listEmpty = useMemo(() => {
|
const listEmpty = useMemo(() => {
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
|
@ -250,7 +250,7 @@ const ComposeAttachments: React.FC = () => {
|
|||||||
keyboardShouldPersistTaps='handled'
|
keyboardShouldPersistTaps='handled'
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
data={composeState.attachments.uploads}
|
data={composeState.attachments.uploads}
|
||||||
keyExtractor={item => item.local!.uri || item.remote!.url}
|
keyExtractor={item => item.local?.uri || item.remote?.url}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
composeState.attachments.uploads.length < 4 ? listFooter : null
|
composeState.attachments.uploads.length < 4 ? listFooter : null
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ const ComposeEmojis: React.FC = () => {
|
|||||||
<SectionList
|
<SectionList
|
||||||
horizontal
|
horizontal
|
||||||
keyboardShouldPersistTaps='handled'
|
keyboardShouldPersistTaps='handled'
|
||||||
sections={composeState.emoji.emojis!}
|
sections={composeState.emoji.emojis || []}
|
||||||
keyExtractor={item => item.shortcode}
|
keyExtractor={item => item.shortcode}
|
||||||
renderSectionHeader={listHeader}
|
renderSectionHeader={listHeader}
|
||||||
renderItem={listItem}
|
renderItem={listItem}
|
||||||
|
@ -69,8 +69,8 @@ const formatText = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const changedTag = differenceWith(tags, prevTags, isEqual)
|
const changedTag = differenceWith(tags, prevTags, isEqual)
|
||||||
if (changedTag.length && !disableDebounce) {
|
if (changedTag.length > 0 && !disableDebounce) {
|
||||||
if (changedTag[0]!.type !== 'url') {
|
if (changedTag[0]?.type !== 'url') {
|
||||||
debouncedSuggestions(composeDispatch, changedTag[0])
|
debouncedSuggestions(composeDispatch, changedTag[0])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -83,30 +83,33 @@ const formatText = ({
|
|||||||
let contentLength: number = 0
|
let contentLength: number = 0
|
||||||
const children = []
|
const children = []
|
||||||
tags.forEach((tag, index) => {
|
tags.forEach((tag, index) => {
|
||||||
const prev = _content.substr(0, tag!.offset - pointer)
|
if (tag) {
|
||||||
const main = _content.substr(tag!.offset - pointer, tag!.length)
|
const prev = _content.substr(0, tag.offset - pointer)
|
||||||
const next = _content.substr(tag!.offset - pointer + tag!.length)
|
const main = _content.substr(tag.offset - pointer, tag.length)
|
||||||
children.push(prev)
|
const next = _content.substr(tag.offset - pointer + tag.length)
|
||||||
contentLength = contentLength + prev.length
|
children.push(prev)
|
||||||
children.push(<TagText key={index} text={main} />)
|
contentLength = contentLength + prev.length
|
||||||
switch (tag!.type) {
|
children.push(<TagText key={index} text={main} />)
|
||||||
case 'url':
|
switch (tag.type) {
|
||||||
contentLength = contentLength + 23
|
case 'url':
|
||||||
break
|
contentLength = contentLength + 23
|
||||||
case 'accounts':
|
break
|
||||||
if (main.match(/@/g)!.length > 1) {
|
case 'accounts':
|
||||||
contentLength =
|
const theMatch = main.match(/@/g)
|
||||||
contentLength + main.split(new RegExp('(@.*?)@'))[1].length
|
if (theMatch && theMatch.length > 1) {
|
||||||
} else {
|
contentLength =
|
||||||
|
contentLength + main.split(new RegExp('(@.*?)@'))[1].length
|
||||||
|
} else {
|
||||||
|
contentLength = contentLength + main.length
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'hashtags':
|
||||||
contentLength = contentLength + main.length
|
contentLength = contentLength + main.length
|
||||||
}
|
break
|
||||||
break
|
}
|
||||||
case 'hashtags':
|
_content = next
|
||||||
contentLength = contentLength + main.length
|
pointer = pointer + prev.length + tag.length
|
||||||
break
|
|
||||||
}
|
}
|
||||||
_content = next
|
|
||||||
pointer = pointer + prev.length + tag!.length
|
|
||||||
})
|
})
|
||||||
children.push(_content)
|
children.push(_content)
|
||||||
contentLength = contentLength + _content.length
|
contentLength = contentLength + _content.length
|
||||||
|
@ -9,7 +9,7 @@ const composePost = async (
|
|||||||
) => {
|
) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
if (params?.type === 'conversation' || params?.type === 'reply') {
|
if (params?.type === 'reply') {
|
||||||
formData.append('in_reply_to_id', composeState.replyToStatus!.id)
|
formData.append('in_reply_to_id', composeState.replyToStatus!.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,9 +20,9 @@ const composePost = async (
|
|||||||
formData.append('status', composeState.text.raw)
|
formData.append('status', composeState.text.raw)
|
||||||
|
|
||||||
if (composeState.poll.active) {
|
if (composeState.poll.active) {
|
||||||
Object.values(composeState.poll.options)
|
Object.values(composeState.poll.options).forEach(
|
||||||
.filter(e => e?.length)
|
e => e && e.length && formData.append('poll[options][]', e)
|
||||||
.forEach(e => formData.append('poll[options][]', e!))
|
)
|
||||||
formData.append('poll[expires_in]', composeState.poll.expire)
|
formData.append('poll[expires_in]', composeState.poll.expire)
|
||||||
formData.append('poll[multiple]', composeState.poll.multiple.toString())
|
formData.append('poll[multiple]', composeState.poll.multiple.toString())
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ const composeReducer = (
|
|||||||
attachments: {
|
attachments: {
|
||||||
...state.attachments,
|
...state.attachments,
|
||||||
uploads: state.attachments.uploads.filter(
|
uploads: state.attachments.uploads.filter(
|
||||||
upload => upload.remote!.id !== action.payload
|
upload => upload.remote?.id !== action.payload
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
|||||||
const { instanceDomain } = queryKey[1]
|
const { instanceDomain } = queryKey[1]
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('client_name', 'tooot📱')
|
formData.append('client_name', 'tooot')
|
||||||
formData.append('website', 'https://tooot.app')
|
formData.append('website', 'https://tooot.app')
|
||||||
formData.append('redirect_uris', redirectUri)
|
formData.append('redirect_uris', redirectUri)
|
||||||
formData.append('scopes', 'read write follow push')
|
formData.append('scopes', 'read write follow push')
|
||||||
|
@ -2,24 +2,56 @@ import client from '@api/client'
|
|||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
import { useQuery, UseQueryOptions } from 'react-query'
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
export type QueryKey = ['Instance', { instanceDomain?: string }]
|
export type QueryKey = [
|
||||||
|
'Instance',
|
||||||
|
{ instanceDomain?: string; checkPublic: boolean }
|
||||||
|
]
|
||||||
|
|
||||||
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
const queryFunction = async ({ queryKey }: { queryKey: QueryKey }) => {
|
||||||
const { instanceDomain } = queryKey[1]
|
const { instanceDomain, checkPublic } = queryKey[1]
|
||||||
|
|
||||||
return client<Mastodon.Instance>({
|
let res: Mastodon.Instance & { publicAllow?: boolean } = await client<
|
||||||
|
Mastodon.Instance
|
||||||
|
>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
instance: 'remote',
|
instance: 'remote',
|
||||||
instanceDomain,
|
instanceDomain,
|
||||||
url: `instance`
|
url: `instance`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (checkPublic) {
|
||||||
|
let check
|
||||||
|
try {
|
||||||
|
check = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'remote',
|
||||||
|
instanceDomain,
|
||||||
|
url: `timelines/public`
|
||||||
|
})
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (check) {
|
||||||
|
res.publicAllow = true
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
res.publicAllow = false
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const useInstanceQuery = <TData = Mastodon.Instance>({
|
const useInstanceQuery = <
|
||||||
|
TData = Mastodon.Instance & { publicAllow?: boolean }
|
||||||
|
>({
|
||||||
options,
|
options,
|
||||||
...queryKeyParams
|
...queryKeyParams
|
||||||
}: QueryKey[1] & {
|
}: QueryKey[1] & {
|
||||||
options?: UseQueryOptions<Mastodon.Instance, AxiosError, TData>
|
options?: UseQueryOptions<
|
||||||
|
Mastodon.Instance & { publicAllow?: boolean },
|
||||||
|
AxiosError,
|
||||||
|
TData
|
||||||
|
>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKey = ['Instance', { ...queryKeyParams }]
|
const queryKey: QueryKey = ['Instance', { ...queryKeyParams }]
|
||||||
return useQuery(queryKey, queryFunction, options)
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
@ -292,7 +292,7 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
|
|||||||
case 'poll':
|
case 'poll':
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
params.payload.type === 'vote' &&
|
params.payload.type === 'vote' &&
|
||||||
params.payload.options!.forEach((option, index) => {
|
params.payload.options?.forEach((option, index) => {
|
||||||
if (option) {
|
if (option) {
|
||||||
formData.append('choices[]', index.toString())
|
formData.append('choices[]', index.toString())
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { RootState } from '@root/store'
|
import { RootState } from '@root/store'
|
||||||
|
import Constants from 'expo-constants'
|
||||||
import * as StoreReview from 'expo-store-review'
|
import * as StoreReview from 'expo-store-review'
|
||||||
|
|
||||||
export const supportedLngs = ['zh-Hans', 'en']
|
export const supportedLngs = ['zh-Hans', 'en']
|
||||||
@ -37,9 +38,11 @@ const contextsSlice = createSlice({
|
|||||||
initialState: contextsInitialState as ContextsState,
|
initialState: contextsInitialState as ContextsState,
|
||||||
reducers: {
|
reducers: {
|
||||||
updateStoreReview: (state, action: PayloadAction<1>) => {
|
updateStoreReview: (state, action: PayloadAction<1>) => {
|
||||||
state.storeReview.current = state.storeReview.current + action.payload
|
if (Constants.manifest.releaseChannel === 'production') {
|
||||||
if (state.storeReview.current === state.storeReview.context) {
|
state.storeReview.current = state.storeReview.current + action.payload
|
||||||
StoreReview.isAvailableAsync().then(() => StoreReview.requestReview())
|
if (state.storeReview.current === state.storeReview.context) {
|
||||||
|
StoreReview.isAvailableAsync().then(() => StoreReview.requestReview())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePublicRemoteNotice: (state, action: PayloadAction<1>) => {
|
updatePublicRemoteNotice: (state, action: PayloadAction<1>) => {
|
||||||
|
@ -259,7 +259,7 @@ export const getLocalUrl = ({ instances: { local } }: RootState) =>
|
|||||||
: undefined
|
: undefined
|
||||||
export const getLocalUri = ({ instances: { local } }: RootState) =>
|
export const getLocalUri = ({ instances: { local } }: RootState) =>
|
||||||
local.activeIndex !== null
|
local.activeIndex !== null
|
||||||
? local.instances[local.activeIndex].url
|
? local.instances[local.activeIndex].uri
|
||||||
: undefined
|
: undefined
|
||||||
export const getLocalAccount = ({ instances: { local } }: RootState) =>
|
export const getLocalAccount = ({ instances: { local } }: RootState) =>
|
||||||
local.activeIndex !== null
|
local.activeIndex !== null
|
||||||
|
Loading…
x
Reference in New Issue
Block a user