Merge pull request #134 from tooot-app/main

Release v2.0
This commit is contained in:
xmflsct 2021-05-27 12:15:31 +02:00 committed by GitHub
commit 8fa458a74d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 3113 additions and 2843 deletions

View File

@ -5,6 +5,8 @@ export SENTRY_PROJECT=""
export SENTRY_AUTH_TOKEN=""
export SENTRY_DSN=""
export TRANSLATE_KEY=""
# Fastlane start
export LC_ALL=""
export LANG=""

View File

@ -40,6 +40,7 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TRANSLATE_KEY: ${{ secrets.TRANSLATE_KEY }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}

View File

@ -15,7 +15,7 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.1.1)
aws-partitions (1.445.0)
aws-partitions (1.455.0)
aws-sdk-core (3.114.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
@ -24,7 +24,7 @@ GEM
aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.93.1)
aws-sdk-s3 (1.94.1)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
@ -71,10 +71,9 @@ GEM
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.8)
connection_pool (2.2.5)
declarative (0.0.20)
digest-crc (0.6.3)
rake (>= 12.0.0, < 14.0.0)
@ -85,25 +84,23 @@ GEM
escape (0.0.4)
ethon (0.12.0)
ffi (>= 1.3.0)
excon (0.80.1)
faraday (1.4.0)
faraday-excon (~> 1.0)
excon (0.81.0)
faraday (1.4.1)
faraday-excon (~> 1.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-net_http_persistent (~> 1.1)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-excon (1.0.0)
excon (>= 0.27.4)
faraday-excon (1.1.0)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.0.3)
net-http-persistent (>= 3.1)
faraday-net_http_persistent (1.1.0)
faraday_middleware (1.0.0)
faraday (~> 1.0)
fastimage (2.2.3)
fastlane (2.180.1)
fastlane (2.182.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0)
@ -111,7 +108,7 @@ GEM
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@ -122,7 +119,7 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
@ -132,7 +129,6 @@ GEM
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
@ -185,14 +181,14 @@ GEM
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.16.1)
googleauth (0.16.2)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.14)
highline (1.7.10)
highline (2.0.3)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
@ -200,7 +196,7 @@ GEM
concurrent-ruby (~> 1.0)
jmespath (1.4.0)
json (2.5.1)
jwt (2.2.2)
jwt (2.2.3)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.0)
@ -211,8 +207,6 @@ GEM
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
netrc (0.11.0)
os (1.1.1)
plist (3.6.0)
@ -237,7 +231,6 @@ GEM
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)

View File

@ -1,5 +1,5 @@
# [tooot](https://tooot.app/) app for Mastodon
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push?style=flat-square)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app?style=flat-square) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases&style=flat-square) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app?style=flat-square) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build?style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate&style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release&style=flat-square)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release)

View File

