From 45681fc1f5e154ff1a3fafc7ed855ea64d6f027f Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sat, 20 Feb 2021 19:12:44 +0100 Subject: [PATCH] Restructure removing remote --- app.config.ts | 3 + ios/Podfile.lock | 7 + package.json | 1 + src/@types/mastodon.d.ts | 13 + src/@types/react-navigation.d.ts | 2 +- src/Screens.tsx | 19 +- src/api/general.ts | 87 ++++ src/api/{client.ts => instance.ts} | 69 ++- src/api/websocket.ts | 8 +- src/components/Instance.tsx | 73 ++-- src/components/Instance/Auth.tsx | 9 +- src/components/Timeline.tsx | 4 +- src/components/Timeline/Conversation.tsx | 15 +- src/components/Timeline/Default.tsx | 10 +- src/components/Timeline/Notifications.tsx | 10 +- src/i18n/en/components/instance.ts | 24 +- src/i18n/en/screens/meSettings.ts | 4 - src/i18n/i18n.ts | 3 +- src/i18n/zh-Hans/_all.ts | 1 + src/i18n/zh-Hans/components/instance.ts | 22 +- src/i18n/zh-Hans/screens/meSettings.ts | 7 +- .../zh-Hans/screens/meSettingsNotification.ts | 24 ++ src/screens/Compose.tsx | 16 +- src/screens/Compose/DraftsList/Root.tsx | 22 +- src/screens/Compose/EditAttachment.tsx | 5 +- src/screens/Compose/Root/Drafts.tsx | 8 +- .../Compose/Root/Footer/addAttachment.ts | 5 +- src/screens/Compose/Root/Header.tsx | 11 +- src/screens/Compose/Root/Header/PostingAs.tsx | 15 +- src/screens/Compose/utils/parseState.ts | 4 +- src/screens/Compose/utils/post.ts | 5 +- src/screens/Tabs.tsx | 70 +-- src/screens/Tabs/Local.tsx | 6 +- src/screens/Tabs/Me.tsx | 14 + src/screens/Tabs/Me/Notification.tsx | 37 ++ src/screens/Tabs/Me/Root.tsx | 12 +- src/screens/Tabs/Me/Root/Logout.tsx | 8 +- src/screens/Tabs/Me/Root/MyInfo.tsx | 8 +- src/screens/Tabs/Me/Root/Settings.tsx | 5 - src/screens/Tabs/Me/Settings/App.tsx | 13 + src/screens/Tabs/Me/Settings/Dev.tsx | 17 +- src/screens/Tabs/Me/Settings/Tooot.tsx | 6 +- src/screens/Tabs/Me/Switch/Root.tsx | 22 +- src/screens/Tabs/Notifications.tsx | 4 +- src/screens/Tabs/Public.tsx | 8 +- .../Tabs/Shared/Account/Information.tsx | 4 +- .../Shared/Account/Information/Account.tsx | 17 +- src/startup/netInfo.ts | 22 +- src/store.ts | 39 +- src/utils/queryHooks/account.ts | 5 +- src/utils/queryHooks/accountCheck.ts | 35 -- src/utils/queryHooks/announcement.ts | 11 +- src/utils/queryHooks/apps.ts | 13 +- src/utils/queryHooks/emojis.ts | 5 +- src/utils/queryHooks/instance.ts | 21 +- src/utils/queryHooks/lists.ts | 5 +- src/utils/queryHooks/push.ts | 24 ++ src/utils/queryHooks/relationship.ts | 11 +- src/utils/queryHooks/relationships.ts | 5 +- src/utils/queryHooks/search.ts | 5 +- src/utils/queryHooks/timeline.ts | 68 +-- src/utils/slices/contextsSlice.ts | 4 +- src/utils/slices/instances/add.ts | 87 ++++ src/utils/slices/instances/push/disable.ts | 21 + src/utils/slices/instances/push/enable.ts | 61 +++ src/utils/slices/instances/remove.ts | 40 ++ .../instances/updateAccountPreferences.ts | 12 + src/utils/slices/instances/updatePush.ts | 17 + src/utils/slices/instancesSlice.ts | 397 +++++++----------- src/utils/slices/settingsSlice.ts | 19 +- yarn.lock | 70 ++- 71 files changed, 998 insertions(+), 756 deletions(-) create mode 100644 src/api/general.ts rename src/api/{client.ts => instance.ts} (69%) create mode 100644 src/i18n/zh-Hans/screens/meSettingsNotification.ts create mode 100644 src/screens/Tabs/Me/Notification.tsx delete mode 100644 src/utils/queryHooks/accountCheck.ts create mode 100644 src/utils/queryHooks/push.ts create mode 100644 src/utils/slices/instances/add.ts create mode 100644 src/utils/slices/instances/push/disable.ts create mode 100644 src/utils/slices/instances/push/enable.ts create mode 100644 src/utils/slices/instances/remove.ts create mode 100644 src/utils/slices/instances/updateAccountPreferences.ts create mode 100644 src/utils/slices/instances/updatePush.ts diff --git a/app.config.ts b/app.config.ts index bf7bb859..21412afe 100644 --- a/app.config.ts +++ b/app.config.ts @@ -30,6 +30,9 @@ export default (): ExpoConfig => ({ } ] }, + ios: { + bundleIdentifier: 'com.xmflsct.app.tooot' + }, android: { versionCode: 4, package: 'com.xmflsct.app.tooot', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 58ec9d41..f6c1c04a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -49,6 +49,9 @@ PODS: - UMCore - UMPermissionsInterface - UMTaskManagerInterface + - EXNotifications (0.8.2): + - UMCore + - UMPermissionsInterface - EXPermissions (10.0.0): - UMCore - UMPermissionsInterface @@ -499,6 +502,7 @@ DEPENDENCIES: - EXLinearGradient (from `../node_modules/expo-linear-gradient/ios`) - EXLocalization (from `../node_modules/expo-localization/ios`) - EXLocation (from `../node_modules/expo-location/ios`) + - EXNotifications (from `../node_modules/expo-notifications/ios`) - EXPermissions (from `../node_modules/expo-permissions/ios`) - EXRandom (from `../node_modules/expo-random/ios`) - EXScreenCapture (from `../node_modules/expo-screen-capture/ios`) @@ -622,6 +626,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-localization/ios" EXLocation: :path: "../node_modules/expo-location/ios" + EXNotifications: + :path: "../node_modules/expo-notifications/ios" EXPermissions: :path: "../node_modules/expo-permissions/ios" EXRandom: @@ -769,6 +775,7 @@ SPEC CHECKSUMS: EXLinearGradient: c803fbd1aa974be038177b1e45524bc35759fe9c EXLocalization: 8b9463c81843da214476b541a27811dd885c9a76 EXLocation: d55e2a37f61bcfb4eba9c813b3f4621d896c4c00 + EXNotifications: fba3319b1555961b99ddd185021b4c7ff978d5dd EXPermissions: 17d4846ad1880f6891c74ae58ca1acb43e47ed47 EXRandom: d7e0f3dd64810aabd27d59f8ecffee359177e2c3 EXScreenCapture: 5b8447139e56e2b922e93ccdc7c773c103fb44fd diff --git a/package.json b/package.json index ac913041..83d24c0f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "expo-linear-gradient": "~8.4.0", "expo-linking": "~2.0.1", "expo-localization": "~9.1.0", + "expo-notifications": "~0.8.2", "expo-random": "~10.0.0", "expo-screen-capture": "^3.0.0", "expo-secure-store": "~9.3.0", diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 72a0ed0d..d127148a 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -343,6 +343,19 @@ declare namespace Mastodon { 'reading:expand:spoilers'?: boolean } + type PushSubscription = { + id: string + endpoint: string + alerts: { + follow: boolean + favourite: boolean + reblog: boolean + mention: boolean + poll: boolean + } + server_key: string + } + type Relationship = { id: string following: boolean diff --git a/src/@types/react-navigation.d.ts b/src/@types/react-navigation.d.ts index 5bc2ba18..0bfcc2ac 100644 --- a/src/@types/react-navigation.d.ts +++ b/src/@types/react-navigation.d.ts @@ -117,7 +117,7 @@ declare namespace Nav { title: Mastodon.List['title'] } 'Tab-Me-Settings': undefined - 'Tab-Me-Settings-UpdateRemote': undefined + 'Tab-Me-Settings-Notification': undefined 'Tab-Me-Switch': undefined } & TabSharedStackParamList } diff --git a/src/Screens.tsx b/src/Screens.tsx index 554669b3..6e42c7bc 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -1,4 +1,4 @@ -import client from '@api/client' +import apiInstance from '@api/instance' import { toast, toastConfig } from '@components/toast' import { NavigationContainer, @@ -10,10 +10,8 @@ import ScreenCompose from '@screens/Compose' import ScreenImagesViewer from '@screens/ImagesViewer' import ScreenTabs from '@screens/Tabs' import { updatePreviousTab } from '@utils/slices/contextsSlice' -import { - getLocalActiveIndex, - updateLocalAccountPreferences -} from '@utils/slices/instancesSlice' +import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences' +import { getInstanceActive } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import { themes } from '@utils/styles/themes' import * as Analytics from 'expo-firebase-analytics' @@ -36,7 +34,7 @@ export const navigationRef = createRef() const Screens: React.FC = ({ localCorrupt }) => { const { t } = useTranslation('common') const dispatch = useDispatch() - const localActiveIndex = useSelector(getLocalActiveIndex) + const instanceActive = useSelector(getInstanceActive) const { mode, theme } = useTheme() enum barStyle { light = 'dark-content', @@ -89,10 +87,9 @@ const Screens: React.FC = ({ localCorrupt }) => { // On launch check if there is any unread announcements useEffect(() => { - localActiveIndex !== null && - client({ + instanceActive !== -1 && + apiInstance({ method: 'get', - instance: 'local', url: `announcements` }) .then(res => { @@ -107,8 +104,8 @@ const Screens: React.FC = ({ localCorrupt }) => { // Lazily update users's preferences, for e.g. composing default visibility useEffect(() => { - if (localActiveIndex !== null) { - dispatch(updateLocalAccountPreferences()) + if (instanceActive !== -1) { + dispatch(updateAccountPreferences()) } }, []) diff --git a/src/api/general.ts b/src/api/general.ts new file mode 100644 index 00000000..0b97304b --- /dev/null +++ b/src/api/general.ts @@ -0,0 +1,87 @@ +import axios from 'axios' +import chalk from 'chalk' + +const ctx = new chalk.Instance({ level: 3 }) + +export type Params = { + method: 'get' | 'post' | 'put' | 'delete' + domain?: string + url: string + params?: { + [key: string]: string | number | boolean + } + headers?: { [key: string]: string } + body?: FormData | Object +} + +const apiGeneral = async ({ + method, + domain, + url, + params, + headers, + body +}: Params): Promise<{ body: T }> => { + if (!domain) { + return Promise.reject() + } + + console.log( + ctx.bgGreen.bold(' API general ') + + ' ' + + domain + + ' ' + + method + + ctx.green(' -> ') + + `/${url}` + + (params ? ctx.green(' -> ') : ''), + params ? params : '' + ) + + return axios({ + timeout: method === 'post' ? 1000 * 60 : 1000 * 15, + method, + baseURL: `https://${domain}/`, + url, + params, + headers: { + 'Content-Type': 'application/json', + ...headers + }, + ...(body && { data: body }) + }) + .then(response => { + return Promise.resolve({ + body: response.data + }) + }) + .catch(error => { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.error( + ctx.bold(' API general '), + ctx.bold('response'), + error.response.status, + error.response.data.error + ) + return Promise.reject(error.response) + } 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) + return Promise.reject() + } else { + console.error( + ctx.bold(' API general '), + ctx.bold('internal'), + error.message, + url + ) + return Promise.reject() + } + }) +} + +export default apiGeneral diff --git a/src/api/client.ts b/src/api/instance.ts similarity index 69% rename from src/api/client.ts rename to src/api/instance.ts index 66ea911a..811e948f 100644 --- a/src/api/client.ts +++ b/src/api/instance.ts @@ -5,22 +5,8 @@ import li from 'li' const ctx = new chalk.Instance({ level: 3 }) -const client = async ({ - method, - instance, - localIndex, - instanceDomain, - version = 'v1', - url, - params, - headers, - body, - onUploadProgress -}: { +export type Params = { method: 'get' | 'post' | 'put' | 'delete' - instance: 'local' | 'remote' - localIndex?: number - instanceDomain?: string version?: 'v1' | 'v2' url: string params?: { @@ -29,30 +15,37 @@ const client = async ({ headers?: { [key: string]: string } body?: FormData onUploadProgress?: (progressEvent: any) => void -}): Promise<{ body: T; links: { prev?: string; next?: string } }> => { - const { store } = require('@root/store') - const state = (store.getState() as RootState).instances - const theLocalIndex = - localIndex !== undefined ? localIndex : state.local.activeIndex +} - let domain = null - let token = null - if (instance === 'remote') { - domain = instanceDomain || state.remote.url +const apiInstance = async ({ + method, + version = 'v1', + url, + params, + headers, + body, + onUploadProgress +}: Params): Promise<{ body: T; links: { prev?: string; next?: string } }> => { + const { store } = require('@root/store') + const state = store.getState() as RootState + const instanceActive = state.instances.instances.findIndex( + instance => instance.active + ) + + let domain + let token + if (instanceActive !== -1 && state.instances.instances[instanceActive]) { + domain = state.instances.instances[instanceActive].url + token = state.instances.instances[instanceActive].token } 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' - ) - return Promise.reject() - } + console.error( + ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided' + ) + return Promise.reject() } console.log( - ctx.bgGreen.bold(' API ') + + ctx.bgGreen.bold(' API instance ') + ' ' + domain + ' ' + @@ -97,7 +90,7 @@ const client = async ({ // The request was made and the server responded with a status code // that falls out of the range of 2xx console.error( - ctx.bold(' API '), + ctx.bold(' API instance '), ctx.bold('response'), error.response.status, error.response.data.error @@ -107,11 +100,11 @@ const client = async ({ // 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 '), ctx.bold('request'), error) + console.error(ctx.bold(' API instance '), ctx.bold('request'), error) return Promise.reject() } else { console.error( - ctx.bold(' API '), + ctx.bold(' API instance '), ctx.bold('internal'), error.message, url @@ -121,4 +114,4 @@ const client = async ({ }) } -export default client +export default apiInstance diff --git a/src/api/websocket.ts b/src/api/websocket.ts index dd4f4ffe..3df4568a 100644 --- a/src/api/websocket.ts +++ b/src/api/websocket.ts @@ -1,7 +1,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { - getLocalInstance, - updateLocalNotification + getInstance, + updateInstanceNotification } from '@utils/slices/instancesSlice' import { useEffect, useRef } from 'react' import { useQueryClient } from 'react-query' @@ -18,7 +18,7 @@ const useWebsocket = ({ const queryClient = useQueryClient() const dispatch = useDispatch() const localInstance = useSelector( - getLocalInstance, + getInstance, (prev, next) => prev?.urls.streaming_api === next?.urls.streaming_api && prev?.token === next?.token @@ -39,7 +39,7 @@ const useWebsocket = ({ case 'notification': const payload: Mastodon.Notification = JSON.parse(message.payload) dispatch( - updateLocalNotification({ latestTime: payload.created_at }) + updateInstanceNotification({ latestTime: payload.created_at }) ) const queryKey: QueryKeyTimeline = [ 'Timeline', diff --git a/src/components/Instance.tsx b/src/components/Instance.tsx index 58da03e5..3c3655d1 100644 --- a/src/components/Instance.tsx +++ b/src/components/Instance.tsx @@ -2,7 +2,7 @@ import Button from '@components/Button' import Icon from '@components/Icon' import { useAppsQuery } from '@utils/queryHooks/apps' import { useInstanceQuery } from '@utils/queryHooks/instance' -import { getLocalInstances } from '@utils/slices/instancesSlice' +import { getInstances } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import * as WebBrowser from 'expo-web-browser' @@ -14,7 +14,6 @@ import { useSelector } from 'react-redux' import { Placeholder, Fade } from 'rn-placeholder' import analytics from './analytics' import InstanceAuth from './Instance/Auth' -import EULA from './Instance/EULA' import InstanceInfo from './Instance/Info' export interface Props { @@ -29,26 +28,23 @@ const ComponentInstance: React.FC = ({ const { t } = useTranslation('componentInstance') const { theme } = useTheme() - const localInstances = useSelector(getLocalInstances, () => true) - const [instanceDomain, setInstanceDomain] = useState() + const instances = useSelector(getInstances, () => true) + const [domain, setDomain] = useState() const instanceQuery = useInstanceQuery({ - instanceDomain, - options: { enabled: false, retry: false } + domain, + options: { enabled: !!domain, retry: false } }) const appsQuery = useAppsQuery({ - instanceDomain, + domain, options: { enabled: false, retry: false } }) const onChangeText = useCallback( debounce( text => { - setInstanceDomain(text.replace(/^http(s)?\:\/\//i, '')) + setDomain(text.replace(/^http(s)?\:\/\//i, '')) appsQuery.remove() - if (text) { - instanceQuery.refetch() - } }, 1000, { trailing: true } @@ -57,40 +53,35 @@ const ComponentInstance: React.FC = ({ ) const processUpdate = useCallback(() => { - if (instanceDomain) { - analytics('instance_local_login') + if (domain) { + analytics('instance_login') if ( - localInstances && - localInstances.filter(instance => instance.url === instanceDomain) - .length + instances && + instances.filter(instance => instance.url === domain).length ) { - Alert.alert( - t('update.local.alert.title'), - t('update.local.alert.message'), - [ - { - text: t('update.local.alert.buttons.cancel'), - style: 'cancel' - }, - { - text: t('update.local.alert.buttons.continue'), - onPress: () => { - appsQuery.refetch() - } + Alert.alert(t('update.alert.title'), t('update.alert.message'), [ + { + text: t('update.alert.buttons.cancel'), + style: 'cancel' + }, + { + text: t('update.alert.buttons.continue'), + onPress: () => { + appsQuery.refetch() } - ] - ) + } + ]) } else { appsQuery.refetch() } } - }, [instanceDomain]) + }, [domain]) const onSubmitEditing = useCallback( ({ nativeEvent: { text } }) => { - analytics('instance_textinput_submit', { match: text === instanceDomain }) + analytics('instance_textinput_submit', { match: text === domain }) if ( - text === instanceDomain && + text === domain && instanceQuery.isSuccess && instanceQuery.data && instanceQuery.data.uri @@ -98,12 +89,12 @@ const ComponentInstance: React.FC = ({ processUpdate() } }, - [instanceDomain, instanceQuery.isSuccess, instanceQuery.data] + [domain, instanceQuery.isSuccess, instanceQuery.data] ) const requestAuth = useMemo(() => { if ( - instanceDomain && + domain && instanceQuery.data?.uri && appsQuery.data?.client_id && appsQuery.data.client_secret @@ -111,7 +102,7 @@ const ComponentInstance: React.FC = ({ return ( = ({ /> ) } - }, [instanceDomain, instanceQuery.data, appsQuery.data]) - - const [agreed, setAgreed] = useState(false) + }, [domain, instanceQuery.data, appsQuery.data]) return ( <> @@ -160,15 +149,13 @@ const ComponentInstance: React.FC = ({ />