mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Fix bugs
This commit is contained in:
@ -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
|
||||
|
Reference in New Issue
Block a user