@ -8,8 +8,8 @@ public class BasePackageList {
public List<Package> getPackageList() {
return Arrays.<Package>asList(
new expo.modules.application.ApplicationPackage(),
new expo.modules.constants.ConstantsPackage(),
new expo.modules.av.AVPackage(),
new expo.modules.constants.ConstantsPackage(),
new expo.modules.crypto.CryptoPackage(),
new expo.modules.device.DevicePackage(),
new expo.modules.errorrecovery.ErrorRecoveryPackage(),
@ -19,17 +19,15 @@ public class BasePackageList {
new expo.modules.font.FontLoaderPackage(),
new expo.modules.haptics.HapticsPackage(),
new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.imagemanipulator.ImageManipulatorPackage(),
new expo.modules.imagepicker.ImagePickerPackage(),
new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.lineargradient.LinearGradientPackage(),
new expo.modules.localization.LocalizationPackage(),
new expo.modules.location.LocationPackage(),
new expo.modules.notifications.NotificationsPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.screencapture.ScreenCapturePackage(),
new expo.modules.securestore.SecureStorePackage(),
new expo.modules.splashscreen.SplashScreenPackage(),
new expo.modules.sqlite.SQLitePackage(),
new expo.modules.storereview.StoreReviewPackage(),
new expo.modules.updates.UpdatesPackage(),
new expo.modules.videothumbnails.VideoThumbnailsPackage(),

View File

@ -4,8 +4,8 @@ buildscript {
ext {
buildToolsVersion = "29.0.3"
minSdkVersion = 21
compileSdkVersion = 29
targetSdkVersion = 29
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "20.1.5948944"
}
repositories {

View File

@ -13,7 +13,8 @@ export default (): ExpoConfig => ({
privacy: 'hidden',
assetBundlePatterns: ['assets/*'],
extra: {
sentryDSN: process.env.SENTRY_DSN
sentryDSN: process.env.SENTRY_DSN,
translateKey: process.env.TRANSLATE_KEY
},
hooks: {
postPublish: [

View File

@ -11,6 +11,7 @@ module.exports = function (api) {
'@assets': './assets',
'@root': './src',
'@api': './src/api',
'@helpers': './src/helpers',
'@components': './src/components',
'@screens': './src/screens',
'@utils': './src/utils'

View File

@ -27,9 +27,4 @@ submission_information({
add_id_info_tracks_action: false,
add_id_info_tracks_install: false,
add_id_info_uses_idfa: true
})
release_notes({
'zh-Hans' => "添加支持修改账户信息",
'en-US' => "Added the possibility to update account information"
})

View File

@ -109,8 +109,8 @@ private_lane :build_ios do
upload_to_app_store( ipa: IPA_FILE, app_version: VERSION )
else
if !is_ci
match( type: "development", readonly: true )
build_ios_app( export_method: "development", output_directory: BUILD_DIRECTORY, silent: true )
match( type: "adhoc", readonly: true )
build_ios_app( export_method: "ad-hoc", output_directory: BUILD_DIRECTORY, silent: true )
install_on_device( skip_wifi: true )
end
end

View File

@ -1 +1,2 @@
Added translation option, translation service is provided by various providers
When updating profile, now avatar and banner can be uploaded

View File

@ -1 +1,2 @@
加入翻译嘟文支持,翻译服务由多个服务商提供
修改个人信息里可以上传头像及横幅

File diff suppressed because it is too large Load Diff

View File

@ -346,7 +346,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = tooot/tooot.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2102022230;
DEVELOPMENT_TEAM = 8EGBLQ2MA6;
@ -366,7 +366,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot;
PRODUCT_NAME = tooot;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.xmflsct.app.tooot";
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc com.xmflsct.app.tooot";
SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;

View File

@ -1,11 +1,11 @@
{
"name": "tooot",
"versions": {
"native": "210317",
"major": 1,
"minor": 1,
"patch": 0,
"expo": "40.0.0"
"native": "210511",
"major": 2,
"minor": 0,
"patch": 1,
"expo": "41.0.0"
},
"description": "tooot app for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
@ -26,43 +26,45 @@
"dependencies": {
"@expo/react-native-action-sheet": "^3.9.0",
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
"@react-native-async-storage/async-storage": "^1.14.1",
"@react-native-async-storage/async-storage": "^1.15.4",
"@react-native-community/blur": "^3.6.0",
"@react-native-community/cameraroll": "^4.0.2",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "^6.0.0",
"@react-native-community/cameraroll": "^4.0.4",
"@react-native-community/masked-view": "0.1.11",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/segmented-control": "2.2.2",
"@react-navigation/bottom-tabs": "^5.11.8",
"@react-navigation/native": "^5.9.3",
"@react-navigation/stack": "^5.14.3",
"@reduxjs/toolkit": "^1.5.0",
"@sentry/react-native": "^2.3.0",
"@sharcoux/slider": "^5.1.3",
"@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/native": "^5.9.4",
"@react-navigation/stack": "^5.14.5",
"@reduxjs/toolkit": "^1.5.1",
"@sentry/react-native": "^2.4.3",
"@sharcoux/slider": "^5.3.0",
"axios": "^0.21.1",
"expo": "^40.0.1",
"expo-auth-session": "~3.1.0",
"expo-av": "~9.0.0",
"expo-crypto": "~9.0.0",
"expo-firebase-analytics": "~3.0.0",
"expo-haptics": "~9.0.0",
"expo-image-picker": "~10.0.0",
"expo-linking": "~2.1.1",
"expo-localization": "~10.0.0",
"expo-notifications": "~0.9.0",
"expo-random": "~11.0.0",
"expo-screen-capture": "^3.0.0",
"expo-splash-screen": "~0.9.0",
"expo-status-bar": "~1.0.3",
"expo-store-review": "~3.0.0",
"expo-video-thumbnails": "~5.0.0",
"expo-web-browser": "~9.0.0",
"i18next": "^19.9.2",
"expo": "^41.0.1",
"expo-auth-session": "~3.2.3",
"expo-av": "~9.1.2",
"expo-crypto": "~9.1.0",
"expo-firebase-analytics": "~4.0.2",
"expo-haptics": "~10.0.0",
"expo-image-manipulator": "~9.1.0",
"expo-image-picker": "~10.1.4",
"expo-linking": "~2.2.3",
"expo-localization": "~10.1.0",
"expo-notifications": "~0.11.6",
"expo-random": "~11.1.2",
"expo-screen-capture": "^3.1.0",
"expo-secure-store": "~10.1.0",
"expo-splash-screen": "~0.10.2",
"expo-status-bar": "~1.0.4",
"expo-store-review": "~4.0.2",
"expo-video-thumbnails": "~5.1.0",
"expo-web-browser": "~9.1.0",
"i18next": "^20.3.0",
"li": "^1.3.0",
"lodash": "^4.17.21",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-i18next": "^11.8.10",
"react-native": "~0.64.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "^11.9.0",
"react-native": "~0.64.1",
"react-native-animated-spinkit": "^1.5.2",
"react-native-blurhash": "^1.1.4",
"react-native-fast-image": "^8.3.4",
@ -70,56 +72,55 @@
"react-native-flash-message": "^0.1.23",
"react-native-gesture-handler": "~1.10.3",
"react-native-htmlview": "^0.16.0",
"react-native-pager-view": "^5.1.2",
"react-native-reanimated": "^2.0.0",
"react-native-pager-view": "5.1.9",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~2.17.1",
"react-native-svg": "12.1.0",
"react-native-swipe-list-view": "^3.2.6",
"react-native-tab-view": "^3.0.0",
"react-native-unimodules": "~0.12.0",
"react-query": "^3.12.2",
"react-redux": "^7.2.2",
"react-native-screens": "~3.3.0",
"react-native-svg": "12.1.1",
"react-native-swipe-list-view": "^3.2.7",
"react-native-tab-view": "^3.0.1",
"react-native-unimodules": "~0.13.3",
"react-query": "^3.16.0",
"react-redux": "^7.2.4",
"react-timeago": "^5.2.0",
"redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3",
"sentry-expo": "^3.0.5",
"tslib": "^2.1.0",
"sentry-expo": "^3.1.3",
"tslib": "^2.2.0",
"valid-url": "^1.0.9"
},
"devDependencies": {
"@babel/core": "~7.13.10",
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/core": "~7.14.3",
"@babel/plugin-proposal-optional-chaining": "^7.14.2",
"@babel/preset-typescript": "^7.13.0",
"@expo/config": "^3.3.31",
"@expo/config": "^3.3.43",
"@jest/types": "^26.6.2",
"@testing-library/jest-native": "^4.0.1",
"@testing-library/react-hooks": "^5.1.0",
"@testing-library/react-hooks": "^5.1.2",
"@testing-library/react-native": "^7.2.0",
"@types/jest": "^26.0.20",
"@types/lodash": "^4.14.168",
"@types/react": "~17.0.3",
"@types/react-dom": "~17.0.2",
"@types/react-native": "~0.63.52",
"@types/jest": "^26.0.23",
"@types/lodash": "^4.14.170",
"@types/react": "~17.0.8",
"@types/react-dom": "~17.0.5",
"@types/react-native": "~0.64.6",
"@types/react-navigation": "^3.4.0",
"@types/react-redux": "^7.1.16",
"@types/react-test-renderer": "^17.0.1",
"@types/react-timeago": "^4.1.2",
"@types/valid-url": "^1.0.3",
"@welldone-software/why-did-you-render": "^6.1.1",
"@welldone-software/why-did-you-render": "^6.1.4",
"babel-jest": "~26.6.3",
"babel-plugin-module-resolver": "^4.1.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"chalk": "^4.1.0",
"dotenv": "^8.2.0",
"chalk": "^4.1.1",
"dotenv": "^10.0.0",
"jest": "^26.6.3",
"jest-expo": "^40.0.2",
"jest-expo": "^41.0.0",
"nock": "^13.0.11",
"react-native-clean-project": "^3.6.3",
"react-native-clean-project": "^3.6.4",
"react-navigation": "^4.4.4",
"react-navigation-stack": "^2.10.4",
"react-test-renderer": "^17.0.1",
"typescript": "~4.2.3",
"uri-scheme": "^1.0.68"
"react-test-renderer": "^17.0.2",
"typescript": "~4.2.4"
}
}
}

View File

@ -1,6 +1,4 @@
declare namespace Nav {
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
type RootStackParamList = {
'Screen-Tabs': undefined
'Screen-Actions':
@ -151,8 +149,4 @@ declare namespace Nav {
fields?: Mastodon.Source['fields']
}
}
type TabMePushStackParamList = {
'Tab-Me-Push-Root': undefined
}
}

View File

@ -1,4 +1,5 @@
declare module 'gl-react-blurhash'
declare module 'htmlparser2-without-node-native'
declare module 'li'
declare module 'react-native-feather'
declare module 'react-native-htmlview'

View File

@ -1,4 +1,5 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import queryClient from '@helpers/queryClient'
import i18n from '@root/i18n/i18n'
import Screens from '@root/Screens'
import audio from '@root/startup/audio'
@ -14,8 +15,7 @@ import * as Notifications from 'expo-notifications'
import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react'
import { AppState, LogBox, Platform } from 'react-native'
import { enableScreens } from 'react-native-screens'
import { QueryClient, QueryClientProvider } from 'react-query'
import { QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import push from './startup/push'
@ -29,12 +29,6 @@ sentry()
audio()
push()
log('log', 'react-query', 'initializing')
export const queryClient = new QueryClient()
log('log', 'react-native-screens', 'initializing')
enableScreens()
const App: React.FC = () => {
log('log', 'App', 'rendering App')
const [localCorrupt, setLocalCorrupt] = useState<string>()

View File

@ -1,9 +1,8 @@
import { HeaderCenter, HeaderLeft } from '@components/Header'
import { displayMessage, Message, removeMessage } from '@components/Message'
import navigationRef from '@helpers/navigationRef'
import { useNetInfo } from '@react-native-community/netinfo'
import {
NavigationContainer,
NavigationContainerRef
} from '@react-navigation/native'
import { NavigationContainer } from '@react-navigation/native'
import ScreenActions from '@screens/Actions'
import ScreenAnnouncements from '@screens/Announcements'
import ScreenCompose from '@screens/Compose'
@ -19,7 +18,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes'
import * as Analytics from 'expo-firebase-analytics'
import { addScreenshotListener } from 'expo-screen-capture'
import React, { createRef, useCallback, useEffect, useRef } from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Platform, StatusBar } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
@ -28,7 +27,6 @@ import { useDispatch, useSelector } from 'react-redux'
import * as Sentry from 'sentry-expo'
const Stack = createNativeStackNavigator<Nav.RootStackParamList>()
export const navigationRef = createRef<NavigationContainerRef>()
export interface Props {
localCorrupt?: string
@ -174,18 +172,30 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
<Stack.Screen
name='Screen-Announcements'
component={ScreenAnnouncements}
options={{
options={({ navigation }) => ({
stackPresentation: 'transparentModal',
stackAnimation: 'fade',
headerShown: false
}}
headerShown: true,
headerHideShadow: true,
headerTopInsetEnabled: false,
headerStyle: { backgroundColor: 'transparent' },
headerLeft: () => (
<HeaderLeft content='X' onPress={() => navigation.goBack()} />
),
headerTitle: t('screenAnnouncements:heading'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('screenAnnouncements:heading')} />
)
})
})}
/>
<Stack.Screen
name='Screen-Compose'
component={ScreenCompose}
options={{
stackPresentation: 'fullScreenModal',
headerShown: false
...(Platform.OS === 'android' && { headerShown: false })
}}
/>
<Stack.Screen
@ -194,7 +204,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
options={{
stackPresentation: 'fullScreenModal',
stackAnimation: 'fade',
headerShown: false
...(Platform.OS === 'android' && { headerShown: false })
}}
/>
</Stack.Navigator>

View File

@ -69,12 +69,16 @@ const apiGeneral = async <T = unknown>({
error.response.status,
error.response.data.error
)
return Promise.reject(error.response)
return Promise.reject(error.response.data.error)
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error(ctx.bold(' API general '), ctx.bold('request'), error)
console.error(
ctx.bold(' API general '),
ctx.bold('request'),
error.request
)
return Promise.reject()
} else {
console.error(

View File

@ -98,7 +98,7 @@ const apiInstance = async <T = unknown>({
error.response.status,
error.response.data.error
)
return Promise.reject(error.response)
return Promise.reject(error.response.data.error)
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of

View File

@ -4,7 +4,6 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { chunk, forEach, groupBy, sortBy } from 'lodash'
import React, {
createContext,
Dispatch,
MutableRefObject,
SetStateAction,
@ -13,44 +12,7 @@ import React, {
useReducer
} from 'react'
import FastImage from 'react-native-fast-image'
type EmojisState = {
enabled: boolean
active: boolean
emojis: { title: string; data: Mastodon.Emoji[][] }[]
shortcode: Mastodon.Emoji['shortcode'] | null
}
type EmojisAction =
| {
type: 'load'
payload: NonNullable<EmojisState['emojis']>
}
| {
type: 'activate'
payload: EmojisState['active']
}
| {
type: 'shortcode'
payload: EmojisState['shortcode']
}
const emojisReducer = (state: EmojisState, action: EmojisAction) => {
switch (action.type) {
case 'activate':
return { ...state, active: action.payload }
case 'load':
return { ...state, emojis: action.payload }
case 'shortcode':
return { ...state, shortcode: action.payload }
}
}
type ContextType = {
emojisState: EmojisState
emojisDispatch: Dispatch<EmojisAction>
}
const EmojisContext = createContext<ContextType>({} as ContextType)
import EmojisContext, { emojisReducer } from './Emojis/helpers/EmojisContext'
const prefetchEmojis = (
sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[],
@ -163,4 +125,4 @@ const ComponentEmojis: React.FC<Props> = ({
)
}
export { ComponentEmojis, EmojisContext, EmojisButton, EmojisList }
export { ComponentEmojis, EmojisButton, EmojisList }

View File

@ -1,9 +1,9 @@
import { EmojisContext } from '@components/Emojis'
import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { Pressable, StyleSheet } from 'react-native'
import EmojisContext from './helpers/EmojisContext'
const EmojisButton = React.memo(
() => {

View File

@ -1,4 +1,3 @@
import { EmojisContext } from '@components/Emojis'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
@ -16,6 +15,7 @@ import {
} from 'react-native'
import FastImage from 'react-native-fast-image'
import validUrl from 'valid-url'
import EmojisContext from './helpers/EmojisContext'
const EmojisList = React.memo(
() => {

View File

@ -0,0 +1,41 @@
import { createContext, Dispatch } from 'react'
export type EmojisState = {
enabled: boolean
active: boolean
emojis: { title: string; data: Mastodon.Emoji[][] }[]
shortcode: Mastodon.Emoji['shortcode'] | null
}
export type EmojisAction =
| {
type: 'load'
payload: NonNullable<EmojisState['emojis']>
}
| {
type: 'activate'
payload: EmojisState['active']
}
| {
type: 'shortcode'
payload: EmojisState['shortcode']
}
type ContextType = {
emojisState: EmojisState
emojisDispatch: Dispatch<EmojisAction>
}
const EmojisContext = createContext<ContextType>({} as ContextType)
export const emojisReducer = (state: EmojisState, action: EmojisAction) => {
switch (action.type) {
case 'activate':
return { ...state, active: action.payload }
case 'load':
return { ...state, emojis: action.payload }
case 'shortcode':
return { ...state, shortcode: action.payload }
}
}
export default EmojisContext

View File

@ -18,12 +18,8 @@ import {
View
} from 'react-native'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import {
ComponentEmojis,
EmojisButton,
EmojisContext,
EmojisList
} from './Emojis'
import { ComponentEmojis, EmojisButton, EmojisList } from './Emojis'
import EmojisContext from './Emojis/helpers/EmojisContext'
export interface Props {
autoFocus?: boolean
@ -114,7 +110,8 @@ const Input: React.FC<Props> = ({
styles.base,
{
borderColor: theme.border,
flexDirection: multiline ? 'column' : 'row'
flexDirection: multiline ? 'column' : 'row',
alignItems: 'stretch'
}
]}
>
@ -157,7 +154,7 @@ const Input: React.FC<Props> = ({
{title}
</Animated.Text>
) : null}
<View style={{ flexDirection: 'row' }}>
<View style={{ flexDirection: 'row', alignSelf: 'flex-end' }}>
{options?.maxLength && value?.length ? (
<Text style={[styles.maxLength, { color: theme.secondary }]}>
{value?.length} / {options.maxLength}

View File

@ -76,96 +76,98 @@ const MenuRow: React.FC<Props> = ({
}
}}
>
<View style={styles.core}>
<View style={styles.front}>
{iconFront && (
<Icon
name={iconFront}
size={StyleConstants.Font.Size.L}
color={theme[iconFrontColor]}
style={styles.iconFront}
/>
)}
{badge ? (
<View
style={{
width: 8,
height: 8,
backgroundColor: theme.red,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
/>
) : null}
<View style={styles.main}>
<Text
style={[styles.title, { color: theme.primaryDefault }]}
numberOfLines={1}
>
{title}
</Text>
</View>
</View>
{content || switchValue !== undefined || iconBack ? (
<View style={styles.back}>
{content ? (
typeof content === 'string' ? (
<Text
style={[
styles.content,
{
color: theme.secondary,
opacity: !iconBack && loading ? 0 : 1
}
]}
numberOfLines={1}
>
{content}
</Text>
) : (
content
)
) : null}
{switchValue !== undefined ? (
<Switch
value={switchValue}
onValueChange={switchOnValueChange}
disabled={switchDisabled}
trackColor={{ true: theme.blue, false: theme.disabled }}
style={{ opacity: loading ? 0 : 1 }}
/>
) : null}
{iconBack ? (
<View style={{ flex: 1 }}>
<View style={styles.core}>
<View style={styles.front}>
{iconFront && (
<Icon
name={iconBack}
name={iconFront}
size={StyleConstants.Font.Size.L}
color={theme[iconBackColor]}
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
color={theme[iconFrontColor]}
style={styles.iconFront}
/>
)}
{badge ? (
<View
style={{
width: 8,
height: 8,
backgroundColor: theme.red,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
/>
) : null}
{loading && loadingSpinkit}
<View style={styles.main}>
<Text
style={[styles.title, { color: theme.primaryDefault }]}
numberOfLines={1}
>
{title}
</Text>
</View>
</View>
{content || switchValue !== undefined || iconBack ? (
<View style={styles.back}>
{content ? (
typeof content === 'string' ? (
<Text
style={[
styles.content,
{
color: theme.secondary,
opacity: !iconBack && loading ? 0 : 1
}
]}
numberOfLines={1}
>
{content}
</Text>
) : (
content
)
) : null}
{switchValue !== undefined ? (
<Switch
value={switchValue}
onValueChange={switchOnValueChange}
disabled={switchDisabled}
trackColor={{ true: theme.blue, false: theme.disabled }}
style={{ opacity: loading ? 0 : 1 }}
/>
) : null}
{iconBack ? (
<Icon
name={iconBack}
size={StyleConstants.Font.Size.L}
color={theme[iconBackColor]}
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
/>
) : null}
{loading && loadingSpinkit}
</View>
) : null}
</View>
{description ? (
<Text style={[styles.description, { color: theme.secondary }]}>
{description}
</Text>
) : null}
</View>
</TapGestureHandler>
{description ? (
<Text style={[styles.description, { color: theme.secondary }]}>
{description}
</Text>
) : null}
</View>
)
}
const styles = StyleSheet.create({
base: {
minHeight: 46,
paddingVertical: StyleConstants.Spacing.S
minHeight: 50
},
core: {
flex: 1,
flexDirection: 'row'
flexDirection: 'row',
paddingVertical: StyleConstants.Spacing.S
},
front: {
flex: 2,

View File

@ -80,7 +80,7 @@ const displayMessage = ({
})
} else {
showMessage({
duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
duration: type === 'error' ? 3500 : duration === 'short' ? 1500 : 2500,
autoHide,
message,
description,

View File

@ -4,7 +4,6 @@ import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux'
@ -28,7 +27,6 @@ const ParseEmojis = React.memo(
adaptiveSize = false,
fontBold = false
}: Props) => {
const { t } = useTranslation('componentParse')
const { reduceMotionEnabled } = useAccessibility()
const adaptiveFontsize = useSelector(getSettingsFontsize)

View File

@ -162,7 +162,9 @@ export interface Props {
showFullLink?: boolean
numberOfLines?: number
expandHint?: string
highlighted?: boolean
disableDetails?: boolean
selectable?: boolean
}
const ParseHTML = React.memo(
@ -176,7 +178,9 @@ const ParseHTML = React.memo(
showFullLink = false,
numberOfLines = 10,
expandHint,
disableDetails = false
highlighted = false,
disableDetails = false,
selectable = false
}: Props) => {
const adaptiveFontsize = useSelector(getSettingsFontsize)
const adaptedFontsize = adaptiveScale(
@ -234,7 +238,7 @@ const ParseHTML = React.memo(
const { t } = useTranslation('componentParse')
const [expandAllow, setExpandAllow] = useState(false)
const [expanded, setExpanded] = useState(false)
const [expanded, setExpanded] = useState(highlighted)
const onTextLayout = useCallback(({ nativeEvent }) => {
if (
@ -253,6 +257,7 @@ const ParseHTML = React.memo(
numberOfLines={
expandAllow ? (expanded ? 999 : numberOfLines) : undefined
}
selectable={selectable}
/>
{expandAllow ? (
<Pressable

View File

@ -19,6 +19,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import TimelineActionsUsers from './Shared/ActionsUsers'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineTranslate from './Shared/Translate'
export interface Props {
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
@ -128,11 +129,13 @@ const TimelineDefault: React.FC<Props> = ({
{!disableDetails && actualStatus.card && (
<TimelineCard card={actualStatus.card} />
)}
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
{!disableDetails ? (
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
) : null}
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
</View>
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
{queryKey && !disableDetails && (
<TimelineActions
queryKey={queryKey}

View File

@ -340,7 +340,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 4,
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
marginHorizontal: StyleConstants.Spacing.S
}
})

View File

@ -38,7 +38,7 @@ const TimelineActionsUsers = React.memo(
'shared.actionsUsers.reblogged_by.accessibilityHint'
)}
accessibilityRole='button'
style={[styles.text, { color: theme.secondary }]}
style={[styles.text, { color: theme.blue }]}
onPress={() => {
analytics('timeline_shared_actionsusers_press_boosted', {
count: status.reblogs_count
@ -68,7 +68,7 @@ const TimelineActionsUsers = React.memo(
'shared.actionsUsers.favourited_by.accessibilityHint'
)}
accessibilityRole='button'
style={[styles.text, { color: theme.secondary }]}
style={[styles.text, { color: theme.blue }]}
onPress={() => {
analytics('timeline_shared_actionsusers_press_boosted', {
count: status.favourites_count
@ -98,10 +98,9 @@ const styles = StyleSheet.create({
base: {
flexDirection: 'row'
},
pressable: { margin: StyleConstants.Spacing.M },
text: {
...StyleConstants.FontStyle.S,
padding: StyleConstants.Spacing.S * 1.5,
...StyleConstants.FontStyle.M,
padding: StyleConstants.Spacing.S,
paddingLeft: 0,
marginRight: StyleConstants.Spacing.S
}

View File

@ -30,7 +30,9 @@ const TimelineContent = React.memo(
mentions={status.mentions}
tags={status.tags}
numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails}
selectable={highlighted}
/>
<ParseHTML
content={status.content}
@ -41,7 +43,9 @@ const TimelineContent = React.memo(
tags={status.tags}
numberOfLines={1}
expandHint={t('shared.content.expandHint')}
highlighted={highlighted}
disableDetails={disableDetails}
selectable={highlighted}
/>
</>
) : (
@ -54,6 +58,7 @@ const TimelineContent = React.memo(
tags={status.tags}
numberOfLines={highlighted ? 999 : numberOfLines}
disableDetails={disableDetails}
selectable={highlighted}
/>
)}
</>

View File

@ -26,7 +26,7 @@ const TimelineFullConversation = React.memo(
style={{
...StyleConstants.FontStyle.S,
color: theme.blue,
marginTop: StyleConstants.Font.Size.S
marginTop: StyleConstants.Spacing.S
}}
>
{t('shared.fullConversation')}

View File

@ -0,0 +1,131 @@
import analytics from '@components/analytics'
import { ParseHTML } from '@components/Parse'
import { useTranslateQuery } from '@utils/queryHooks/translate'
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, Text } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import { useSelector } from 'react-redux'
export interface Props {
highlighted: boolean
status: Mastodon.Status
}
const TimelineTranslate = React.memo(
({ highlighted, status }: Props) => {
if (!highlighted) {
return null
}
if (!status.language) {
return null
}
const { t } = useTranslation('componentTimeline')
const { theme } = useTheme()
const tootLanguage = status.language.slice(0, 2)
const settingsLanguage = useSelector(getSettingsLanguage)
if (settingsLanguage.includes(tootLanguage)) {
return null
}
let text = status.spoiler_text
? [status.spoiler_text, status.content]
: [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, '')
}
}
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
uri: status.uri,
source: status.language,
target: settingsLanguage,
text,
options: { enabled }
})
return (
<>
<Pressable
style={[styles.button, { paddingBottom: isSuccess ? 0 : undefined }]}
onPress={() => {
if (enabled) {
if (!isSuccess) {
analytics('timeline_shared_translate_retry', {
language: status.language
})
refetch()
}
} else {
analytics('timeline_shared_translate', {
language: status.language
})
setEnabled(true)
}
}}
>
<Text
style={{
...StyleConstants.FontStyle.M,
color:
isLoading || isSuccess
? theme.secondary
: isError
? theme.red
: theme.blue
}}
>
{isError
? t('shared.translate.failed')
: isSuccess
? t('shared.translate.succeed', {
provider: data?.provider,
source: data?.sourceLanguage
})
: t('shared.translate.default')}
{__DEV__ ? ` Source: ${status.language}` : undefined}
</Text>
{isLoading ? (
<Circle
size={StyleConstants.Font.Size.M}
color={theme.disabled}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : null}
</Pressable>
{data
? data.text.map((d, i) => (
<ParseHTML
key={i}
content={d}
size={'M'}
numberOfLines={999}
selectable
/>
))
: null}
</>
)
},
() => true
)
const styles = StyleSheet.create({
button: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S
}
})
export default TimelineTranslate

View File

@ -1,13 +1,14 @@
import * as ImagePicker from 'expo-image-picker'
import { Alert, Linking } from 'react-native'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import i18next from 'i18next'
import analytics from '@components/analytics'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import * as ImageManipulator from 'expo-image-manipulator'
import * as ImagePicker from 'expo-image-picker'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import i18next from 'i18next'
import { Alert, Linking } from 'react-native'
export interface Props {
mediaTypes?: ImagePicker.MediaTypeOptions
uploader: (imageInfo: ImageInfo) => void
resize?: { width?: number; height?: number } // Resize mode contain
showActionSheetWithOptions: (
options: ActionSheetOptions,
callback: (i: number) => void
@ -16,118 +17,134 @@ export interface Props {
const mediaSelector = async ({
mediaTypes = ImagePicker.MediaTypeOptions.All,
uploader,
resize,
showActionSheetWithOptions
}: Props): Promise<any> => {
showActionSheetWithOptions(
{
title: i18next.t('componentMediaSelector:title'),
options: [
i18next.t('componentMediaSelector:options.library'),
i18next.t('componentMediaSelector:options.photo'),
i18next.t('componentMediaSelector:options.cancel')
],
cancelButtonIndex: 2
},
async buttonIndex => {
if (buttonIndex === 0) {
const {
status
} = await ImagePicker.requestMediaLibraryPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t('componentMediaSelector:library.alert.title'),
i18next.t('componentMediaSelector:library.alert.message'),
[
{
text: i18next.t(
'componentMediaSelector:library.alert.buttons.cancel'
),
style: 'cancel',
onPress: () =>
analytics('mediaSelector_nopermission', { action: 'cancel' })
},
{
text: i18next.t(
'componentMediaSelector:library.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('mediaSelector_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes,
exif: false
})
if (!result.cancelled) {
// https://github.com/expo/expo/issues/11214
const fixResult = {
...result,
uri: result.uri.replace('file:/data', 'file:///data')
}
uploader(fixResult)
return
}: Props): Promise<ImageInfo> => {
return new Promise((resolve, reject) => {
const resolveResult = async (result: ImageInfo) => {
if (resize && result.type === 'image') {
let newResult: ImageManipulator.ImageResult
if (resize.width && resize.height) {
if (resize.width / resize.height > result.width / result.height) {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize: { width: resize.width } }
])
} else {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize: { height: resize.height } }
])
}
}
} else if (buttonIndex === 1) {
const { status } = await ImagePicker.requestCameraPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t('componentMediaSelector:photo.alert.title'),
i18next.t('componentMediaSelector:photo.alert.message'),
[
{
text: i18next.t(
'componentMediaSelector:photo.alert.buttons.cancel'
),
style: 'cancel',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'cancel'
})
}
},
{
text: i18next.t(
'componentMediaSelector:photo.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchCameraAsync({
mediaTypes,
exif: false
})
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize }
])
}
resolve(newResult)
} else {
resolve(result)
}
}
if (!result.cancelled) {
// https://github.com/expo/expo/issues/11214
const fixResult = {
...result,
uri: result.uri.replace('file:/data', 'file:///data')
showActionSheetWithOptions(
{
title: i18next.t('componentMediaSelector:title'),
options: [
i18next.t('componentMediaSelector:options.library'),
i18next.t('componentMediaSelector:options.photo'),
i18next.t('componentMediaSelector:options.cancel')
],
cancelButtonIndex: 2
},
async buttonIndex => {
if (buttonIndex === 0) {
const {
status
} = await ImagePicker.requestMediaLibraryPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t('componentMediaSelector:library.alert.title'),
i18next.t('componentMediaSelector:library.alert.message'),
[
{
text: i18next.t(
'componentMediaSelector:library.alert.buttons.cancel'
),
style: 'cancel',
onPress: () =>
analytics('mediaSelector_nopermission', {
action: 'cancel'
})
},
{
text: i18next.t(
'componentMediaSelector:library.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('mediaSelector_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes,
exif: false
})
if (!result.cancelled) {
await resolveResult(result)
}
}
} else if (buttonIndex === 1) {
const { status } = await ImagePicker.requestCameraPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t('componentMediaSelector:photo.alert.title'),
i18next.t('componentMediaSelector:photo.alert.message'),
[
{
text: i18next.t(
'componentMediaSelector:photo.alert.buttons.cancel'
),
style: 'cancel',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'cancel'
})
}
},
{
text: i18next.t(
'componentMediaSelector:photo.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchCameraAsync({
mediaTypes,
exif: false
})
if (!result.cancelled) {
await resolveResult(result)
}
uploader(fixResult)
return
}
}
}
}
)
)
})
}
export default mediaSelector

View File

@ -1,6 +1,6 @@
import apiInstance from '@api/instance'
import navigationRef from '@helpers/navigationRef'
import { NavigationProp, ParamListBase } from '@react-navigation/native'
import { navigationRef } from '@root/Screens'
import { store } from '@root/store'
import { SearchResult } from '@utils/queryHooks/search'
import { getInstanceUrl } from '@utils/slices/instancesSlice'

View File

@ -0,0 +1,6 @@
import { NavigationContainerRef } from '@react-navigation/native'
import { createRef } from 'react'
const navigationRef = createRef<NavigationContainerRef>()
export default navigationRef

View File

@ -0,0 +1,5 @@
import { QueryClient } from 'react-query'
const queryClient = new QueryClient()
export default queryClient

View File

@ -74,6 +74,11 @@
"expandHint": "hidden content"
},
"fullConversation": "Read conversations",
"translate": {
"default": "Translate",
"succeed": "Translated by {{provider}} from {{source}}",
"failed": "Translation failed"
},
"header": {
"shared": {
"account": {

View File

@ -102,11 +102,11 @@
},
"avatar": {
"title": "Avatar",
"description": "Available in next version"
"description": "Will be downscaled to 400x400px"
},
"banner": {
"header": {
"title": "Banner",
"description": "Available in next version"
"description": "Will be downscaled to 1500x500px"
},
"note": {
"title": "Description"

View File

@ -74,6 +74,11 @@
"expandHint": "隐藏内容"
},
"fullConversation": "阅读全部对话",
"translate": {
"default": "翻译",
"succeed": "由{{provider}}翻译自{{source}}",
"failed": "翻译失败"
},
"header": {
"shared": {
"account": {

View File

@ -102,11 +102,11 @@
},
"avatar": {
"title": "头像",
"description": "将在下一版中启用"
"description": "将缩小到400x400px"
},
"banner": {
"header": {
"title": "横幅",
"description": "将在下一版中启用"
"description": "将缩小到1500x500px"
},
"note": {
"title": "简介"

View File

@ -1,7 +1,6 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import { ParseHTML } from '@components/Parse'
import RelativeTime from '@components/RelativeTime'
import { BlurView } from '@react-native-community/blur'
@ -88,6 +87,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
emojis={item.emojis}
mentions={item.mentions}
numberOfLines={999}
selectable
/>
</ScrollView>
{item.reactions?.length ? (
@ -210,28 +210,6 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
reducedTransparencyFallbackColor={theme.backgroundDefault}
>
<SafeAreaView style={styles.base}>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
flexBasis: 44
}}
>
<HeaderLeft
content='X'
native={false}
onPress={() => navigation.goBack()}
/>
<HeaderCenter content={t('screenAnnouncements:heading')} />
<View style={{ opacity: 0 }} accessible={false}>
<HeaderRight
content='MoreHorizontal'
native={false}
onPress={() => {}}
/>
</View>
</View>
<FlatList
horizontal
data={query.data}

View File

@ -88,6 +88,14 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
return {
...composeInitialState,
timestamp: Date.now(),
attachments: {
...composeInitialState.attachments,
sensitive:
localAccount?.preferences &&
localAccount?.preferences['posting:default:sensitive']
? localAccount?.preferences['posting:default:sensitive']
: false
},
visibility:
localAccount?.preferences &&
localAccount.preferences['posting:default:visibility']
@ -397,12 +405,18 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
<Stack.Screen
name='Screen-Compose-DraftsList'
component={ComposeDraftsList}
options={{ stackPresentation: 'modal', headerShown: false }}
options={{
stackPresentation: 'modal',
...(Platform.OS === 'android' && { headerShown: false })
}}
/>
<Stack.Screen
name='Screen-Compose-EditAttachment'
component={ComposeEditAttachment}
options={{ stackPresentation: 'modal', headerShown: false }}
options={{
stackPresentation: 'modal',
...(Platform.OS === 'android' && { headerShown: false })
}}
/>
</Stack.Navigator>
</ComposeContext.Provider>

View File

@ -3,7 +3,7 @@ import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { forEach, groupBy, sortBy } from 'lodash'
import { chunk, forEach, groupBy, sortBy } from 'lodash'
import React, {
useCallback,
useContext,
@ -28,23 +28,26 @@ import ComposeContext from './utils/createContext'
import ComposeDrafts from './Root/Drafts'
import FastImage from 'react-native-fast-image'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { ComposeState } from './utils/types'
const prefetchEmojis = (
sortedEmojis: { title: string; data: Mastodon.Emoji[] }[],
sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
reduceMotionEnabled: boolean
) => {
const prefetches: { uri: string }[] = []
let requestedIndex = 0
sortedEmojis.forEach(sorted => {
sorted.data.forEach(emoji => {
if (requestedIndex > 40) {
return
}
prefetches.push({
uri: reduceMotionEnabled ? emoji.static_url : emoji.url
sorted.data.forEach(emojis =>
emojis.forEach(emoji => {
if (requestedIndex > 40) {
return
}
prefetches.push({
uri: reduceMotionEnabled ? emoji.static_url : emoji.url
})
requestedIndex++
})
requestedIndex++
})
)
})
try {
FastImage.preload(prefetches)
@ -90,10 +93,11 @@ const ComposeRoot = React.memo(
const { data: emojisData } = useEmojisQuery({})
useEffect(() => {
if (emojisData && emojisData.length) {
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []
let sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[] = []
forEach(
groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'),
(value, key) => sortedEmojis.push({ title: key, data: value })
(value, key) =>
sortedEmojis.push({ title: key, data: chunk(value, 5) })
)
composeDispatch({
type: 'emoji',

View File

@ -1,15 +1,8 @@
import analytics from '@components/analytics'
import haptics from '@components/haptics'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, {
RefObject,
useCallback,
useContext,
useEffect,
useMemo
} from 'react'
import React, { RefObject, useCallback, useContext, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import {
AccessibilityInfo,
@ -25,52 +18,15 @@ import validUrl from 'valid-url'
import updateText from '../../updateText'
import ComposeContext from '../../utils/createContext'
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
const { t } = useTranslation()
const { reduceMotionEnabled } = useAccessibility()
const { composeState, composeDispatch } = useContext(ComposeContext)
const onPress = useCallback(() => {
analytics('compose_emoji_add')
updateText({
composeState,
composeDispatch,
newText: `:${emoji.shortcode}:`,
type: 'emoji'
})
haptics('Light')
}, [composeState])
const children = useMemo(() => {
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
if (validUrl.isHttpsUri(uri)) {
return (
<FastImage
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
emoji: emoji.shortcode
})}
accessibilityHint={t(
'screenCompose:content.root.footer.emojis.accessibilityHint'
)}
source={{ uri: reduceMotionEnabled ? emoji.static_url : emoji.url }}
style={styles.emoji}
/>
)
} else {
return null
}
}, [])
return (
<Pressable key={emoji.shortcode} onPress={onPress} children={children} />
)
}
export interface Props {
accessibleRefEmojis: RefObject<SectionList>
}
const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
const { composeState } = useContext(ComposeContext)
const { composeState, composeDispatch } = useContext(ComposeContext)
const { reduceMotionEnabled } = useAccessibility()
const { theme } = useTheme()
const { t } = useTranslation()
useEffect(() => {
const tagEmojis = findNodeHandle(accessibleRefEmojis.current)
@ -86,21 +42,49 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
[]
)
const emojiList = useCallback(
section =>
section.data.map((emoji: Mastodon.Emoji) => (
<SingleEmoji key={emoji.shortcode} emoji={emoji} />
)),
[]
)
const listItem = useCallback(
({ section, index }) =>
index === 0 ? (
<View key={section.title} style={styles.emojis}>
{emojiList(section)}
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
return (
<View key={index} style={styles.emojis}>
{item.map(emoji => {
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
if (validUrl.isHttpsUri(uri)) {
return (
<Pressable
key={emoji.shortcode}
onPress={() => {
updateText({
composeState,
composeDispatch,
newText: `:${emoji.shortcode}:`,
type: 'emoji'
})
haptics('Light')
}}
>
<FastImage
accessibilityLabel={t(
'common:customEmoji.accessibilityLabel',
{
emoji: emoji.shortcode
}
)}
accessibilityHint={t(
'screenCompose:content.root.footer.emojis.accessibilityHint'
)}
source={{ uri }}
style={styles.emoji}
/>
</Pressable>
)
} else {
return null
}
})}
</View>
) : null,
[]
)
},
[composeState]
)
return (
@ -111,7 +95,7 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
horizontal
keyboardShouldPersistTaps='always'
sections={composeState.emoji.emojis || []}
keyExtractor={item => item.shortcode}
keyExtractor={item => item[0].shortcode}
renderSectionHeader={listHeader}
renderItem={listItem}
windowSize={2}

View File

@ -123,7 +123,8 @@ const addAttachment = async ({
})
}
mediaSelector({ uploader, showActionSheetWithOptions })
const result = await mediaSelector({ showActionSheetWithOptions })
await uploader(result)
}
export default addAttachment

View File

@ -31,7 +31,10 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
multiple: false,
expire: '86400'
},
attachments: { sensitive: false, uploads: [] },
attachments: {
sensitive: false,
uploads: []
},
visibility: 'public',
visibilityLock: false,
replyToStatus: undefined,

View File

@ -40,7 +40,7 @@ export type ComposeState = {
}
emoji: {
active: boolean
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined
emojis: { title: string; data: Mastodon.Emoji[][] }[] | undefined
}
poll: {
active: boolean

View File

@ -109,16 +109,28 @@ const TabMe = React.memo(
component={TabMeProfile}
options={{
stackPresentation: 'modal',
headerShown: false
...(Platform.OS === 'android' && { headerShown: false })
}}
/>
<Stack.Screen
name='Tab-Me-Push'
component={TabMePush}
options={{
options={({ navigation }) => ({
stackPresentation: 'modal',
headerShown: false
}}
headerShown: true,
headerTitle: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.push.name')} />
)
}),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
)
})}
/>
<Stack.Screen
name='Tab-Me-Settings'
@ -149,10 +161,22 @@ const TabMe = React.memo(
<Stack.Screen
name='Tab-Me-Switch'
component={TabMeSwitch}
options={{
options={({ navigation }) => ({
stackPresentation: 'modal',
headerShown: false
}}
headerShown: true,
headerTitle: t('me.stacks.switch.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.switch.name')} />
)
}),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
)
})}
/>
{sharedScreens(Stack as any)}

View File

@ -30,7 +30,6 @@ const TabMeProfile: React.FC<StackScreenProps<
>
<Stack.Screen
name='Tab-Me-Profile-Root'
component={TabMeProfileRoot}
options={{
headerTitle: t('me.stacks.profile.name'),
...(Platform.OS === 'android' && {
@ -45,7 +44,15 @@ const TabMeProfile: React.FC<StackScreenProps<
/>
)
}}
/>
>
{({ route, navigation }) => (
<TabMeProfileRoot
messageRef={messageRef}
route={route}
navigation={navigation}
/>
)}
</Stack.Screen>
<Stack.Screen
name='Tab-Me-Profile-Name'
options={{

View File

@ -1,6 +1,5 @@
import { HeaderLeft, HeaderRight } from '@components/Header'
import Input from '@components/Input'
import { displayMessage } from '@components/Message'
import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants'
@ -79,32 +78,20 @@ const TabMeProfileFields: React.FC<StackScreenProps<
content='Save'
onPress={async () => {
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.note.title',
succeed: true,
failed: true
},
type: 'fields_attributes',
data: newFields
.filter(field => field.name.length && field.value.length)
.map(field => ({ name: field.name, value: field.value }))
}).then(() => {
navigation.navigate('Tab-Me-Profile-Root')
})
.then(() => {
navigation.navigate('Tab-Me-Profile-Root')
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.succeed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'success'
})
})
.catch(() => {
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'error'
})
})
}}
/>
)

View File

@ -1,6 +1,5 @@
import { HeaderLeft, HeaderRight } from '@components/Header'
import Input from '@components/Input'
import { displayMessage } from '@components/Message'
import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants'
@ -65,28 +64,19 @@ const TabMeProfileName: React.FC<StackScreenProps<
loading={status === 'loading'}
content='Save'
onPress={async () => {
mutateAsync({ type: 'display_name', data: displayName })
.then(() => {
navigation.navigate('Tab-Me-Profile-Root')
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.succeed', {
type: t('me.profile.root.name.title')
}),
mode,
type: 'success'
})
})
.catch(() => {
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.name.title')
}),
mode,
type: 'error'
})
})
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.name.title',
succeed: true,
failed: true
},
type: 'display_name',
data: displayName
}).then(() => {
navigation.navigate('Tab-Me-Profile-Root')
})
}}
/>
)

View File

@ -1,6 +1,5 @@
import { HeaderLeft, HeaderRight } from '@components/Header'
import Input from '@components/Input'
import { displayMessage } from '@components/Message'
import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants'
@ -65,28 +64,19 @@ const TabMeProfileNote: React.FC<StackScreenProps<
loading={status === 'loading'}
content='Save'
onPress={async () => {
mutateAsync({ type: 'note', data: newNote })
.then(() => {
navigation.navigate('Tab-Me-Profile-Root')
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.succeed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'success'
})
})
.catch(() => {
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'error'
})
})
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.note.title',
succeed: true,
failed: true
},
type: 'note',
data: newNote
}).then(() => {
navigation.navigate('Tab-Me-Profile-Root')
})
}}
/>
)

View File

@ -1,21 +1,29 @@
import analytics from '@components/analytics'
import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import React, { useCallback } from 'react'
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
import { ScrollView } from 'react-native-gesture-handler'
import { useDispatch } from 'react-redux'
import ProfileAvatarHeader from './Root/AvatarHeader'
const TabMeProfileRoot: React.FC<StackScreenProps<
Nav.TabMeProfileStackParamList,
'Tab-Me-Profile-Root'
>> = ({ navigation }) => {
> & { messageRef: RefObject<FlashMessage> }> = ({ messageRef, navigation }) => {
const { mode } = useTheme()
const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet()
const { data, isLoading } = useProfileQuery({})
const { mutate } = useProfileMutation()
const { mutateAsync } = useProfileMutation()
const dispatch = useDispatch()
const onPressVisibility = useCallback(() => {
showActionSheetWithOptions(
@ -32,41 +40,90 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
async buttonIndex => {
switch (buttonIndex) {
case 0:
mutate({ type: 'source[privacy]', data: 'public' })
break
case 1:
mutate({ type: 'source[privacy]', data: 'unlisted' })
break
case 2:
mutate({ type: 'source[privacy]', data: 'private' })
const indexVisibilityMapping = [
'public',
'unlisted',
'private'
] as ['public', 'unlisted', 'private']
if (data?.source.privacy !== indexVisibilityMapping[buttonIndex]) {
analytics('me_profile_visibility', {
current: t(
`me.profile.root.visibility.options.${data?.source.privacy}`
),
new: indexVisibilityMapping[buttonIndex]
})
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.visibility.title',
succeed: false,
failed: true
},
type: 'source[privacy]',
data: indexVisibilityMapping[buttonIndex]
}).then(() => dispatch(updateAccountPreferences()))
}
break
}
}
)
}, [])
}, [data?.source.privacy])
const onPressSensitive = useCallback(() => {
if (data?.source.sensitive === undefined) {
mutate({ type: 'source[sensitive]', data: true })
} else {
mutate({ type: 'source[sensitive]', data: !data.source.sensitive })
}
analytics('me_profile_sensitive', {
current: data?.source.sensitive,
new: data?.source.sensitive === undefined ? true : !data.source.sensitive
})
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.sensitive.title',
succeed: false,
failed: true
},
type: 'source[sensitive]',
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
}).then(() => dispatch(updateAccountPreferences()))
}, [data?.source.sensitive])
const onPressLock = useCallback(() => {
if (data?.locked === undefined) {
mutate({ type: 'locked', data: true })
} else {
mutate({ type: 'locked', data: !data.locked })
}
analytics('me_profile_lock', {
current: data?.locked,
new: data?.locked === undefined ? true : !data.locked
})
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.lock.title',
succeed: false,
failed: true
},
type: 'locked',
data: data?.locked === undefined ? true : !data.locked
})
}, [data?.locked])
const onPressBot = useCallback(() => {
if (data?.bot === undefined) {
mutate({ type: 'bot', data: true })
} else {
mutate({ type: 'bot', data: !data?.bot })
}
analytics('me_profile_bot', {
current: data?.bot,
new: data?.bot === undefined ? true : !data.bot
})
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.bot.title',
succeed: false,
failed: true
},
type: 'bot',
data: data?.bot === undefined ? true : !data.bot
})
}, [data?.bot])
return (
@ -84,43 +141,18 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
})
}}
/>
<MenuRow
title={t('me.profile.root.avatar.title')}
description={t('me.profile.root.avatar.description')}
// content={
// <GracefullyImage
// style={{ flex: 1 }}
// uri={{
// original: data?.avatar_static
// }}
// />
// }
// loading={isLoading}
// iconBack='ChevronRight'
/>
<MenuRow
title={t('me.profile.root.banner.title')}
description={t('me.profile.root.banner.description')}
// content={
// <GracefullyImage
// style={{ flex: 1 }}
// uri={{
// original: data?.header_static
// }}
// />
// }
// loading={isLoading}
// iconBack='ChevronRight'
/>
<ProfileAvatarHeader type='avatar' messageRef={messageRef} />
<ProfileAvatarHeader type='header' messageRef={messageRef} />
<MenuRow
title={t('me.profile.root.note.title')}
content={data?.source.note}
loading={isLoading}
iconBack='ChevronRight'
onPress={() => {
navigation.navigate('Tab-Me-Profile-Note', {
note: data?.source?.note || ''
})
data &&
navigation.navigate('Tab-Me-Profile-Note', {
note: data.source?.note
})
}}
/>
<MenuRow

View File

@ -0,0 +1,53 @@
import mediaSelector from '@components/mediaSelector'
import { MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import { useTheme } from '@utils/styles/ThemeManager'
import * as ImagePicker from 'expo-image-picker'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
export interface Props {
type: 'avatar' | 'header'
messageRef: RefObject<FlashMessage>
}
const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
const { mode } = useTheme()
const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet()
const query = useProfileQuery({})
const mutation = useProfileMutation()
return (
<MenuRow
title={t(`me.profile.root.${type}.title`)}
description={t(`me.profile.root.${type}.description`)}
loading={query.isLoading || mutation.isLoading}
iconBack='ChevronRight'
onPress={async () => {
const image = await mediaSelector({
showActionSheetWithOptions,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
resize: { width: 400, height: 400 }
})
mutation.mutate({
mode,
messageRef,
message: {
text: `me.profile.root.${type}.title`,
succeed: true,
failed: true
},
type,
data: image.uri
})
}}
/>
)
}
export default ProfileAvatarHeader

View File

@ -1,42 +1,173 @@
import { HeaderCenter, HeaderLeft } from '@components/Header'
import { StackScreenProps } from '@react-navigation/stack'
import React from 'react'
import analytics from '@components/analytics'
import Button from '@components/Button'
import { MenuContainer, MenuRow } from '@components/Menu'
import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
import {
clearPushLoading,
getInstanceAccount,
getInstancePush,
getInstanceUri
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import * as Notifications from 'expo-notifications'
import * as WebBrowser from 'expo-web-browser'
import React, { useState, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import TabMePushRoot from './Push/Root'
import { AppState, Linking, ScrollView } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
const Stack = createNativeStackNavigator<Nav.TabMePushStackParamList>()
const TabMePush: React.FC<StackScreenProps<
Nav.TabMeStackParamList,
'Tab-Me-Push'
>> = ({ navigation }) => {
const TabMePush: React.FC = () => {
const { t } = useTranslation('screenTabs')
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.acct === next?.acct
)
const instanceUri = useSelector(getInstanceUri)
const dispatch = useDispatch()
const instancePush = useSelector(getInstancePush)
const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
const checkPush = async () => {
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
}
useEffect(() => {
checkPush()
AppState.addEventListener('change', checkPush)
return () => {
AppState.removeEventListener('change', checkPush)
}
}, [])
useEffect(() => {
dispatch(clearPushLoading())
}, [])
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
const alerts = useMemo(() => {
return instancePush?.alerts
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
'follow',
'favourite',
'reblog',
'mention',
'poll'
]).map(alert => (
<MenuRow
key={alert}
title={t(`me.push.${alert}.heading`)}
switchDisabled={
!pushEnabled || !instancePush.global.value || isLoading
}
switchValue={instancePush?.alerts[alert].value}
switchOnValueChange={() => {
analytics(`me_push_${alert}`, {
current: instancePush?.alerts[alert].value,
new: !instancePush?.alerts[alert].value
})
dispatch(
updateInstancePushAlert({
changed: alert,
alerts: {
...instancePush?.alerts,
[alert]: {
...instancePush?.alerts[alert],
value: !instancePush?.alerts[alert].value
}
}
})
)
}}
/>
))
: null
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
return (
<Stack.Navigator
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
>
<Stack.Screen
name='Tab-Me-Push-Root'
component={TabMePushRoot}
options={{
headerTitle: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.push.name')} />
)
}),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
)
}}
/>
</Stack.Navigator>
<ScrollView>
{pushEnabled === false ? (
<MenuContainer>
<Button
type='text'
content={
pushCanAskAgain
? t('me.push.enable.direct')
: t('me.push.enable.settings')
}
style={{
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
onPress={async () => {
if (pushCanAskAgain) {
analytics('me_push_enabled_dialogue')
const result = await Notifications.requestPermissionsAsync()
setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain)
} else {
analytics('me_push_enabled_setting')
Linking.openSettings()
}
}}
/>
</MenuContainer>
) : null}
<MenuContainer>
<MenuRow
title={t('me.push.global.heading', {
acct: `@${instanceAccount?.acct}@${instanceUri}`
})}
description={t('me.push.global.description')}
loading={instancePush?.global.loading}
switchDisabled={!pushEnabled || isLoading}
switchValue={
pushEnabled === false ? false : instancePush?.global.value
}
switchOnValueChange={() => {
analytics('me_push_global', {
current: instancePush?.global.value,
new: !instancePush?.global.value
})
dispatch(updateInstancePush(!instancePush?.global.value))
}}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('me.push.decode.heading')}
description={t('me.push.decode.description')}
loading={instancePush?.decode.loading}
switchDisabled={
!pushEnabled || !instancePush?.global.value || isLoading
}
switchValue={instancePush?.decode.value}
switchOnValueChange={() => {
analytics('me_push_decode', {
current: instancePush?.decode.value,
new: !instancePush?.decode.value
})
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
}}
/>
<MenuRow
title={t('me.push.howitworks')}
iconBack='ExternalLink'
onPress={() => {
analytics('me_push_howitworks')
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
}}
/>
</MenuContainer>
<MenuContainer>{alerts}</MenuContainer>
</ScrollView>
)
}

View File

@ -1,163 +0,0 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
import {
clearPushLoading,
getInstanceAccount,
getInstancePush,
getInstanceUri
} from '@utils/slices/instancesSlice'
import * as WebBrowser from 'expo-web-browser'
import * as Notifications from 'expo-notifications'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler'
import { useDispatch, useSelector } from 'react-redux'
import layoutAnimation from '@utils/styles/layoutAnimation'
import Button from '@components/Button'
import { StyleConstants } from '@utils/styles/constants'
import { AppState, Linking } from 'react-native'
import { StackScreenProps } from '@react-navigation/stack'
const TabMePushRoot: React.FC<StackScreenProps<
Nav.TabMeStackParamList,
'Tab-Me-Push'
>> = () => {
const { t } = useTranslation('screenTabs')
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.acct === next?.acct
)
const instanceUri = useSelector(getInstanceUri)
const dispatch = useDispatch()
const instancePush = useSelector(getInstancePush)
const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
const checkPush = async () => {
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
}
useEffect(() => {
checkPush()
AppState.addEventListener('change', checkPush)
return () => {
AppState.removeEventListener('change', checkPush)
}
}, [])
useEffect(() => {
dispatch(clearPushLoading())
}, [])
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
const alerts = useMemo(() => {
return instancePush?.alerts
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
'follow',
'favourite',
'reblog',
'mention',
'poll'
]).map(alert => (
<MenuRow
key={alert}
title={t(`me.push.${alert}.heading`)}
switchDisabled={
!pushEnabled || !instancePush.global.value || isLoading
}
switchValue={instancePush?.alerts[alert].value}
switchOnValueChange={() =>
dispatch(
updateInstancePushAlert({
changed: alert,
alerts: {
...instancePush?.alerts,
[alert]: {
...instancePush?.alerts[alert],
value: !instancePush?.alerts[alert].value
}
}
})
)
}
/>
))
: null
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
return (
<ScrollView>
{pushEnabled === false ? (
<MenuContainer>
<Button
type='text'
content={
pushCanAskAgain
? t('me.push.enable.direct')
: t('me.push.enable.settings')
}
style={{
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
onPress={async () => {
if (pushCanAskAgain) {
const result = await Notifications.requestPermissionsAsync()
setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain)
} else {
Linking.openSettings()
}
}}
/>
</MenuContainer>
) : null}
<MenuContainer>
<MenuRow
title={t('me.push.global.heading', {
acct: `@${instanceAccount?.acct}@${instanceUri}`
})}
description={t('me.push.global.description')}
loading={instancePush?.global.loading}
switchDisabled={!pushEnabled || isLoading}
switchValue={
pushEnabled === false ? false : instancePush?.global.value
}
switchOnValueChange={() =>
dispatch(updateInstancePush(!instancePush?.global.value))
}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('me.push.decode.heading')}
description={t('me.push.decode.description')}
loading={instancePush?.decode.loading}
switchDisabled={
!pushEnabled || !instancePush?.global.value || isLoading
}
switchValue={instancePush?.decode.value}
switchOnValueChange={() =>
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
}
/>
<MenuRow
title={t('me.push.howitworks')}
iconBack='ExternalLink'
onPress={() =>
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
}
/>
</MenuContainer>
<MenuContainer>{alerts}</MenuContainer>
</ScrollView>
)
}
export default TabMePushRoot

View File

@ -2,65 +2,25 @@ import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { useListsQuery } from '@utils/queryHooks/lists'
import React, { useMemo } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
const Collections: React.FC = () => {
const { t, i18n } = useTranslation('screenTabs')
const { t } = useTranslation('screenTabs')
const navigation = useNavigation()
const listsQuery = useListsQuery({
options: {
notifyOnChangeProps: []
notifyOnChangeProps: ['data']
}
})
const rowLists = useMemo(() => {
if (listsQuery.isSuccess && listsQuery.data?.length) {
return (
<MenuRow
iconFront='List'
iconBack='ChevronRight'
title={t('me.stacks.lists.name')}
onPress={() => navigation.navigate('Tab-Me-Lists')}
/>
)
} else {
return null
}
}, [listsQuery.isSuccess, listsQuery.data, i18n.language])
const announcementsQuery = useAnnouncementQuery({
showAll: true,
options: {
notifyOnChangeProps: []
notifyOnChangeProps: ['data']
}
})
const rowAnnouncements = useMemo(() => {
if (announcementsQuery.isSuccess && announcementsQuery.data?.length) {
const amount = announcementsQuery.data.filter(
announcement => !announcement.read
).length
return (
<MenuRow
iconFront='Clipboard'
iconBack='ChevronRight'
title={t('screenAnnouncements:heading')}
content={
amount
? t('me.root.announcements.content.unread', {
amount
})
: t('me.root.announcements.content.read')
}
onPress={() =>
navigation.navigate('Screen-Announcements', { showAll: true })
}
/>
)
} else {
return null
}
}, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language])
return (
<MenuContainer>
@ -82,8 +42,34 @@ const Collections: React.FC = () => {
title={t('me.stacks.favourites.name')}
onPress={() => navigation.navigate('Tab-Me-Favourites')}
/>
{rowLists}
{rowAnnouncements}
{listsQuery.data?.length ? (
<MenuRow
iconFront='List'
iconBack='ChevronRight'
title={t('me.stacks.lists.name')}
onPress={() => navigation.navigate('Tab-Me-Lists')}
/>
) : null}
{announcementsQuery.data?.length ? (
<MenuRow
iconFront='Clipboard'
iconBack='ChevronRight'
title={t('screenAnnouncements:heading')}
content={
announcementsQuery.data.filter(announcement => !announcement.read)
.length
? t('me.root.announcements.content.unread', {
amount: announcementsQuery.data.filter(
announcement => !announcement.read
).length
})
: t('me.root.announcements.content.read')
}
onPress={() =>
navigation.navigate('Screen-Announcements', { showAll: true })
}
/>
) : null}
</MenuContainer>
)
}

View File

@ -1,47 +1,152 @@
import { HeaderCenter, HeaderLeft } from '@components/Header'
import { StackScreenProps } from '@react-navigation/stack'
import React from 'react'
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import ComponentInstance from '@components/Instance'
import { useNavigation } from '@react-navigation/native'
import {
getInstanceActive,
getInstances,
Instance,
updateInstanceActive
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import TabMeSwitchRoot from './Switch/Root'
import {
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
View
} from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
const Stack = createNativeStackNavigator()
interface Props {
instance: Instance
selected?: boolean
}
const TabMeSwitch: React.FC<StackScreenProps<
Nav.TabMeStackParamList,
'Tab-Me-Switch'
>> = ({ navigation }) => {
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
const queryClient = useQueryClient()
const navigation = useNavigation()
const dispatch = useDispatch()
return (
<Button
type='text'
selected={selected}
style={styles.button}
content={`@${instance.account.acct}@${instance.uri}${
selected ? ' ✓' : ''
}`}
onPress={() => {
haptics('Light')
analytics('switch_existing_press')
dispatch(updateInstanceActive(instance))
queryClient.clear()
navigation.goBack()
}}
/>
)
}
const TabMeSwitch: React.FC = () => {
const { t } = useTranslation('screenTabs')
const { theme } = useTheme()
const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive, () => true)
const scrollViewRef = useRef<ScrollView>(null)
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<Stack.Navigator
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
<ScrollView
ref={scrollViewRef}
style={styles.base}
keyboardShouldPersistTaps='always'
>
<Stack.Screen
name='Screen-Me-Switch-Root'
component={TabMeSwitchRoot}
options={{
headerTitle: t('me.stacks.switch.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.switch.name')} />
)
}),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
)
}}
/>
</Stack.Navigator>
<View
style={[styles.firstSection, { borderBottomColor: theme.border }]}
>
<Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.existing')}
</Text>
<View style={styles.accountButtons}>
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(
`${b.uri}${b.account.acct}`
)
)
.map((instance, index) => {
const localAccount = instances[instanceActive!]
return (
<AccountButton
key={index}
instance={instance}
selected={
instance.url === localAccount.url &&
instance.token === localAccount.token &&
instance.account.id === localAccount.account.id
}
/>
)
})
: null}
</View>
</View>
<View style={styles.secondSection}>
<Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.new')}
</Text>
<ComponentInstance
scrollViewRef={scrollViewRef}
disableHeaderImage
goBack
/>
</View>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
base: {
marginBottom: StyleConstants.Spacing.L * 2
},
header: {
...StyleConstants.FontStyle.M,
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S
},
firstSection: {
marginTop: StyleConstants.Spacing.S,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.S,
borderBottomWidth: StyleSheet.hairlineWidth
},
secondSection: {
paddingTop: StyleConstants.Spacing.M
},
accountButtons: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M
},
button: {
marginBottom: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.M
}
})
export default TabMeSwitch

