diff --git a/README.md b/README.md
new file mode 100644
index 00000000..9e10710f
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+@tooot@xmflsct.com
\ No newline at end of file
diff --git a/app.config.ts b/app.config.ts
index cef5160e..00bdddb0 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -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: {
diff --git a/demo/statuses.json b/demo/statuses.json
new file mode 100644
index 00000000..2c654588
--- /dev/null
+++ b/demo/statuses.json
@@ -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": "
Would you like to try out this simple and open-source mobile app for Mastodon? 😊
",
+ "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": "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.
",
+ "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": "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.
",
+ "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": []
+ }
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/src/api/client.ts b/src/api/client.ts
index 986b3ede..52cdac4f 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -35,11 +35,13 @@ const client = async ({
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 ({
headers: {
'Content-Type': 'application/json',
...headers,
- ...(instance === 'local' && {
- Authorization: `Bearer ${state.local!.instances[theLocalIndex!].token}`
+ ...(token && {
+ Authorization: `Bearer ${token}`
})
},
...(body && { data: body }),
diff --git a/src/components/Instance.tsx b/src/components/Instance.tsx
index a1f3f659..8b1b7b48 100644
--- a/src/components/Instance.tsx
+++ b/src/components/Instance.tsx
@@ -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 = ({
const instanceQuery = useInstanceQuery({
instanceDomain,
+ checkPublic: type === 'remote',
options: { enabled: false, retry: false }
})
const appsQuery = useAppsQuery({
@@ -170,7 +171,12 @@ const ComponentInstance: React.FC = ({
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 = ({
type='text'
content={buttonContent}
onPress={processUpdate}
- disabled={!instanceQuery.data?.uri}
+ disabled={
+ !instanceQuery.data?.uri ||
+ (type === 'remote' && !instanceQuery.data.publicAllow)
+ }
loading={instanceQuery.isFetching || appsQuery.isFetching}
/>
+ {type === 'remote' &&
+ instanceQuery.data &&
+ !instanceQuery.data.publicAllow ? (
+
+ {t('server.privateInstance')}
+
+ ) : null}
= ({
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 (
@@ -50,7 +58,13 @@ const ParseEmojis: React.FC = ({
{/* When emoji starts a paragraph, lineHeight will break */}
{i === 0 ? : null}
-
+
+
+
)
} else {
diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx
index bae5b4b6..c80ad435 100644
--- a/src/components/Parse/HTML.tsx
+++ b/src/components/Parse/HTML.tsx
@@ -154,12 +154,16 @@ const ParseHTML: React.FC = ({
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 = ({
)
},
- [theme]
+ [theme, i18n.language]
)
return (
diff --git a/src/components/Timelines.tsx b/src/components/Timelines.tsx
index c3605f1b..6cb45a1c 100644
--- a/src/components/Timelines.tsx
+++ b/src/components/Timelines.tsx
@@ -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 = ({ name, content }) => {
+const Timelines: React.FC = ({ 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 = ({ 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: () =>
})
}
}
} else {
return {
headerCenter: () => (
-
-
- setSegment(nativeEvent.selectedSegmentIndex)
- }
- />
-
+ p.title)}
+ selectedIndex={segment}
+ onChange={({ nativeEvent }) =>
+ setSegment(nativeEvent.selectedSegmentIndex)
+ }
+ style={styles.segmentsContainer}
+ />
),
headerRight: () => (
)
}
}
- }, [localActiveIndex, mode, segment])
+ }, [localActiveIndex, mode, segment, i18n.language])
const renderPager = useCallback(props => , [])
diff --git a/src/components/Timelines/Timeline.tsx b/src/components/Timelines/Timeline.tsx
index 36783101..f866eb3a 100644
--- a/src/components/Timelines/Timeline.tsx
+++ b/src/components/Timelines/Timeline.tsx
@@ -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 = ({
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 = ({
// 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>(null)
useEffect(() => {
@@ -166,43 +159,29 @@ const Timeline: React.FC = ({
[hasNextPage]
)
- const queryClient = useQueryClient()
+ const isSwipeDown = useRef(false)
const refreshControl = useMemo(
() => (
{
- // if (hasPreviousPage) {
- // fetchPreviousPage()
- // } else {
- // queryClient.setQueryData | 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 })
diff --git a/src/components/Timelines/Timeline/Conversation.tsx b/src/components/Timelines/Timeline/Conversation.tsx
index 3f35f19f..351c35d0 100644
--- a/src/components/Timelines/Timeline/Conversation.tsx
+++ b/src/components/Timelines/Timeline/Conversation.tsx
@@ -79,43 +79,44 @@ const TimelineConversation: React.FC = ({
{conversation.last_status ? (
-
-
- {conversation.last_status.poll && (
-
+
+
- )}
-
+ {conversation.last_status.poll && (
+
+ )}
+
+
+
+
+ >
) : null}
-
-
-
-
)
}
diff --git a/src/components/Timelines/Timeline/Shared/Attachment/Audio.tsx b/src/components/Timelines/Timeline/Shared/Attachment/Audio.tsx
index cbaa0bde..90eec222 100644
--- a/src/components/Timelines/Timeline/Shared/Attachment/Audio.tsx
+++ b/src/components/Timelines/Timeline/Shared/Attachment/Audio.tsx
@@ -111,12 +111,10 @@ const AttachmentAudio: React.FC = ({
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
diff --git a/src/components/Timelines/Timeline/Shared/Attachment/Unsupported.tsx b/src/components/Timelines/Timeline/Shared/Attachment/Unsupported.tsx
index f1681a75..e525e83c 100644
--- a/src/components/Timelines/Timeline/Shared/Attachment/Unsupported.tsx
+++ b/src/components/Timelines/Timeline/Shared/Attachment/Unsupported.tsx
@@ -59,7 +59,9 @@ const AttachmentUnsupported: React.FC = ({
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}
>
diff --git a/src/i18n/en/components/instance.ts b/src/i18n/en/components/instance.ts
index a64ec33d..e5ea0d78 100644
--- a/src/i18n/en/components/instance.ts
+++ b/src/i18n/en/components/instance.ts
@@ -1,6 +1,7 @@
export default {
server: {
textInput: { placeholder: "Instance' domain" },
+ privateInstance: 'Private instance, peeping not allowed',
button: {
local: 'Login',
remote: 'Peep'
diff --git a/src/i18n/en/components/parse.ts b/src/i18n/en/components/parse.ts
index 5a56b039..fd05c099 100644
--- a/src/i18n/en/components/parse.ts
+++ b/src/i18n/en/components/parse.ts
@@ -3,6 +3,7 @@ export default {
expanded: {
true: 'Fold {{hint}}',
false: 'Expand {{hint}}'
- }
+ },
+ defaultHint: 'article'
}
}
diff --git a/src/i18n/en/screens/meRoot.ts b/src/i18n/en/screens/meRoot.ts
index a2dadbe2..4f4aee22 100644
--- a/src/i18n/en/screens/meRoot.ts
+++ b/src/i18n/en/screens/meRoot.ts
@@ -9,7 +9,7 @@ export default {
heading: '$t(sharedAnnouncements:heading)',
content: {
unread: '{{amount}} unread',
- read: 'all read'
+ read: 'All read'
}
}
},
diff --git a/src/i18n/zh-Hans/components/instance.ts b/src/i18n/zh-Hans/components/instance.ts
index 6ad3c05a..4da6b041 100644
--- a/src/i18n/zh-Hans/components/instance.ts
+++ b/src/i18n/zh-Hans/components/instance.ts
@@ -1,6 +1,7 @@
export default {
server: {
textInput: { placeholder: '输入社区服务器地址' },
+ privateInstance: '非公开社区, 不能围观',
button: {
local: '登录',
remote: '围观'
diff --git a/src/i18n/zh-Hans/components/parse.ts b/src/i18n/zh-Hans/components/parse.ts
index 67b48fe2..f1575725 100644
--- a/src/i18n/zh-Hans/components/parse.ts
+++ b/src/i18n/zh-Hans/components/parse.ts
@@ -3,6 +3,7 @@ export default {
expanded: {
true: '折叠{{hint}}',
false: '展开{{hint}}'
- }
+ },
+ defaultHint: '全文'
}
}
diff --git a/src/screens/Local.tsx b/src/screens/Local.tsx
index 6f673f81..1300c49b 100644
--- a/src/screens/Local.tsx
+++ b/src/screens/Local.tsx
@@ -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 (
-
- )
-}
+const ScreenLocal = React.memo(
+ () => {
+ return
+ },
+ () => true
+)
export default ScreenLocal
diff --git a/src/screens/Me/Root/Collections.tsx b/src/screens/Me/Root/Collections.tsx
index 4af18dcf..af55da15 100644
--- a/src/screens/Me/Root/Collections.tsx
+++ b/src/screens/Me/Root/Collections.tsx
@@ -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 (
diff --git a/src/screens/Me/Settings.tsx b/src/screens/Me/Settings.tsx
index 85353d02..148e8b39 100644
--- a/src/screens/Me/Settings.tsx
+++ b/src/screens/Me/Settings.tsx
@@ -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])
diff --git a/src/screens/Me/Switch/Root.tsx b/src/screens/Me/Switch/Root.tsx
index 2a5d3c87..a8634102 100644
--- a/src/screens/Me/Switch/Root.tsx
+++ b/src/screens/Me/Switch/Root.tsx
@@ -50,7 +50,7 @@ const AccountButton: React.FC = ({
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
}
})
diff --git a/src/screens/Public.tsx b/src/screens/Public.tsx
index bf1b5516..82b8c634 100644
--- a/src/screens/Public.tsx
+++ b/src/screens/Public.tsx
@@ -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 (
-
- )
-}
+const ScreenPublic = React.memo(
+ () => {
+ return
+ },
+ () => true
+)
export default ScreenPublic
diff --git a/src/screens/Shared/Account/Information/Created.tsx b/src/screens/Shared/Account/Information/Created.tsx
index 7228bbc2..572d2f0a 100644
--- a/src/screens/Shared/Account/Information/Created.tsx
+++ b/src/screens/Shared/Account/Information/Created.tsx
@@ -47,7 +47,7 @@ const AccountInformationCreated = forwardRef(
}}
>
{t('content.created_at', {
- date: new Date(account?.created_at!).toLocaleDateString(
+ date: new Date(account?.created_at || '').toLocaleDateString(
i18n.language,
{
year: 'numeric',
diff --git a/src/screens/Shared/Compose.tsx b/src/screens/Shared/Compose.tsx
index 863bf139..310e2d79 100644
--- a/src/screens/Shared/Compose.tsx
+++ b/src/screens/Shared/Compose.tsx
@@ -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 = ({
}}
/>
),
- [totalTextCount]
+ [totalTextCount, composeState]
)
const headerCenter = useCallback(
() => (
@@ -190,7 +191,8 @@ const Compose: React.FC = ({
}
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 = ({
})
}}
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]
diff --git a/src/screens/Shared/Compose/EditAttachment.tsx b/src/screens/Shared/Compose/EditAttachment.tsx
index e0daf91a..165cd653 100644
--- a/src/screens/Shared/Compose/EditAttachment.tsx
+++ b/src/screens/Shared/Compose/EditAttachment.tsx
@@ -54,10 +54,11 @@ const ComposeEditAttachment: React.FC = ({
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
}
}
diff --git a/src/screens/Shared/Compose/EditAttachment/Image.tsx b/src/screens/Shared/Compose/EditAttachment/Image.tsx
index c7f1016a..10397965 100644
--- a/src/screens/Shared/Compose/EditAttachment/Image.tsx
+++ b/src/screens/Shared/Compose/EditAttachment/Image.tsx
@@ -28,20 +28,21 @@ const ComposeEditAttachmentImage: React.FC = ({ 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 = ({ index, focus }) => {
height: imageDimensionis.height
}}
source={{
- uri: theAttachmentLocal.uri || theAttachmentRemote.preview_url
+ uri: theAttachmentLocal?.uri || theAttachmentRemote?.preview_url
}}
/>
diff --git a/src/screens/Shared/Compose/EditAttachment/Root.tsx b/src/screens/Shared/Compose/EditAttachment/Root.tsx
index 39486c6e..6f6da03d 100644
--- a/src/screens/Shared/Compose/EditAttachment/Root.tsx
+++ b/src/screens/Shared/Compose/EditAttachment/Root.tsx
@@ -32,31 +32,33 @@ const ComposeEditAttachmentRoot: React.FC = ({
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
- case 'video':
- case 'gifv':
- const video = composeState.attachments.uploads[index]
- return (
-
- )
+ if (theAttachment) {
+ switch (theAttachment.type) {
+ case 'image':
+ return
+ case 'video':
+ case 'gifv':
+ const video = composeState.attachments.uploads[index]
+ return (
+
+ )
+ }
}
return null
}, [])
diff --git a/src/screens/Shared/Compose/Posting.tsx b/src/screens/Shared/Compose/Posting.tsx
index 5164e851..c3225d7f 100644
--- a/src/screens/Shared/Compose/Posting.tsx
+++ b/src/screens/Shared/Compose/Posting.tsx
@@ -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={
-
- }
- />
+
}
/>
)
@@ -32,12 +22,4 @@ const ComposePosting = React.memo(
() => true
)
-const styles = StyleSheet.create({
- base: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center'
- }
-})
-
export default ComposePosting
diff --git a/src/screens/Shared/Compose/Root.tsx b/src/screens/Shared/Compose/Root.tsx
index 0ac7a24f..c49c762d 100644
--- a/src/screens/Shared/Compose/Root.tsx
+++ b/src/screens/Shared/Compose/Root.tsx
@@ -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) {
diff --git a/src/screens/Shared/Compose/Root/Footer/Attachments.tsx b/src/screens/Shared/Compose/Root/Footer/Attachments.tsx
index 8504cd59..6a9818eb 100644
--- a/src/screens/Shared/Compose/Root/Footer/Attachments.tsx
+++ b/src/screens/Shared/Compose/Root/Footer/Attachments.tsx
@@ -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
}
diff --git a/src/screens/Shared/Compose/Root/Footer/Emojis.tsx b/src/screens/Shared/Compose/Root/Footer/Emojis.tsx
index 36e239a9..c73c8c46 100644
--- a/src/screens/Shared/Compose/Root/Footer/Emojis.tsx
+++ b/src/screens/Shared/Compose/Root/Footer/Emojis.tsx
@@ -70,7 +70,7 @@ const ComposeEmojis: React.FC = () => {
item.shortcode}
renderSectionHeader={listHeader}
renderItem={listItem}
diff --git a/src/screens/Shared/Compose/formatText.tsx b/src/screens/Shared/Compose/formatText.tsx
index 99752b08..7ee4feb0 100644
--- a/src/screens/Shared/Compose/formatText.tsx
+++ b/src/screens/Shared/Compose/formatText.tsx
@@ -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()
- 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()
+ 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
diff --git a/src/screens/Shared/Compose/utils/post.ts b/src/screens/Shared/Compose/utils/post.ts
index c62d0457..2464c870 100644
--- a/src/screens/Shared/Compose/utils/post.ts
+++ b/src/screens/Shared/Compose/utils/post.ts
@@ -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())
}
diff --git a/src/screens/Shared/Compose/utils/reducer.ts b/src/screens/Shared/Compose/utils/reducer.ts
index 1f124e82..33932383 100644
--- a/src/screens/Shared/Compose/utils/reducer.ts
+++ b/src/screens/Shared/Compose/utils/reducer.ts
@@ -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
)
}
}
diff --git a/src/utils/queryHooks/apps.ts b/src/utils/queryHooks/apps.ts
index cb445e49..3ea507a3 100644
--- a/src/utils/queryHooks/apps.ts
+++ b/src/utils/queryHooks/apps.ts
@@ -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')
diff --git a/src/utils/queryHooks/instance.ts b/src/utils/queryHooks/instance.ts
index dcb287e7..f44c9280 100644
--- a/src/utils/queryHooks/instance.ts
+++ b/src/utils/queryHooks/instance.ts
@@ -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({
+ 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({
+ 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 = ({
+const useInstanceQuery = <
+ TData = Mastodon.Instance & { publicAllow?: boolean }
+>({
options,
...queryKeyParams
}: QueryKey[1] & {
- options?: UseQueryOptions
+ options?: UseQueryOptions<
+ Mastodon.Instance & { publicAllow?: boolean },
+ AxiosError,
+ TData
+ >
}) => {
const queryKey: QueryKey = ['Instance', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts
index 4ca557bc..2bed0099 100644
--- a/src/utils/queryHooks/timeline.ts
+++ b/src/utils/queryHooks/timeline.ts
@@ -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())
}
diff --git a/src/utils/slices/contextsSlice.ts b/src/utils/slices/contextsSlice.ts
index 28cd1327..2d5069a0 100644
--- a/src/utils/slices/contextsSlice.ts
+++ b/src/utils/slices/contextsSlice.ts
@@ -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>) => {
diff --git a/src/utils/slices/instancesSlice.ts b/src/utils/slices/instancesSlice.ts
index 11d9ffe8..31718612 100644
--- a/src/utils/slices/instancesSlice.ts
+++ b/src/utils/slices/instancesSlice.ts
@@ -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