mirror of
https://github.com/tooot-app/app
synced 2025-02-18 04:40:57 +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'
|
||||
},
|
||||
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/*'],
|
||||
extra: {
|
||||
sentryDSN: process.env.SENTRY_DSN
|
||||
},
|
||||
hooks: {
|
||||
postPublish: [
|
||||
{
|
||||
@ -51,8 +35,24 @@ export default (): ExpoConfig => ({
|
||||
}
|
||||
]
|
||||
},
|
||||
extra: {
|
||||
sentryDSN: process.env.SENTRY_DSN
|
||||
ios: {
|
||||
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: {
|
||||
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
|
||||
|
||||
let domain = null
|
||||
let token = null
|
||||
if (instance === 'remote') {
|
||||
domain = instanceDomain || state.remote.url
|
||||
} else {
|
||||
if (theLocalIndex !== null && state.local.instances[theLocalIndex]) {
|
||||
domain = state.local.instances[theLocalIndex].url
|
||||
token = state.local.instances[theLocalIndex].token
|
||||
} else {
|
||||
console.error(
|
||||
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
|
||||
@ -69,8 +71,8 @@ const client = async <T = unknown>({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
...(instance === 'local' && {
|
||||
Authorization: `Bearer ${state.local!.instances[theLocalIndex!].token}`
|
||||
...(token && {
|
||||
Authorization: `Bearer ${token}`
|
||||
})
|
||||
},
|
||||
...(body && { data: body }),
|
||||
|
@ -10,7 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as Linking from 'expo-linking'
|
||||
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 { Alert, Image, StyleSheet, Text, TextInput, View } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
@ -42,6 +42,7 @@ const ComponentInstance: React.FC<Props> = ({
|
||||
|
||||
const instanceQuery = useInstanceQuery({
|
||||
instanceDomain,
|
||||
checkPublic: type === 'remote',
|
||||
options: { enabled: false, retry: false }
|
||||
})
|
||||
const appsQuery = useAppsQuery({
|
||||
@ -170,7 +171,12 @@ const ComponentInstance: React.FC<Props> = ({
|
||||
styles.textInput,
|
||||
{
|
||||
color: theme.primary,
|
||||
borderBottomColor: theme.border
|
||||
borderBottomColor:
|
||||
type === 'remote' &&
|
||||
instanceQuery.data &&
|
||||
!instanceQuery.data.publicAllow
|
||||
? theme.red
|
||||
: theme.border
|
||||
}
|
||||
]}
|
||||
onChangeText={onChangeText}
|
||||
@ -188,10 +194,20 @@ const ComponentInstance: React.FC<Props> = ({
|
||||
type='text'
|
||||
content={buttonContent}
|
||||
onPress={processUpdate}
|
||||
disabled={!instanceQuery.data?.uri}
|
||||
disabled={
|
||||
!instanceQuery.data?.uri ||
|
||||
(type === 'remote' && !instanceQuery.data.publicAllow)
|
||||
}
|
||||
loading={instanceQuery.isFetching || appsQuery.isFetching}
|
||||
/>
|
||||
</View>
|
||||
{type === 'remote' &&
|
||||
instanceQuery.data &&
|
||||
!instanceQuery.data.publicAllow ? (
|
||||
<Text style={[styles.privateInstance, { color: theme.red }]}>
|
||||
{t('server.privateInstance')}
|
||||
</Text>
|
||||
) : null}
|
||||
<View>
|
||||
<InstanceInfo
|
||||
visible={instanceQuery.data?.title !== undefined}
|
||||
@ -278,6 +294,12 @@ const styles = StyleSheet.create({
|
||||
...StyleConstants.FontStyle.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
},
|
||||
privateInstance: {
|
||||
...StyleConstants.FontStyle.S,
|
||||
fontWeight: StyleConstants.Font.Weight.Bold,
|
||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginTop: StyleConstants.Spacing.XS
|
||||
},
|
||||
instanceStats: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
import React, { useMemo } from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Image } from 'react-native-expo-image-cache'
|
||||
|
||||
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
|
||||
@ -19,18 +19,26 @@ const ParseEmojis: React.FC<Props> = ({
|
||||
size = 'M',
|
||||
fontBold = false
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
color: theme.primary,
|
||||
...StyleConstants.FontStyle[size],
|
||||
...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold })
|
||||
},
|
||||
image: {
|
||||
width: StyleConstants.Font.Size[size],
|
||||
height: StyleConstants.Font.Size[size]
|
||||
}
|
||||
})
|
||||
const { mode, theme } = useTheme()
|
||||
const styles = useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
text: {
|
||||
color: theme.primary,
|
||||
...StyleConstants.FontStyle[size],
|
||||
...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold })
|
||||
},
|
||||
imageContainer: {
|
||||
paddingVertical:
|
||||
(StyleConstants.Font.LineHeight[size] -
|
||||
StyleConstants.Font.Size[size]) /
|
||||
3
|
||||
},
|
||||
image: {
|
||||
width: StyleConstants.Font.Size[size],
|
||||
height: StyleConstants.Font.Size[size]
|
||||
}
|
||||
})
|
||||
}, [mode])
|
||||
|
||||
return (
|
||||
<Text style={styles.text}>
|
||||
@ -50,7 +58,13 @@ const ParseEmojis: React.FC<Props> = ({
|
||||
<Text key={i}>
|
||||
{/* When emoji starts a paragraph, lineHeight will break */}
|
||||
{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>
|
||||
)
|
||||
} else {
|
||||
|
@ -154,12 +154,16 @@ const ParseHTML: React.FC<Props> = ({
|
||||
tags,
|
||||
showFullLink = false,
|
||||
numberOfLines = 10,
|
||||
expandHint = '全文',
|
||||
expandHint,
|
||||
disableDetails = false
|
||||
}) => {
|
||||
const navigation = useNavigation()
|
||||
const route = useRoute()
|
||||
const { theme } = useTheme()
|
||||
const { t, i18n } = useTranslation('componentParse')
|
||||
if (!expandHint) {
|
||||
expandHint = t('HTML.defaultHint')
|
||||
}
|
||||
|
||||
const renderNodeCallback = useCallback(
|
||||
(node, index) =>
|
||||
@ -261,7 +265,7 @@ const ParseHTML: React.FC<Props> = ({
|
||||
</View>
|
||||
)
|
||||
},
|
||||
[theme]
|
||||
[theme, i18n.language]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -6,7 +6,8 @@ import sharedScreens from '@screens/Shared/sharedScreens'
|
||||
import { getLocalActiveIndex, getRemoteUrl } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
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 { TabView } from 'react-native-tab-view'
|
||||
import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter'
|
||||
@ -18,22 +19,32 @@ const Stack = createNativeStackNavigator<
|
||||
|
||||
export interface Props {
|
||||
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 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 { mode } = useTheme()
|
||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||
const publicDomain = useSelector(getRemoteUrl)
|
||||
const [segment, setSegment] = useState(0)
|
||||
|
||||
const onPressSearch = useCallback(() => {
|
||||
navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' })
|
||||
}, [])
|
||||
|
||||
const routes = content
|
||||
const routes = mapNameToContent[name]
|
||||
.filter(p => (localActiveIndex !== null ? true : p.page === 'RemotePublic'))
|
||||
.map(p => ({ key: p.page }))
|
||||
|
||||
@ -54,39 +65,37 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
[localActiveIndex]
|
||||
)
|
||||
|
||||
const { mode } = useTheme()
|
||||
const [segment, setSegment] = useState(0)
|
||||
const screenOptions = useMemo(() => {
|
||||
if (localActiveIndex === null) {
|
||||
if (name === 'Public') {
|
||||
return {
|
||||
headerTitle: publicDomain,
|
||||
headerTitle: remoteUrl,
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => <HeaderCenter content={publicDomain} />
|
||||
headerCenter: () => <HeaderCenter content={remoteUrl} />
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
headerCenter: () => (
|
||||
<View style={styles.segmentsContainer}>
|
||||
<SegmentedControl
|
||||
appearance={mode}
|
||||
values={[
|
||||
content[0].title,
|
||||
content[1].remote ? remoteUrl : content[1].title
|
||||
]}
|
||||
selectedIndex={segment}
|
||||
onChange={({ nativeEvent }) =>
|
||||
setSegment(nativeEvent.selectedSegmentIndex)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<SegmentedControl
|
||||
appearance={mode}
|
||||
values={mapNameToContent[name].map(p => p.title)}
|
||||
selectedIndex={segment}
|
||||
onChange={({ nativeEvent }) =>
|
||||
setSegment(nativeEvent.selectedSegmentIndex)
|
||||
}
|
||||
style={styles.segmentsContainer}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderRight content='Search' onPress={onPressSearch} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [localActiveIndex, mode, segment])
|
||||
}, [localActiveIndex, mode, segment, i18n.language])
|
||||
|
||||
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 TimelineHeader from '@components/Timelines/Timeline/Header'
|
||||
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 { StyleConstants } from '@utils/styles/constants'
|
||||
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 { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { findIndex } from 'lodash'
|
||||
import { InfiniteData, useQueryClient } from 'react-query'
|
||||
import { getPublicRemoteNotice } from '@utils/slices/contextsSlice'
|
||||
|
||||
export interface Props {
|
||||
@ -58,25 +57,12 @@ const Timeline: React.FC<Props> = ({
|
||||
isSuccess,
|
||||
isFetching,
|
||||
isLoading,
|
||||
hasPreviousPage,
|
||||
fetchPreviousPage,
|
||||
isFetchingPreviousPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useTimelineQuery({
|
||||
...queryKeyParams,
|
||||
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 => {
|
||||
return Array.isArray(lastPage) && lastPage.length
|
||||
? {
|
||||
@ -94,16 +80,23 @@ const Timeline: React.FC<Props> = ({
|
||||
|
||||
// Clear unread notification badge
|
||||
const dispatch = useDispatch()
|
||||
const navigation = useNavigation()
|
||||
useEffect(() => {
|
||||
if (page === 'Notifications' && flattenData.length) {
|
||||
dispatch(
|
||||
localUpdateNotification({
|
||||
unread: false,
|
||||
latestTime: (flattenData[0] as Mastodon.Notification).created_at
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [flattenData])
|
||||
const unsubscribe = navigation.addListener('focus', props => {
|
||||
if (props.target && props.target.includes('Screen-Notifications-Root')) {
|
||||
if (flattenData.length) {
|
||||
dispatch(
|
||||
localUpdateNotification({
|
||||
unread: false,
|
||||
latestTime: (flattenData[0] as Mastodon.Notification).created_at
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [navigation, flattenData])
|
||||
|
||||
const flRef = useRef<FlatList<any>>(null)
|
||||
useEffect(() => {
|
||||
@ -166,43 +159,29 @@ const Timeline: React.FC<Props> = ({
|
||||
[hasNextPage]
|
||||
)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const isSwipeDown = useRef(false)
|
||||
const refreshControl = useMemo(
|
||||
() => (
|
||||
<RefreshControl
|
||||
{...(Platform.OS === 'android' && { enabled: true })}
|
||||
refreshing={
|
||||
isFetchingPreviousPage ||
|
||||
(isFetching && !isFetchingNextPage && !isLoading)
|
||||
isSwipeDown.current && isFetching && !isFetchingNextPage && !isLoading
|
||||
}
|
||||
onRefresh={() => {
|
||||
// if (hasPreviousPage) {
|
||||
// fetchPreviousPage()
|
||||
// } else {
|
||||
// queryClient.setQueryData<InfiniteData<any> | undefined>(
|
||||
// queryKey,
|
||||
// data => {
|
||||
// if (data) {
|
||||
// return {
|
||||
// pages: data.pages.slice(1),
|
||||
// pageParams: data.pageParams.slice(1)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
isSwipeDown.current = true
|
||||
refetch()
|
||||
// }
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[
|
||||
hasPreviousPage,
|
||||
isFetchingPreviousPage,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
isLoading
|
||||
]
|
||||
[isSwipeDown.current, isFetching, isFetchingNextPage, isLoading]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
isSwipeDown.current = false
|
||||
}
|
||||
}, [isFetching])
|
||||
|
||||
const onScrollToIndexFailed = useCallback(error => {
|
||||
const offset = error.averageItemLength * error.index
|
||||
flRef.current?.scrollToOffset({ offset })
|
||||
|
@ -79,43 +79,44 @@ const TimelineConversation: React.FC<Props> = ({
|
||||
</View>
|
||||
|
||||
{conversation.last_status ? (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<TimelineContent
|
||||
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}
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<TimelineContent
|
||||
status={conversation.last_status}
|
||||
highlighted={highlighted}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
status={conversation.last_status}
|
||||
reblog={false}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
status={conversation.last_status!}
|
||||
reblog={false}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
@ -111,12 +111,10 @@ const AttachmentAudio: React.FC<Props> = ({
|
||||
minimumTrackTintColor={theme.secondary}
|
||||
maximumTrackTintColor={theme.disabled}
|
||||
// onSlidingStart={() => {
|
||||
// console.log('yes!!!')
|
||||
// audioPlayer?.pauseAsync()
|
||||
// setAudioPlaying(false)
|
||||
// }}
|
||||
// onSlidingComplete={value => {
|
||||
// console.log('no!!!')
|
||||
// setAudioPosition(value)
|
||||
// }}
|
||||
enabled={false} // Bug in above sliding actions
|
||||
|
@ -59,7 +59,9 @@ const AttachmentUnsupported: React.FC<Props> = ({
|
||||
content={t('shared.attachment.unsupported.button')}
|
||||
size='S'
|
||||
overlay
|
||||
onPress={async () => await openLink(attachment.remote_url!)}
|
||||
onPress={async () =>
|
||||
attachment.remote_url && (await openLink(attachment.remote_url))
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -1,6 +1,7 @@
|
||||
export default {
|
||||
server: {
|
||||
textInput: { placeholder: "Instance' domain" },
|
||||
privateInstance: 'Private instance, peeping not allowed',
|
||||
button: {
|
||||
local: 'Login',
|
||||
remote: 'Peep'
|
||||
|
@ -3,6 +3,7 @@ export default {
|
||||
expanded: {
|
||||
true: 'Fold {{hint}}',
|
||||
false: 'Expand {{hint}}'
|
||||
}
|
||||
},
|
||||
defaultHint: 'article'
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export default {
|
||||
heading: '$t(sharedAnnouncements:heading)',
|
||||
content: {
|
||||
unread: '{{amount}} unread',
|
||||
read: 'all read'
|
||||
read: 'All read'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
export default {
|
||||
server: {
|
||||
textInput: { placeholder: '输入社区服务器地址' },
|
||||
privateInstance: '非公开社区, 不能围观',
|
||||
button: {
|
||||
local: '登录',
|
||||
remote: '围观'
|
||||
|
@ -3,6 +3,7 @@ export default {
|
||||
expanded: {
|
||||
true: '折叠{{hint}}',
|
||||
false: '展开{{hint}}'
|
||||
}
|
||||
},
|
||||
defaultHint: '全文'
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,11 @@
|
||||
import Timelines from '@components/Timelines'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ScreenLocal: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Timelines
|
||||
name='Local'
|
||||
content={[
|
||||
{ title: t('local:heading.segments.left'), page: 'Following' },
|
||||
{ title: t('local:heading.segments.right'), page: 'Local' }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const ScreenLocal = React.memo(
|
||||
() => {
|
||||
return <Timelines name='Local' />
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
export default ScreenLocal
|
||||
|
@ -5,7 +5,7 @@ import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Collections: React.FC = () => {
|
||||
const { t } = useTranslation('meRoot')
|
||||
const { t, i18n } = useTranslation('meRoot')
|
||||
const navigation = useNavigation()
|
||||
|
||||
const { data, isFetching } = useAnnouncementQuery({ showAll: true })
|
||||
@ -19,7 +19,7 @@ const Collections: React.FC = () => {
|
||||
return t('content.collections.announcements.content.read')
|
||||
}
|
||||
}
|
||||
}, [data])
|
||||
}, [data, i18n.language])
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
|
@ -125,10 +125,10 @@ const ScreenMeSettings: React.FC = () => {
|
||||
{
|
||||
title: t('content.language.heading'),
|
||||
options,
|
||||
cancelButtonIndex: i18n.languages.length
|
||||
cancelButtonIndex: options.length - 1
|
||||
},
|
||||
buttonIndex => {
|
||||
if (buttonIndex < i18n.languages.length) {
|
||||
if (buttonIndex < options.length) {
|
||||
haptics('Success')
|
||||
dispatch(changeLanguage(availableLanguages[buttonIndex]))
|
||||
i18n.changeLanguage(availableLanguages[buttonIndex])
|
||||
|
@ -50,7 +50,7 @@ const AccountButton: React.FC<Props> = ({
|
||||
disabled={disabled}
|
||||
loading={isLoading}
|
||||
style={styles.button}
|
||||
content={`@${data?.acct || '...'}@${instance.url}`}
|
||||
content={`@${data?.acct || '...'}@${instance.uri}${disabled ? ' ✓' : ''}`}
|
||||
onPress={() => {
|
||||
dispatch(localUpdateActiveIndex(index))
|
||||
queryClient.clear()
|
||||
@ -125,7 +125,8 @@ const styles = StyleSheet.create({
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
},
|
||||
button: {
|
||||
marginBottom: StyleConstants.Spacing.M
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,23 +1,11 @@
|
||||
import Timelines from '@components/Timelines'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ScreenPublic: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Timelines
|
||||
name='Public'
|
||||
content={[
|
||||
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
|
||||
{
|
||||
title: t('public:heading.segments.right'),
|
||||
page: 'RemotePublic',
|
||||
remote: true
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const ScreenPublic = React.memo(
|
||||
() => {
|
||||
return <Timelines name='Public' />
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
export default ScreenPublic
|
||||
|
@ -47,7 +47,7 @@ const AccountInformationCreated = forwardRef<ShimmerPlaceholder, Props>(
|
||||
}}
|
||||
>
|
||||
{t('content.created_at', {
|
||||
date: new Date(account?.created_at!).toLocaleDateString(
|
||||
date: new Date(account?.created_at || '').toLocaleDateString(
|
||||
i18n.language,
|
||||
{
|
||||
year: 'numeric',
|
||||
|
@ -22,6 +22,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
import ComposeEditAttachment from './Compose/EditAttachment'
|
||||
import ComposeContext from './Compose/utils/createContext'
|
||||
import composeInitialState from './Compose/utils/initialState'
|
||||
@ -145,7 +146,7 @@ const Compose: React.FC<SharedComposeProp> = ({
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[totalTextCount]
|
||||
[totalTextCount, composeState]
|
||||
)
|
||||
const headerCenter = useCallback(
|
||||
() => (
|
||||
@ -190,7 +191,8 @@ const Compose: React.FC<SharedComposeProp> = ({
|
||||
}
|
||||
navigation.goBack()
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(error => {
|
||||
Sentry.Native.captureException(error)
|
||||
haptics('Error')
|
||||
composeDispatch({ type: 'posting', payload: false })
|
||||
Alert.alert(t('heading.right.alert.title'), undefined, [
|
||||
@ -201,7 +203,13 @@ const Compose: React.FC<SharedComposeProp> = ({
|
||||
})
|
||||
}}
|
||||
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]
|
||||
|
@ -54,10 +54,11 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
||||
|
||||
if (theAttachment.type === 'image') {
|
||||
if (focus.current.x !== 0 || focus.current.y !== 0) {
|
||||
theAttachment.meta!.focus = {
|
||||
x: focus.current.x > 1 ? 1 : focus.current.x,
|
||||
y: focus.current.y > 1 ? 1 : focus.current.y
|
||||
}
|
||||
theAttachment.meta &&
|
||||
(theAttachment.meta.focus = {
|
||||
x: focus.current.x > 1 ? 1 : focus.current.x,
|
||||
y: focus.current.y > 1 ? 1 : focus.current.y
|
||||
})
|
||||
needUpdate = true
|
||||
}
|
||||
}
|
||||
|
@ -28,20 +28,21 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
const theAttachmentRemote = composeState.attachments.uploads[index].remote!
|
||||
const theAttachmentLocal = composeState.attachments.uploads[index].local!
|
||||
const theAttachmentRemote = composeState.attachments.uploads[index].remote
|
||||
const theAttachmentLocal = composeState.attachments.uploads[index].local
|
||||
|
||||
const imageWidthBase =
|
||||
theAttachmentRemote.meta?.original?.aspect < 1
|
||||
theAttachmentRemote?.meta?.original?.aspect < 1
|
||||
? Dimensions.get('screen').width *
|
||||
theAttachmentRemote.meta?.original?.aspect
|
||||
theAttachmentRemote?.meta?.original?.aspect
|
||||
: Dimensions.get('screen').width
|
||||
const padding = (Dimensions.get('screen').width - imageWidthBase) / 2
|
||||
const imageDimensionis = {
|
||||
width: imageWidthBase,
|
||||
height:
|
||||
imageWidthBase /
|
||||
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.original?.aspect!
|
||||
((theAttachmentRemote as Mastodon.AttachmentImage).meta?.original
|
||||
?.aspect || 1)
|
||||
}
|
||||
|
||||
const panX = useSharedValue(
|
||||
@ -115,7 +116,7 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
|
||||
height: imageDimensionis.height
|
||||
}}
|
||||
source={{
|
||||
uri: theAttachmentLocal.uri || theAttachmentRemote.preview_url
|
||||
uri: theAttachmentLocal?.uri || theAttachmentRemote?.preview_url
|
||||
}}
|
||||
/>
|
||||
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||
|
@ -32,31 +32,33 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({
|
||||
const { t } = useTranslation('sharedCompose')
|
||||
const { theme } = useTheme()
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
const theAttachment = composeState.attachments.uploads[index].remote!
|
||||
const theAttachment = composeState.attachments.uploads[index].remote
|
||||
|
||||
const mediaDisplay = useMemo(() => {
|
||||
switch (theAttachment.type) {
|
||||
case 'image':
|
||||
return <ComposeEditAttachmentImage index={index} focus={focus} />
|
||||
case 'video':
|
||||
case 'gifv':
|
||||
const video = composeState.attachments.uploads[index]
|
||||
return (
|
||||
<AttachmentVideo
|
||||
total={1}
|
||||
index={0}
|
||||
sensitiveShown={false}
|
||||
video={
|
||||
video.local
|
||||
? ({
|
||||
url: video.local.uri,
|
||||
preview_url: video.local.local_thumbnail,
|
||||
blurhash: video.remote?.blurhash
|
||||
} as Mastodon.AttachmentVideo)
|
||||
: (video.remote! as Mastodon.AttachmentVideo)
|
||||
}
|
||||
/>
|
||||
)
|
||||
if (theAttachment) {
|
||||
switch (theAttachment.type) {
|
||||
case 'image':
|
||||
return <ComposeEditAttachmentImage index={index} focus={focus} />
|
||||
case 'video':
|
||||
case 'gifv':
|
||||
const video = composeState.attachments.uploads[index]
|
||||
return (
|
||||
<AttachmentVideo
|
||||
total={1}
|
||||
index={0}
|
||||
sensitiveShown={false}
|
||||
video={
|
||||
video.local
|
||||
? ({
|
||||
url: video.local.uri,
|
||||
preview_url: video.local.local_thumbnail,
|
||||
blurhash: video.remote?.blurhash
|
||||
} as Mastodon.AttachmentVideo)
|
||||
: (video.remote as Mastodon.AttachmentVideo)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [])
|
||||
|
@ -1,9 +1,7 @@
|
||||
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 ComposeContext from './utils/createContext'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
|
||||
const ComposePosting = React.memo(
|
||||
() => {
|
||||
@ -16,15 +14,7 @@ const ComposePosting = React.memo(
|
||||
animationType='fade'
|
||||
visible={composeState.posting}
|
||||
children={
|
||||
<View
|
||||
style={[styles.base, { backgroundColor: theme.backgroundOverlay }]}
|
||||
children={
|
||||
<Chase
|
||||
size={StyleConstants.Font.Size.L * 2}
|
||||
color={theme.primaryOverlay}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<View style={{ flex: 1, backgroundColor: theme.backgroundOverlay }} />
|
||||
}
|
||||
/>
|
||||
)
|
||||
@ -32,12 +22,4 @@ const ComposePosting = React.memo(
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default ComposePosting
|
||||
|
@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
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 ComposeActions from './Root/Actions'
|
||||
import ComposePosting from './Posting'
|
||||
@ -53,6 +53,14 @@ const ComposeRoot: React.FC = () => {
|
||||
})
|
||||
}
|
||||
}, [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(() => {
|
||||
if (isFetching) {
|
||||
|
@ -250,7 +250,7 @@ const ComposeAttachments: React.FC = () => {
|
||||
keyboardShouldPersistTaps='handled'
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={composeState.attachments.uploads}
|
||||
keyExtractor={item => item.local!.uri || item.remote!.url}
|
||||
keyExtractor={item => item.local?.uri || item.remote?.url}
|
||||
ListFooterComponent={
|
||||
composeState.attachments.uploads.length < 4 ? listFooter : null
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ const ComposeEmojis: React.FC = () => {
|
||||
<SectionList
|
||||
horizontal
|
||||
keyboardShouldPersistTaps='handled'
|
||||
sections={composeState.emoji.emojis!}
|
||||
sections={composeState.emoji.emojis || []}
|
||||
keyExtractor={item => item.shortcode}
|
||||
renderSectionHeader={listHeader}
|
||||
renderItem={listItem}
|
||||
|
@ -69,8 +69,8 @@ const formatText = ({
|
||||
})
|
||||
|
||||
const changedTag = differenceWith(tags, prevTags, isEqual)
|
||||
if (changedTag.length && !disableDebounce) {
|
||||
if (changedTag[0]!.type !== 'url') {
|
||||
if (changedTag.length > 0 && !disableDebounce) {
|
||||
if (changedTag[0]?.type !== 'url') {
|
||||
debouncedSuggestions(composeDispatch, changedTag[0])
|
||||
}
|
||||
} else {
|
||||
@ -83,30 +83,33 @@ const formatText = ({
|
||||
let contentLength: number = 0
|
||||
const children = []
|
||||
tags.forEach((tag, index) => {
|
||||
const prev = _content.substr(0, tag!.offset - pointer)
|
||||
const main = _content.substr(tag!.offset - pointer, tag!.length)
|
||||
const next = _content.substr(tag!.offset - pointer + tag!.length)
|
||||
children.push(prev)
|
||||
contentLength = contentLength + prev.length
|
||||
children.push(<TagText key={index} text={main} />)
|
||||
switch (tag!.type) {
|
||||
case 'url':
|
||||
contentLength = contentLength + 23
|
||||
break
|
||||
case 'accounts':
|
||||
if (main.match(/@/g)!.length > 1) {
|
||||
contentLength =
|
||||
contentLength + main.split(new RegExp('(@.*?)@'))[1].length
|
||||
} else {
|
||||
if (tag) {
|
||||
const prev = _content.substr(0, tag.offset - pointer)
|
||||
const main = _content.substr(tag.offset - pointer, tag.length)
|
||||
const next = _content.substr(tag.offset - pointer + tag.length)
|
||||
children.push(prev)
|
||||
contentLength = contentLength + prev.length
|
||||
children.push(<TagText key={index} text={main} />)
|
||||
switch (tag.type) {
|
||||
case 'url':
|
||||
contentLength = contentLength + 23
|
||||
break
|
||||
case 'accounts':
|
||||
const theMatch = main.match(/@/g)
|
||||
if (theMatch && theMatch.length > 1) {
|
||||
contentLength =
|
||||
contentLength + main.split(new RegExp('(@.*?)@'))[1].length
|
||||
} else {
|
||||
contentLength = contentLength + main.length
|
||||
}
|
||||
break
|
||||
case 'hashtags':
|
||||
contentLength = contentLength + main.length
|
||||
}
|
||||
break
|
||||
case 'hashtags':
|
||||
contentLength = contentLength + main.length
|
||||
break
|
||||
break
|
||||
}
|
||||
_content = next
|
||||
pointer = pointer + prev.length + tag.length
|
||||
}
|
||||
_content = next
|
||||
pointer = pointer + prev.length + tag!.length
|
||||
})
|
||||
children.push(_content)
|
||||
contentLength = contentLength + _content.length
|
||||
|
@ -9,7 +9,7 @@ const composePost = async (
|
||||
) => {
|
||||
const formData = new FormData()
|
||||
|
||||
if (params?.type === 'conversation' || params?.type === 'reply') {
|
||||
if (params?.type === 'reply') {
|
||||
formData.append('in_reply_to_id', composeState.replyToStatus!.id)
|
||||
}
|
||||
|
||||
@ -20,9 +20,9 @@ const composePost = async (
|
||||
formData.append('status', composeState.text.raw)
|
||||
|
||||
if (composeState.poll.active) {
|
||||
Object.values(composeState.poll.options)
|
||||
.filter(e => e?.length)
|
||||
.forEach(e => formData.append('poll[options][]', e!))
|
||||
Object.values(composeState.poll.options).forEach(
|
||||
e => e && e.length && formData.append('poll[options][]', e)
|
||||
)
|
||||
formData.append('poll[expires_in]', composeState.poll.expire)
|
||||
formData.append('poll[multiple]', composeState.poll.multiple.toString())
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ const composeReducer = (
|
||||
attachments: {
|
||||
...state.attachments,
|
||||
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 formData = new FormData()
|
||||
formData.append('client_name', 'tooot📱')
|
||||
formData.append('client_name', 'tooot')
|
||||
formData.append('website', 'https://tooot.app')
|
||||
formData.append('redirect_uris', redirectUri)
|
||||
formData.append('scopes', 'read write follow push')
|
||||
|
@ -2,24 +2,56 @@ import client from '@api/client'
|
||||
import { AxiosError } from 'axios'
|
||||
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 { instanceDomain } = queryKey[1]
|
||||
const queryFunction = async ({ queryKey }: { queryKey: QueryKey }) => {
|
||||
const { instanceDomain, checkPublic } = queryKey[1]
|
||||
|
||||
return client<Mastodon.Instance>({
|
||||
let res: Mastodon.Instance & { publicAllow?: boolean } = await client<
|
||||
Mastodon.Instance
|
||||
>({
|
||||
method: 'get',
|
||||
instance: 'remote',
|
||||
instanceDomain,
|
||||
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,
|
||||
...queryKeyParams
|
||||
}: QueryKey[1] & {
|
||||
options?: UseQueryOptions<Mastodon.Instance, AxiosError, TData>
|
||||
options?: UseQueryOptions<
|
||||
Mastodon.Instance & { publicAllow?: boolean },
|
||||
AxiosError,
|
||||
TData
|
||||
>
|
||||
}) => {
|
||||
const queryKey: QueryKey = ['Instance', { ...queryKeyParams }]
|
||||
return useQuery(queryKey, queryFunction, options)
|
||||
|
@ -292,7 +292,7 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
|
||||
case 'poll':
|
||||
const formData = new FormData()
|
||||
params.payload.type === 'vote' &&
|
||||
params.payload.options!.forEach((option, index) => {
|
||||
params.payload.options?.forEach((option, index) => {
|
||||
if (option) {
|
||||
formData.append('choices[]', index.toString())
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { RootState } from '@root/store'
|
||||
import Constants from 'expo-constants'
|
||||
import * as StoreReview from 'expo-store-review'
|
||||
|
||||
export const supportedLngs = ['zh-Hans', 'en']
|
||||
@ -37,9 +38,11 @@ const contextsSlice = createSlice({
|
||||
initialState: contextsInitialState as ContextsState,
|
||||
reducers: {
|
||||
updateStoreReview: (state, action: PayloadAction<1>) => {
|
||||
state.storeReview.current = state.storeReview.current + action.payload
|
||||
if (state.storeReview.current === state.storeReview.context) {
|
||||
StoreReview.isAvailableAsync().then(() => StoreReview.requestReview())
|
||||
if (Constants.manifest.releaseChannel === 'production') {
|
||||
state.storeReview.current = state.storeReview.current + action.payload
|
||||
if (state.storeReview.current === state.storeReview.context) {
|
||||
StoreReview.isAvailableAsync().then(() => StoreReview.requestReview())
|
||||
}
|
||||
}
|
||||
},
|
||||
updatePublicRemoteNotice: (state, action: PayloadAction<1>) => {
|
||||
|
@ -259,7 +259,7 @@ export const getLocalUrl = ({ instances: { local } }: RootState) =>
|
||||
: undefined
|
||||
export const getLocalUri = ({ instances: { local } }: RootState) =>
|
||||
local.activeIndex !== null
|
||||
? local.instances[local.activeIndex].url
|
||||
? local.instances[local.activeIndex].uri
|
||||
: undefined
|
||||
export const getLocalAccount = ({ instances: { local } }: RootState) =>
|
||||
local.activeIndex !== null
|
||||
|
Loading…
x
Reference in New Issue
Block a user