View File

@ -1,139 +0,0 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import ComponentInstance from '@components/Instance'
import { useNavigation } from '@react-navigation/native'
import {
getInstanceActive,
getInstances,
Instance,
updateInstanceActive
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
interface Props {
instance: Instance
selected?: boolean
}
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
const queryClient = useQueryClient()
const navigation = useNavigation()
const dispatch = useDispatch()
return (
<Button
type='text'
selected={selected}
style={styles.button}
content={`@${instance.account.acct}@${instance.uri}${
selected ? ' ✓' : ''
}`}
onPress={() => {
haptics('Light')
analytics('switch_existing_press')
dispatch(updateInstanceActive(instance))
queryClient.clear()
navigation.goBack()
}}
/>
)
}
const TabMeSwitchRoot: React.FC = () => {
const { t } = useTranslation('screenTabs')
const { theme } = useTheme()
const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive, () => true)
const scrollViewRef = useRef<ScrollView>(null)
return (
<ScrollView
ref={scrollViewRef}
style={styles.base}
keyboardShouldPersistTaps='always'
>
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
<Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.existing')}
</Text>
<View style={styles.accountButtons}>
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(
`${b.uri}${b.account.acct}`
)
)
.map((instance, index) => {
const localAccount = instances[instanceActive!]
return (
<AccountButton
key={index}
instance={instance}
selected={
instance.url === localAccount.url &&
instance.token === localAccount.token &&
instance.account.id === localAccount.account.id
}
/>
)
})
: null}
</View>
</View>
<View style={styles.secondSection}>
<Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.new')}
</Text>
<ComponentInstance
scrollViewRef={scrollViewRef}
disableHeaderImage
goBack
/>
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
base: {
marginBottom: StyleConstants.Spacing.L
},
header: {
...StyleConstants.FontStyle.M,
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S
},
firstSection: {
marginTop: StyleConstants.Spacing.S,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.S,
borderBottomWidth: StyleSheet.hairlineWidth
},
secondSection: {
paddingTop: StyleConstants.Spacing.M
},
accountButtons: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M
},
button: {
marginBottom: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.M
}
})
export default TabMeSwitchRoot

View File

@ -34,6 +34,7 @@ const AccountInformationFields = React.memo(
emojis={account.emojis}
showFullLink
numberOfLines={5}
selectable
/>
{field.verified_at ? (
<Icon
@ -51,6 +52,7 @@ const AccountInformationFields = React.memo(
emojis={account.emojis}
showFullLink
numberOfLines={5}
selectable
/>
</View>
</View>
@ -58,7 +60,7 @@ const AccountInformationFields = React.memo(
</View>
)
},
() => true
(_, next) => next.account === undefined
)
const styles = StyleSheet.create({

View File

@ -28,11 +28,11 @@ const AccountInformationNote = React.memo(
return (
<View style={styles.note}>
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} />
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} selectable />
</View>
)
},
() => true
(_, next) => next.account === undefined
)
const styles = StyleSheet.create({

View File

@ -14,11 +14,11 @@ import { debounce } from 'lodash'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Platform, StyleSheet, Text, TextInput, View } from 'react-native'
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript'
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript/native-stack'
import {
NativeStackNavigationEventMap,
NativeStackNavigatorProps
} from 'react-native-screens/lib/typescript/types'
} from 'react-native-screens/lib/typescript/native-stack/types'
export type BaseScreens =
| Nav.TabLocalStackParamList
@ -150,17 +150,13 @@ const sharedScreens = (
<View style={styles.searchBar}>
<TextInput
editable={false}
children={
<Text
style={[
styles.textInput,
{
color: theme.primaryDefault
}
]}
children={t('shared.search.header.prefix')}
/>
}
style={[
styles.textInput,
{
color: theme.primaryDefault
}
]}
defaultValue={t('shared.search.header.prefix')}
/>
<TextInput
accessibilityRole='search'

View File

@ -1,12 +1,12 @@
import Constants from 'expo-constants'
import * as Updates from 'expo-updates'
import { Constants } from 'react-native-unimodules'
import * as Sentry from 'sentry-expo'
import log from './log'
const sentry = () => {
log('log', 'Sentry', 'initializing')
Sentry.init({
dsn: Constants.manifest.extra.sentryDSN,
dsn: Constants.manifest?.extra?.sentryDSN,
enableInExpoDevelopment: false,
debug:
__DEV__ ||

View File

@ -1,9 +1,12 @@
import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import { queryClient } from '@root/App'
import queryClient from '@helpers/queryClient'
import { AxiosError } from 'axios'
import i18next from 'i18next'
import { RefObject } from 'react'
import FlashMessage from 'react-native-flash-message'
import { useMutation, useQuery, UseQueryOptions } from 'react-query'
import { QueryKeyAccount } from './account'
type AccountWithSource = Mastodon.Account &
Required<Pick<Mastodon.Account, 'source'>>
@ -26,7 +29,7 @@ const useProfileQuery = <TData = AccountWithSource>({
return useQuery(queryKey, queryFunction, options)
}
type MutationVarsProfile =
type MutationVarsProfileBase =
| { type: 'display_name'; data: string }
| { type: 'note'; data: string }
| { type: 'avatar'; data: string }
@ -46,6 +49,16 @@ type MutationVarsProfile =
data: { name: string; value: string }[]
}
type MutationVarsProfile = MutationVarsProfileBase & {
mode: 'light' | 'dark'
messageRef: RefObject<FlashMessage>
message: {
text: string
succeed: boolean
failed: boolean
}
}
const mutationFunction = async ({ type, data }: MutationVarsProfile) => {
const formData = new FormData()
if (type === 'fields_attributes') {
@ -109,8 +122,33 @@ const useProfileMutation = () => {
return oldData
},
onError: (_, variables, context) => {
onError: (err, variables, context) => {
queryClient.setQueryData(queryKey, context)
haptics('Error')
if (variables.message.failed) {
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.failed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
...(err && { description: err.message }),
mode: variables.mode,
type: 'error'
})
}
},
onSuccess: (_, variables) => {
if (variables.message.succeed) {
haptics('Success')
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.succeed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
mode: variables.mode,
type: 'success'
})
}
},
onSettled: () => {
queryClient.invalidateQueries(queryKey)

View File

@ -1,6 +1,6 @@
import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { queryClient } from '@root/App'
import queryClient from '@helpers/queryClient'
import { store } from '@root/store'
import { getInstanceNotificationsFilter } from '@utils/slices/instancesSlice'
import { AxiosError } from 'axios'

View File

@ -1,4 +1,4 @@
import { queryClient } from '@root/App'
import queryClient from '@helpers/queryClient'
import { InfiniteData } from 'react-query'
import { MutationVarsTimelineDeleteItem } from '../timeline'

View File

@ -1,4 +1,4 @@
import { queryClient } from '@root/App'
import queryClient from '@helpers/queryClient'
import { findIndex } from 'lodash'
import { InfiniteData } from 'react-query'
import {

View File

@ -0,0 +1,65 @@
import apiGeneral from '@api/general'
import haptics from '@components/haptics'
import { AxiosError } from 'axios'
import { Buffer } from 'buffer'
import Constants from 'expo-constants'
import { useQuery, UseQueryOptions } from 'react-query'
type Translations = {
provider: string
sourceLanguage: string
text: string[]
}
export type QueryKeyTranslate = [
'Translate',
{
uri: string
source: string
target: string
text: string[]
}
]
export const TRANSLATE_SERVER = __DEV__
? 'testtranslate.tooot.app'
: 'translate.tooot.app'
const queryFunction = async ({ queryKey }: { queryKey: QueryKeyTranslate }) => {
const key = Constants.manifest.extra?.translateKey
if (!key) {
return Promise.reject()
}
const { uri, source, target, text } = queryKey[1]
const uriEncoded = Buffer.from(uri.replace(/https?:\/\//, ''))
.toString('base64')
.replace('+', '-')
.replace('/', '_')
.replace(/=+$/, '')
const original = Buffer.from(JSON.stringify({ source, text })).toString(
'base64'
)
const res = await apiGeneral<Translations>({
domain: TRANSLATE_SERVER,
method: 'get',
url: `v1/translate/${uriEncoded}/${target}`,
headers: { key, original }
})
haptics('Light')
return res.body
}
const useTranslateQuery = ({
options,
...queryKeyParams
}: QueryKeyTranslate[1] & {
options?: UseQueryOptions<Translations, AxiosError, Translations>
}) => {
const queryKey: QueryKeyTranslate = ['Translate', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
}
export { useTranslateQuery }

View File

@ -1,6 +1,6 @@
import apiGeneral from '@api/general'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import apiGeneral from '@api/general'
import { Constants } from 'react-native-unimodules'
export const retriveVersionLatest = createAsyncThunk(

View File

@ -9,10 +9,12 @@
"skipLibCheck": true,
"resolveJsonModule": true,
"strict": true,
"strictFunctionTypes": false,
"baseUrl": "./",
"paths": {
"@api/*": ["./src/api/*"],
"@components/*": ["./src/components/*"],
"@helpers/*": ["./src/helpers/*"],
"@screens/*": ["./src/screens/*"],
"@utils/*": ["./src/utils/*"],
"@root/*": ["./src/*"]

2761
yarn.lock

File diff suppressed because it is too large Load Diff