diff --git a/app/soapbox/actions/apps.js b/app/soapbox/actions/apps.ts similarity index 80% rename from app/soapbox/actions/apps.js rename to app/soapbox/actions/apps.ts index 7eb00c98c..d2ef2ff0d 100644 --- a/app/soapbox/actions/apps.js +++ b/app/soapbox/actions/apps.ts @@ -8,6 +8,8 @@ import { baseClient } from '../api'; +import type { AnyAction } from 'redux'; + export const APP_CREATE_REQUEST = 'APP_CREATE_REQUEST'; export const APP_CREATE_SUCCESS = 'APP_CREATE_SUCCESS'; export const APP_CREATE_FAIL = 'APP_CREATE_FAIL'; @@ -16,12 +18,12 @@ export const APP_VERIFY_CREDENTIALS_REQUEST = 'APP_VERIFY_CREDENTIALS_REQUEST'; export const APP_VERIFY_CREDENTIALS_SUCCESS = 'APP_VERIFY_CREDENTIALS_SUCCESS'; export const APP_VERIFY_CREDENTIALS_FAIL = 'APP_VERIFY_CREDENTIALS_FAIL'; -export function createApp(params, baseURL) { - return (dispatch, getState) => { +export function createApp(params?: Record, baseURL?: string) { + return (dispatch: React.Dispatch) => { dispatch({ type: APP_CREATE_REQUEST, params }); return baseClient(null, baseURL).post('/api/v1/apps', params).then(({ data: app }) => { dispatch({ type: APP_CREATE_SUCCESS, params, app }); - return app; + return app as Record; }).catch(error => { dispatch({ type: APP_CREATE_FAIL, params, error }); throw error; @@ -29,8 +31,8 @@ export function createApp(params, baseURL) { }; } -export function verifyAppCredentials(token) { - return (dispatch, getState) => { +export function verifyAppCredentials(token: string) { + return (dispatch: React.Dispatch) => { dispatch({ type: APP_VERIFY_CREDENTIALS_REQUEST, token }); return baseClient(token).get('/api/v1/apps/verify_credentials').then(({ data: app }) => { dispatch({ type: APP_VERIFY_CREDENTIALS_SUCCESS, token, app }); diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.ts similarity index 55% rename from app/soapbox/actions/auth.js rename to app/soapbox/actions/auth.ts index a42bccdef..49de0e475 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.ts @@ -26,6 +26,10 @@ import api, { baseClient } from '../api'; import { importFetchedAccount } from './importer'; +import type { AxiosError } from 'axios'; +import type { Map as ImmutableMap } from 'immutable'; +import type { AppDispatch, RootState } from 'soapbox/store'; + export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; export const AUTH_APP_CREATED = 'AUTH_APP_CREATED'; @@ -48,35 +52,32 @@ export const messages = defineMessages({ invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' }, }); -const noOp = () => new Promise(f => f()); +const noOp = () => new Promise(f => f(undefined)); -const getScopes = state => { - const instance = state.get('instance'); +const getScopes = (state: RootState) => { + const instance = state.instance; const { scopes } = getFeatures(instance); return scopes; }; -function createAppAndToken() { - return (dispatch, getState) => { - return dispatch(getAuthApp()).then(() => { - return dispatch(createAppToken()); - }); - }; -} +const createAppAndToken = () => + (dispatch: AppDispatch) => + dispatch(getAuthApp()).then(() => + dispatch(createAppToken()), + ); /** Create an auth app, or use it from build config */ -function getAuthApp() { - return (dispatch, getState) => { +const getAuthApp = () => + (dispatch: AppDispatch) => { if (customApp?.client_secret) { return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp })); } else { return dispatch(createAuthApp()); } }; -} -function createAuthApp() { - return (dispatch, getState) => { +const createAuthApp = () => + (dispatch: AppDispatch, getState: () => any) => { const params = { client_name: sourceCode.displayName, redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', @@ -84,15 +85,14 @@ function createAuthApp() { website: sourceCode.homepage, }; - return dispatch(createApp(params)).then(app => { - return dispatch({ type: AUTH_APP_CREATED, app }); - }); + return dispatch(createApp(params)).then((app: Record) => + dispatch({ type: AUTH_APP_CREATED, app }), + ); }; -} -function createAppToken() { - return (dispatch, getState) => { - const app = getState().getIn(['auth', 'app']); +const createAppToken = () => + (dispatch: AppDispatch, getState: () => any) => { + const app = getState().auth.get('app'); const params = { client_id: app.get('client_id'), @@ -102,15 +102,14 @@ function createAppToken() { scope: getScopes(getState()), }; - return dispatch(obtainOAuthToken(params)).then(token => { - return dispatch({ type: AUTH_APP_AUTHORIZED, app, token }); - }); + return dispatch(obtainOAuthToken(params)).then((token: Record) => + dispatch({ type: AUTH_APP_AUTHORIZED, app, token }), + ); }; -} -function createUserToken(username, password) { - return (dispatch, getState) => { - const app = getState().getIn(['auth', 'app']); +const createUserToken = (username: string, password: string) => + (dispatch: AppDispatch, getState: () => any) => { + const app = getState().auth.get('app'); const params = { client_id: app.get('client_id'), @@ -123,14 +122,13 @@ function createUserToken(username, password) { }; return dispatch(obtainOAuthToken(params)) - .then(token => dispatch(authLoggedIn(token))); + .then((token: Record) => dispatch(authLoggedIn(token))); }; -} -export function refreshUserToken() { - return (dispatch, getState) => { - const refreshToken = getState().getIn(['auth', 'user', 'refresh_token']); - const app = getState().getIn(['auth', 'app']); +export const refreshUserToken = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const refreshToken = getState().auth.getIn(['user', 'refresh_token']); + const app = getState().auth.get('app'); if (!refreshToken) return dispatch(noOp); @@ -144,13 +142,12 @@ export function refreshUserToken() { }; return dispatch(obtainOAuthToken(params)) - .then(token => dispatch(authLoggedIn(token))); + .then((token: Record) => dispatch(authLoggedIn(token))); }; -} -export function otpVerify(code, mfa_token) { - return (dispatch, getState) => { - const app = getState().getIn(['auth', 'app']); +export const otpVerify = (code: string, mfa_token: string) => + (dispatch: AppDispatch, getState: () => any) => { + const app = getState().auth.get('app'); return api(getState, 'app').post('/oauth/mfa/challenge', { client_id: app.get('client_id'), client_secret: app.get('client_secret'), @@ -161,18 +158,17 @@ export function otpVerify(code, mfa_token) { scope: getScopes(getState()), }).then(({ data: token }) => dispatch(authLoggedIn(token))); }; -} -export function verifyCredentials(token, accountUrl) { +export const verifyCredentials = (token: string, accountUrl?: string) => { const baseURL = parseBaseURL(accountUrl); - return (dispatch, getState) => { + return (dispatch: AppDispatch, getState: () => any) => { dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token }); return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => { dispatch(importFetchedAccount(account)); dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); - if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account)); + if (account.id === getState().me) dispatch(fetchMeSuccess(account)); return account; }).catch(error => { if (error?.response?.status === 403 && error?.response?.data?.id) { @@ -180,75 +176,64 @@ export function verifyCredentials(token, accountUrl) { const account = error.response.data; dispatch(importFetchedAccount(account)); dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); - if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account)); + if (account.id === getState().me) dispatch(fetchMeSuccess(account)); return account; } else { - if (getState().get('me') === null) dispatch(fetchMeFail(error)); + if (getState().me === null) dispatch(fetchMeFail(error)); dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error, skipAlert: true }); return error; } }); }; -} +}; -export function rememberAuthAccount(accountUrl) { - return (dispatch, getState) => { +export const rememberAuthAccount = (accountUrl: string) => + (dispatch: AppDispatch, getState: () => any) => { dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl }); return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => { dispatch(importFetchedAccount(account)); dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl }); - if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account)); + if (account.id === getState().me) dispatch(fetchMeSuccess(account)); return account; }).catch(error => { dispatch({ type: AUTH_ACCOUNT_REMEMBER_FAIL, error, accountUrl, skipAlert: true }); }); }; -} - -export function loadCredentials(token, accountUrl) { - return (dispatch, getState) => { - return dispatch(rememberAuthAccount(accountUrl)) - .then(account => account) - .then(() => { - dispatch(verifyCredentials(token, accountUrl)); - }) - .catch(error => dispatch(verifyCredentials(token, accountUrl))); - }; -} -export function logIn(intl, username, password) { - return (dispatch, getState) => { - return dispatch(getAuthApp()).then(() => { - return dispatch(createUserToken(username, password)); - }).catch(error => { - if (error.response.data.error === 'mfa_required') { - // If MFA is required, throw the error and handle it in the component. - throw error; - } else if (error.response.data.error === 'invalid_grant') { - // Mastodon returns this user-unfriendly error as a catch-all - // for everything from "bad request" to "wrong password". - // Assume our code is correct and it's a wrong password. - dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials))); - } else if (error.response.data.error) { - // If the backend returns an error, display it. - dispatch(snackbar.error(error.response.data.error)); - } else { - // Return "wrong password" message. - dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials))); - } +export const loadCredentials = (token: string, accountUrl: string) => + (dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl)) + .then(() => { + dispatch(verifyCredentials(token, accountUrl)); + }) + .catch(() => dispatch(verifyCredentials(token, accountUrl))); + +export const logIn = (username: string, password: string) => + (dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => { + return dispatch(createUserToken(username, password)); + }).catch((error: AxiosError) => { + if ((error.response?.data as any).error === 'mfa_required') { + // If MFA is required, throw the error and handle it in the component. throw error; - }); - }; -} + } else if ((error.response?.data as any).error === 'invalid_grant') { + // Mastodon returns this user-unfriendly error as a catch-all + // for everything from "bad request" to "wrong password". + // Assume our code is correct and it's a wrong password. + dispatch(snackbar.error(messages.invalidCredentials)); + } else if ((error.response?.data as any).error) { + // If the backend returns an error, display it. + dispatch(snackbar.error((error.response?.data as any).error)); + } else { + // Return "wrong password" message. + dispatch(snackbar.error(messages.invalidCredentials)); + } + throw error; + }); -export function deleteSession() { - return (dispatch, getState) => { - return api(getState).delete('/api/sign_out'); - }; -} +export const deleteSession = () => + (dispatch: AppDispatch, getState: () => any) => api(getState).delete('/api/sign_out'); -export function logOut(intl) { - return (dispatch, getState) => { +export const logOut = () => + (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const account = getLoggedInAccount(state); const standalone = isStandalone(state); @@ -256,9 +241,9 @@ export function logOut(intl) { if (!account) return dispatch(noOp); const params = { - client_id: state.getIn(['auth', 'app', 'client_id']), - client_secret: state.getIn(['auth', 'app', 'client_secret']), - token: state.getIn(['auth', 'users', account.url, 'access_token']), + client_id: state.auth.getIn(['app', 'client_id']), + client_secret: state.auth.getIn(['app', 'client_secret']), + token: state.auth.getIn(['users', account.url, 'access_token']), }; return Promise.all([ @@ -266,52 +251,47 @@ export function logOut(intl) { dispatch(deleteSession()), ]).finally(() => { dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); - dispatch(snackbar.success(intl.formatMessage(messages.loggedOut))); + return dispatch(snackbar.success(messages.loggedOut)); }); }; -} -export function switchAccount(accountId, background = false) { - return (dispatch, getState) => { - const account = getState().getIn(['accounts', accountId]); - dispatch({ type: SWITCH_ACCOUNT, account, background }); +export const switchAccount = (accountId: string, background = false) => + (dispatch: AppDispatch, getState: () => any) => { + const account = getState().accounts.get(accountId); + return dispatch({ type: SWITCH_ACCOUNT, account, background }); }; -} -export function fetchOwnAccounts() { - return (dispatch, getState) => { +export const fetchOwnAccounts = () => + (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - state.getIn(['auth', 'users']).forEach(user => { - const account = state.getIn(['accounts', user.get('id')]); + return state.auth.get('users').forEach((user: ImmutableMap) => { + const account = state.accounts.get(user.get('id')); if (!account) { - dispatch(verifyCredentials(user.get('access_token'), user.get('url'))); + dispatch(verifyCredentials(user.get('access_token')!, user.get('url'))); } }); }; -} -export function register(params) { - return (dispatch, getState) => { + +export const register = (params: Record) => + (dispatch: AppDispatch) => { params.fullname = params.username; return dispatch(createAppAndToken()) .then(() => dispatch(createAccount(params))) - .then(({ token }) => { + .then(({ token }: { token: Record }) => { dispatch(startOnboarding()); return dispatch(authLoggedIn(token)); }); }; -} -export function fetchCaptcha() { - return (dispatch, getState) => { +export const fetchCaptcha = () => + (_dispatch: AppDispatch, getState: () => any) => { return api(getState).get('/api/pleroma/captcha'); }; -} -export function authLoggedIn(token) { - return (dispatch, getState) => { +export const authLoggedIn = (token: Record) => + (dispatch: AppDispatch) => { dispatch({ type: AUTH_LOGGED_IN, token }); return token; }; -} diff --git a/app/soapbox/actions/backups.js b/app/soapbox/actions/backups.ts similarity index 63% rename from app/soapbox/actions/backups.js rename to app/soapbox/actions/backups.ts index 844c55ce5..2057782ba 100644 --- a/app/soapbox/actions/backups.js +++ b/app/soapbox/actions/backups.ts @@ -1,5 +1,7 @@ import api from '../api'; +import type { AppDispatch } from 'soapbox/store'; + export const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST'; export const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS'; export const BACKUPS_FETCH_FAIL = 'BACKUPS_FETCH_FAIL'; @@ -8,24 +10,22 @@ export const BACKUPS_CREATE_REQUEST = 'BACKUPS_CREATE_REQUEST'; export const BACKUPS_CREATE_SUCCESS = 'BACKUPS_CREATE_SUCCESS'; export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL'; -export function fetchBackups() { - return (dispatch, getState) => { +export const fetchBackups = () => + (dispatch: AppDispatch, getState: () => any) => { dispatch({ type: BACKUPS_FETCH_REQUEST }); - return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) => { - dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }); - }).catch(error => { + return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) => + dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }), + ).catch(error => { dispatch({ type: BACKUPS_FETCH_FAIL, error }); }); }; -} -export function createBackup() { - return (dispatch, getState) => { +export const createBackup = () => + (dispatch: AppDispatch, getState: () => any) => { dispatch({ type: BACKUPS_CREATE_REQUEST }); - return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) => { - dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }); - }).catch(error => { + return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) => + dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }), + ).catch(error => { dispatch({ type: BACKUPS_CREATE_FAIL, error }); }); }; -} diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.ts similarity index 63% rename from app/soapbox/actions/external_auth.js rename to app/soapbox/actions/external_auth.ts index 4f389667f..064e100c9 100644 --- a/app/soapbox/actions/external_auth.js +++ b/app/soapbox/actions/external_auth.ts @@ -18,7 +18,10 @@ import { getQuirks } from 'soapbox/utils/quirks'; import { baseClient } from '../api'; -const fetchExternalInstance = baseURL => { +import type { AppDispatch } from 'soapbox/store'; +import type { Instance } from 'soapbox/types/entities'; + +const fetchExternalInstance = (baseURL?: string) => { return baseClient(null, baseURL) .get('/api/v1/instance') .then(({ data: instance }) => normalizeInstance(instance)) @@ -33,8 +36,8 @@ const fetchExternalInstance = baseURL => { }); }; -function createExternalApp(instance, baseURL) { - return (dispatch, getState) => { +const createExternalApp = (instance: Instance, baseURL?: string) => + (dispatch: AppDispatch) => { // Mitra: skip creating the auth app if (getQuirks(instance).noApps) return new Promise(f => f({})); @@ -49,14 +52,13 @@ function createExternalApp(instance, baseURL) { return dispatch(createApp(params, baseURL)); }; -} -function externalAuthorize(instance, baseURL) { - return (dispatch, getState) => { +const externalAuthorize = (instance: Instance, baseURL: string) => + (dispatch: AppDispatch) => { const { scopes } = getFeatures(instance); - return dispatch(createExternalApp(instance, baseURL)).then(app => { - const { client_id, redirect_uri } = app; + return dispatch(createExternalApp(instance, baseURL)).then((app) => { + const { client_id, redirect_uri } = app as Record; const query = new URLSearchParams({ client_id, @@ -72,58 +74,56 @@ function externalAuthorize(instance, baseURL) { window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`; }); }; -} -export function externalEthereumLogin(instance, baseURL) { - return (dispatch, getState) => { - const loginMessage = instance.get('login_message'); +const externalEthereumLogin = (instance: Instance, baseURL?: string) => + (dispatch: AppDispatch) => { + const loginMessage = instance.login_message; return getWalletAndSign(loginMessage).then(({ wallet, signature }) => { - return dispatch(createExternalApp(instance, baseURL)).then(app => { + return dispatch(createExternalApp(instance, baseURL)).then((app) => { + const { client_id, client_secret } = app as Record; const params = { grant_type: 'ethereum', wallet_address: wallet.toLowerCase(), - client_id: app.client_id, - client_secret: app.client_secret, - password: signature, + client_id: client_id, + client_secret: client_secret, + password: signature as string, redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', scope: getFeatures(instance).scopes, }; return dispatch(obtainOAuthToken(params, baseURL)) - .then(token => dispatch(authLoggedIn(token))) - .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))) - .then(account => dispatch(switchAccount(account.id))) + .then((token: Record) => dispatch(authLoggedIn(token))) + .then(({ access_token }: any) => dispatch(verifyCredentials(access_token, baseURL))) + .then((account: { id: string }) => dispatch(switchAccount(account.id))) .then(() => window.location.href = '/'); }); }); }; -} -export function externalLogin(host) { - return (dispatch, getState) => { +export const externalLogin = (host: string) => + (dispatch: AppDispatch) => { const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`); - return fetchExternalInstance(baseURL).then(instance => { + return fetchExternalInstance(baseURL).then((instance) => { const features = getFeatures(instance); const quirks = getQuirks(instance); if (features.ethereumLogin && quirks.noOAuthForm) { - return dispatch(externalEthereumLogin(instance, baseURL)); + dispatch(externalEthereumLogin(instance, baseURL)); } else { - return dispatch(externalAuthorize(instance, baseURL)); + dispatch(externalAuthorize(instance, baseURL)); } }); }; -} -export function loginWithCode(code) { - return (dispatch, getState) => { - const { client_id, client_secret, redirect_uri } = JSON.parse(localStorage.getItem('soapbox:external:app')); - const baseURL = localStorage.getItem('soapbox:external:baseurl'); - const scope = localStorage.getItem('soapbox:external:scopes'); +export const loginWithCode = (code: string) => + (dispatch: AppDispatch) => { + const { client_id, client_secret, redirect_uri } = JSON.parse(localStorage.getItem('soapbox:external:app')!); + const baseURL = localStorage.getItem('soapbox:external:baseurl')!; + const scope = localStorage.getItem('soapbox:external:scopes')!; - const params = { + const params: Record = { client_id, client_secret, redirect_uri, @@ -133,9 +133,8 @@ export function loginWithCode(code) { }; return dispatch(obtainOAuthToken(params, baseURL)) - .then(token => dispatch(authLoggedIn(token))) - .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))) - .then(account => dispatch(switchAccount(account.id))) + .then((token: Record) => dispatch(authLoggedIn(token))) + .then(({ access_token }: any) => dispatch(verifyCredentials(access_token as string, baseURL))) + .then((account: { id: string }) => dispatch(switchAccount(account.id))) .then(() => window.location.href = '/'); }; -} diff --git a/app/soapbox/actions/oauth.js b/app/soapbox/actions/oauth.ts similarity index 83% rename from app/soapbox/actions/oauth.js rename to app/soapbox/actions/oauth.ts index 9662972a8..aefef7b6f 100644 --- a/app/soapbox/actions/oauth.js +++ b/app/soapbox/actions/oauth.ts @@ -8,6 +8,8 @@ import { baseClient } from '../api'; +import type { AppDispatch } from 'soapbox/store'; + export const OAUTH_TOKEN_CREATE_REQUEST = 'OAUTH_TOKEN_CREATE_REQUEST'; export const OAUTH_TOKEN_CREATE_SUCCESS = 'OAUTH_TOKEN_CREATE_SUCCESS'; export const OAUTH_TOKEN_CREATE_FAIL = 'OAUTH_TOKEN_CREATE_FAIL'; @@ -16,8 +18,8 @@ export const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST'; export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS'; export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL'; -export function obtainOAuthToken(params, baseURL) { - return (dispatch, getState) => { +export const obtainOAuthToken = (params: Record, baseURL?: string) => + (dispatch: AppDispatch) => { dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params }); return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => { dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token }); @@ -27,10 +29,9 @@ export function obtainOAuthToken(params, baseURL) { throw error; }); }; -} -export function revokeOAuthToken(params) { - return (dispatch, getState) => { +export const revokeOAuthToken = (params: Record) => + (dispatch: AppDispatch) => { dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params }); return baseClient().post('/oauth/revoke', params).then(({ data }) => { dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data }); @@ -40,4 +41,3 @@ export function revokeOAuthToken(params) { throw error; }); }; -} diff --git a/app/soapbox/api.ts b/app/soapbox/api.ts index 05d08b768..33cd803ad 100644 --- a/app/soapbox/api.ts +++ b/app/soapbox/api.ts @@ -53,7 +53,7 @@ const getAuthBaseURL = createSelector([ * @param {string} baseURL * @returns {object} Axios instance */ -export const baseClient = (accessToken: string, baseURL: string = ''): AxiosInstance => { +export const baseClient = (accessToken?: string | null, baseURL: string = ''): AxiosInstance => { return axios.create({ // When BACKEND_URL is set, always use it. baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL, diff --git a/app/soapbox/components/birthday_input.tsx b/app/soapbox/components/birthday_input.tsx index 2b4e4833f..a1dc36c88 100644 --- a/app/soapbox/components/birthday_input.tsx +++ b/app/soapbox/components/birthday_input.tsx @@ -25,7 +25,7 @@ const BirthdayInput: React.FC = ({ value, onChange, required }) const features = useFeatures(); const supportsBirthdays = features.birthdays; - const minAge = useAppSelector((state) => state.instance.getIn(['pleroma', 'metadata', 'birthday_min_age'])) as number; + const minAge = useAppSelector((state) => state.instance.pleroma.getIn(['metadata', 'birthday_min_age'])) as number; const maxDate = useMemo(() => { if (!supportsBirthdays) return null; diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index e4d574a74..5b8c79230 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -18,7 +18,7 @@ const SidebarNavigation = () => { const settings = useAppSelector((state) => getSettings(state)); const account = useOwnAccount(); const notificationCount = useAppSelector((state) => state.notifications.get('unread')); - const chatsCount = useAppSelector((state) => state.chats.get('items').reduce((acc: any, curr: any) => acc + Math.min(curr.get('unread', 0), 1), 0)); + const chatsCount = useAppSelector((state) => state.chats.items.reduce((acc, curr) => acc + Math.min(curr.unread || 0, 1), 0)); const followRequestsCount = useAppSelector((state) => state.user_lists.getIn(['follow_requests', 'items'], ImmutableOrderedSet()).count()); const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); diff --git a/app/soapbox/components/sidebar_menu.tsx b/app/soapbox/components/sidebar_menu.tsx index c53ec0b40..cb3962010 100644 --- a/app/soapbox/components/sidebar_menu.tsx +++ b/app/soapbox/components/sidebar_menu.tsx @@ -109,7 +109,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const onClickLogOut: React.MouseEventHandler = (e) => { e.preventDefault(); - dispatch(logOut(intl)); + dispatch(logOut()); }; const handleSwitcherClick: React.MouseEventHandler = (e) => { diff --git a/app/soapbox/components/thumb_navigation.tsx b/app/soapbox/components/thumb_navigation.tsx index 963b5d234..adb89869f 100644 --- a/app/soapbox/components/thumb_navigation.tsx +++ b/app/soapbox/components/thumb_navigation.tsx @@ -8,7 +8,7 @@ import { getFeatures } from 'soapbox/utils/features'; const ThumbNavigation: React.FC = (): JSX.Element => { const account = useOwnAccount(); const notificationCount = useAppSelector((state) => state.notifications.unread); - const chatsCount = useAppSelector((state) => state.chats.get('items').reduce((acc: number, curr: any) => acc + Math.min(curr.get('unread', 0), 1), 0)); + const chatsCount = useAppSelector((state) => state.chats.items.reduce((acc, curr) => acc + Math.min(curr.unread || 0, 1), 0)); const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); const features = getFeatures(useAppSelector((state) => state.instance)); diff --git a/app/soapbox/features/auth_login/components/captcha.tsx b/app/soapbox/features/auth_login/components/captcha.tsx index 465ddc4fd..d967c76a2 100644 --- a/app/soapbox/features/auth_login/components/captcha.tsx +++ b/app/soapbox/features/auth_login/components/captcha.tsx @@ -6,6 +6,8 @@ import { fetchCaptcha } from 'soapbox/actions/auth'; import { Stack, Text, Input } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; +import type { AxiosResponse } from 'axios'; + const noOp = () => {}; const messages = defineMessages({ @@ -39,7 +41,7 @@ const CaptchaField: React.FC = ({ const [refresh, setRefresh] = useState(undefined); const getCaptcha = () => { - dispatch(fetchCaptcha()).then(response => { + dispatch(fetchCaptcha()).then((response: AxiosResponse) => { const captcha = ImmutableMap(response.data); setCaptcha(captcha); onFetch(captcha); diff --git a/app/soapbox/features/auth_login/components/login_page.tsx b/app/soapbox/features/auth_login/components/login_page.tsx index 3f7834b7f..dd37e5f63 100644 --- a/app/soapbox/features/auth_login/components/login_page.tsx +++ b/app/soapbox/features/auth_login/components/login_page.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import { useIntl } from 'react-intl'; import { Redirect } from 'react-router-dom'; import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; @@ -15,7 +14,6 @@ import OtpAuthForm from './otp_auth_form'; import type { AxiosError } from 'axios'; const LoginPage = () => { - const intl = useIntl(); const dispatch = useAppDispatch(); const me = useAppSelector((state) => state.me); @@ -36,8 +34,8 @@ const LoginPage = () => { const handleSubmit: React.FormEventHandler = (event) => { const { username, password } = getFormData(event.target as HTMLFormElement); - dispatch(logIn(intl, username, password)).then(({ access_token }: { access_token: string }) => { - return dispatch(verifyCredentials(access_token)) + dispatch(logIn(username, password)).then(({ access_token }) => { + return dispatch(verifyCredentials(access_token as string)) // Refetch the instance for authenticated fetch .then(() => dispatch(fetchInstance() as any)); }).then((account: { id: string }) => { diff --git a/app/soapbox/features/auth_login/components/logout.tsx b/app/soapbox/features/auth_login/components/logout.tsx index 5c1bee915..6f70e0bdd 100644 --- a/app/soapbox/features/auth_login/components/logout.tsx +++ b/app/soapbox/features/auth_login/components/logout.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Redirect } from 'react-router-dom'; @@ -8,12 +7,11 @@ import { Spinner } from 'soapbox/components/ui'; /** Component that logs the user out when rendered */ const Logout: React.FC = () => { - const intl = useIntl(); const dispatch = useDispatch(); const [done, setDone] = useState(false); useEffect(() => { - dispatch(logOut(intl) as any) + dispatch(logOut() as any) .then(() => setDone(true)) .catch(console.warn); }, []); diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.tsx b/app/soapbox/features/auth_login/components/otp_auth_form.tsx index eaeade08b..b4ef3d9d5 100644 --- a/app/soapbox/features/auth_login/components/otp_auth_form.tsx +++ b/app/soapbox/features/auth_login/components/otp_auth_form.tsx @@ -32,8 +32,8 @@ const OtpAuthForm: React.FC = ({ mfa_token }) => { const { code } = getFormData(event.target); dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => { setCodeError(false); - return dispatch(verifyCredentials(access_token)); - }).then(account => { + return dispatch(verifyCredentials(access_token as string)); + }).then((account: Record) => { setShouldRedirect(true); return dispatch(switchAccount(account.id)); }).catch(() => { diff --git a/app/soapbox/features/backups/index.tsx b/app/soapbox/features/backups/index.tsx index d1baf4991..c78ceb298 100644 --- a/app/soapbox/features/backups/index.tsx +++ b/app/soapbox/features/backups/index.tsx @@ -2,10 +2,7 @@ import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { - fetchBackups, - createBackup, -} from 'soapbox/actions/backups'; +import { fetchBackups, createBackup } from 'soapbox/actions/backups'; import ScrollableList from 'soapbox/components/scrollable_list'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; diff --git a/app/soapbox/features/chats/components/chat.tsx b/app/soapbox/features/chats/components/chat.tsx index 8e10a2f91..63430dea4 100644 --- a/app/soapbox/features/chats/components/chat.tsx +++ b/app/soapbox/features/chats/components/chat.tsx @@ -20,7 +20,7 @@ interface IChat { const Chat: React.FC = ({ chatId, onClick }) => { const chat = useAppSelector((state) => { - const chat = state.chats.getIn(['items', chatId]); + const chat = state.chats.items.get(chatId); return chat ? getChat(state, (chat as any).toJS()) : undefined; }) as ChatEntity; diff --git a/app/soapbox/features/chats/components/chat_list.tsx b/app/soapbox/features/chats/components/chat_list.tsx index 8347c0e89..5d9c422a8 100644 --- a/app/soapbox/features/chats/components/chat_list.tsx +++ b/app/soapbox/features/chats/components/chat_list.tsx @@ -49,9 +49,9 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) const dispatch = useDispatch(); const intl = useIntl(); - const chatIds = useAppSelector(state => sortedChatIdsSelector(state.chats.get('items'))); - const hasMore = useAppSelector(state => !!state.chats.get('next')); - const isLoading = useAppSelector(state => state.chats.get('isLoading')); + const chatIds = useAppSelector(state => sortedChatIdsSelector(state.chats.items)); + const hasMore = useAppSelector(state => !!state.chats.next); + const isLoading = useAppSelector(state => state.chats.isLoading); const handleLoadMore = useCallback(() => { if (hasMore && !isLoading) { diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js deleted file mode 100644 index 74647c98c..000000000 --- a/app/soapbox/features/chats/components/chat_panes.js +++ /dev/null @@ -1,127 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; -import { createSelector } from 'reselect'; - -import { openChat, launchChat, toggleMainWindow } from 'soapbox/actions/chats'; -import { getSettings } from 'soapbox/actions/settings'; -import AccountSearch from 'soapbox/components/account_search'; -import { Counter } from 'soapbox/components/ui'; -import AudioToggle from 'soapbox/features/chats/components/audio_toggle'; - -import ChatList from './chat_list'; -import ChatWindow from './chat_window'; - -const messages = defineMessages({ - searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' }, -}); - -const getChatsUnreadCount = state => { - const chats = state.getIn(['chats', 'items']); - return chats.reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0); -}; - -// Filter out invalid chats -const normalizePanes = (chats, panes = ImmutableList()) => ( - panes.filter(pane => chats.get(pane.get('chat_id'))) -); - -const makeNormalizeChatPanes = () => createSelector([ - state => state.getIn(['chats', 'items']), - state => getSettings(state).getIn(['chats', 'panes']), -], normalizePanes); - -const makeMapStateToProps = () => { - const mapStateToProps = state => { - const normalizeChatPanes = makeNormalizeChatPanes(); - - return { - panes: normalizeChatPanes(state), - mainWindowState: getSettings(state).getIn(['chats', 'mainWindow']), - unreadCount: getChatsUnreadCount(state), - }; - }; - - return mapStateToProps; -}; - -export default @connect(makeMapStateToProps) -@injectIntl -@withRouter -class ChatPanes extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - mainWindowState: PropTypes.string, - panes: ImmutablePropTypes.list, - history: PropTypes.object, - } - - handleClickChat = (chat) => { - this.props.dispatch(openChat(chat.get('id'))); - } - - handleSuggestion = accountId => { - this.props.dispatch(launchChat(accountId, this.props.history)); - } - - handleMainWindowToggle = () => { - this.props.dispatch(toggleMainWindow()); - } - - render() { - const { intl, panes, mainWindowState, unreadCount } = this.props; - const open = mainWindowState === 'open'; - - const mainWindowPane = ( -
-
- {unreadCount > 0 && ( -
- -
- )} - - -
-
- {open && ( - <> - - - - )} -
-
- ); - - return ( -
- {mainWindowPane} - {panes.map((pane, i) => ( - - ))} -
- ); - } - -} diff --git a/app/soapbox/features/chats/components/chat_panes.tsx b/app/soapbox/features/chats/components/chat_panes.tsx new file mode 100644 index 000000000..fa701a8a2 --- /dev/null +++ b/app/soapbox/features/chats/components/chat_panes.tsx @@ -0,0 +1,108 @@ +import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; +import React from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; +import { createSelector } from 'reselect'; + +import { openChat, launchChat, toggleMainWindow } from 'soapbox/actions/chats'; +import { getSettings } from 'soapbox/actions/settings'; +import AccountSearch from 'soapbox/components/account_search'; +import { Counter } from 'soapbox/components/ui'; +import AudioToggle from 'soapbox/features/chats/components/audio_toggle'; +import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; +import { RootState } from 'soapbox/store'; +import { Chat } from 'soapbox/types/entities'; + +import ChatList from './chat_list'; +import ChatWindow from './chat_window'; + +const messages = defineMessages({ + searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' }, +}); + +const getChatsUnreadCount = (state: RootState) => { + const chats = state.chats.items; + return chats.reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0); +}; + +// Filter out invalid chats +const normalizePanes = (chats: Immutable.Map, panes = ImmutableList>()) => ( + panes.filter(pane => chats.get(pane.get('chat_id'))) +); + +const makeNormalizeChatPanes = () => createSelector([ + (state: RootState) => state.chats.items, + (state: RootState) => getSettings(state).getIn(['chats', 'panes']) as any, +], normalizePanes); + +const normalizeChatPanes = makeNormalizeChatPanes(); + +const ChatPanes = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const history = useHistory(); + + const panes = useAppSelector((state) => normalizeChatPanes(state)); + const mainWindowState = useSettings().getIn(['chats', 'mainWindow']); + const unreadCount = useAppSelector((state) => getChatsUnreadCount(state)); + + const handleClickChat = ((chat: Chat) => { + dispatch(openChat(chat.id)); + }); + + const handleSuggestion = (accountId: string) => { + dispatch(launchChat(accountId, history)); + }; + + const handleMainWindowToggle = () => { + dispatch(toggleMainWindow()); + }; + + const open = mainWindowState === 'open'; + + const mainWindowPane = ( +
+
+ {unreadCount > 0 && ( +
+ +
+ )} + + +
+
+ {open && ( + <> + + + + )} +
+
+ ); + + return ( +
+ {mainWindowPane} + {panes.map((pane, i) => ( + + ))} +
+ ); +}; + +export default ChatPanes; diff --git a/app/soapbox/features/crypto_donate/utils/manifest_map.js b/app/soapbox/features/crypto_donate/utils/manifest_map.js deleted file mode 100644 index bab27695f..000000000 --- a/app/soapbox/features/crypto_donate/utils/manifest_map.js +++ /dev/null @@ -1,12 +0,0 @@ -// @preval -// Converts cryptocurrency-icon's manifest file from a list to a map. -// See: https://github.com/spothq/cryptocurrency-icons/blob/master/manifest.json - -const manifest = require('cryptocurrency-icons/manifest.json'); -const { Map: ImmutableMap, fromJS } = require('immutable'); - -const manifestMap = fromJS(manifest).reduce((acc, entry) => { - return acc.set(entry.get('symbol').toLowerCase(), entry); -}, ImmutableMap()); - -module.exports = manifestMap.toJS(); diff --git a/app/soapbox/features/crypto_donate/utils/manifest_map.ts b/app/soapbox/features/crypto_donate/utils/manifest_map.ts new file mode 100644 index 000000000..b89ba6ef2 --- /dev/null +++ b/app/soapbox/features/crypto_donate/utils/manifest_map.ts @@ -0,0 +1,11 @@ +// Converts cryptocurrency-icon's manifest file from a list to a map. +// See: https://github.com/spothq/cryptocurrency-icons/blob/master/manifest.json + +import manifest from 'cryptocurrency-icons/manifest.json'; +import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable'; + +const manifestMap = (fromJS(manifest) as ImmutableList>).reduce((acc: ImmutableMap>, entry: ImmutableMap) => { + return acc.set(entry.get('symbol')!.toLowerCase(), entry); +}, ImmutableMap()); + +export default manifestMap.toJS(); diff --git a/app/soapbox/features/public_layout/components/header.tsx b/app/soapbox/features/public_layout/components/header.tsx index 40eed3039..d44dfe450 100644 --- a/app/soapbox/features/public_layout/components/header.tsx +++ b/app/soapbox/features/public_layout/components/header.tsx @@ -48,7 +48,7 @@ const Header = () => { event.preventDefault(); setLoading(true); - dispatch(logIn(intl, username, password) as any) + dispatch(logIn(username, password) as any) .then(({ access_token }: { access_token: string }) => { return ( dispatch(verifyCredentials(access_token) as any) diff --git a/app/soapbox/features/ui/components/link_footer.tsx b/app/soapbox/features/ui/components/link_footer.tsx index b87ddaad6..4365ba9d5 100644 --- a/app/soapbox/features/ui/components/link_footer.tsx +++ b/app/soapbox/features/ui/components/link_footer.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import React from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; @@ -27,11 +27,10 @@ const LinkFooter: React.FC = (): JSX.Element => { const features = useFeatures(); const soapboxConfig = useSoapboxConfig(); - const intl = useIntl(); const dispatch = useDispatch(); const onClickLogOut: React.EventHandler = (e) => { - dispatch(logOut(intl)); + dispatch(logOut()); e.preventDefault(); }; diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index eb403c8fc..ca0d02705 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -44,7 +44,7 @@ const Navbar = () => { event.preventDefault(); setLoading(true); - dispatch(logIn(intl, username, password) as any) + dispatch(logIn(username, password) as any) .then(({ access_token }: { access_token: string }) => { setLoading(false); diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index dd2d8a6fc..01cef2b66 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -43,7 +43,7 @@ const ProfileDropdown: React.FC = ({ account, children }) => { const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.get('id')))); const handleLogOut = () => { - dispatch(logOut(intl)); + dispatch(logOut()); }; const handleSwitchAccount = (account: AccountEntity) => { diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index a04b4f0cd..2fdf9980b 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -52,7 +52,7 @@ const Registration = () => { event.preventDefault(); dispatch(createAccount(username, password)) - .then(() => dispatch(logIn(intl, username, password))) + .then(() => dispatch(logIn(username, password))) .then(({ access_token }: any) => dispatch(verifyCredentials(access_token))) .then(() => dispatch(fetchInstance())) .then(() => { diff --git a/app/soapbox/features/verification/waitlist_page.tsx b/app/soapbox/features/verification/waitlist_page.tsx index 00182e58f..71677d3d3 100644 --- a/app/soapbox/features/verification/waitlist_page.tsx +++ b/app/soapbox/features/verification/waitlist_page.tsx @@ -1,5 +1,4 @@ import React, { useEffect } from 'react'; -import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; @@ -12,7 +11,6 @@ import { useAppSelector, useOwnAccount } from 'soapbox/hooks'; const WaitlistPage = (/* { account } */) => { const dispatch = useDispatch(); - const intl = useIntl(); const title = useAppSelector((state) => state.instance.title); const me = useOwnAccount(); @@ -20,7 +18,7 @@ const WaitlistPage = (/* { account } */) => { const onClickLogOut: React.MouseEventHandler = (event) => { event.preventDefault(); - dispatch(logOut(intl)); + dispatch(logOut()); }; const openVerifySmsModal = () => dispatch(openModal('VERIFY_SMS')); diff --git a/app/soapbox/hooks/useAppDispatch.ts b/app/soapbox/hooks/useAppDispatch.ts index 11e7226d5..fa28aa0d2 100644 --- a/app/soapbox/hooks/useAppDispatch.ts +++ b/app/soapbox/hooks/useAppDispatch.ts @@ -1,5 +1,5 @@ import { useDispatch } from 'react-redux'; -import { AppDispatch } from 'soapbox/store'; +import type { AppDispatch } from 'soapbox/store'; export const useAppDispatch = () => useDispatch(); \ No newline at end of file diff --git a/app/soapbox/normalizers/__tests__/instance-test.js b/app/soapbox/normalizers/__tests__/instance.test.ts similarity index 99% rename from app/soapbox/normalizers/__tests__/instance-test.js rename to app/soapbox/normalizers/__tests__/instance.test.ts index 638cefbe7..ab51c9159 100644 --- a/app/soapbox/normalizers/__tests__/instance-test.js +++ b/app/soapbox/normalizers/__tests__/instance.test.ts @@ -27,6 +27,7 @@ describe('normalizeInstance()', () => { fedibird_capabilities: [], invites_enabled: false, languages: [], + login_message: '', pleroma: { metadata: { account_activation_required: false, diff --git a/app/soapbox/normalizers/instance.ts b/app/soapbox/normalizers/instance.ts index 7d908b3dc..e137c2165 100644 --- a/app/soapbox/normalizers/instance.ts +++ b/app/soapbox/normalizers/instance.ts @@ -39,6 +39,7 @@ export const InstanceRecord = ImmutableRecord({ fedibird_capabilities: ImmutableList(), invites_enabled: false, languages: ImmutableList(), + login_message: '', pleroma: ImmutableMap({ metadata: ImmutableMap({ account_activation_required: false, diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 41b9edf11..316517a55 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -244,8 +244,8 @@ type APIChat = { id: string, last_message: string }; export const makeGetChat = () => { return createSelector( [ - (state: RootState, { id }: APIChat) => state.chats.getIn(['items', id]) as ReducerChat, - (state: RootState, { id }: APIChat) => state.accounts.get(state.chats.getIn(['items', id, 'account'])), + (state: RootState, { id }: APIChat) => state.chats.items.get(id) as ReducerChat, + (state: RootState, { id }: APIChat) => state.accounts.get(state.chats.items.getIn([id, 'account'])), (state: RootState, { last_message }: APIChat) => state.chat_messages.get(last_message), ], diff --git a/app/soapbox/utils/auth.ts b/app/soapbox/utils/auth.ts index 1a5b1b38e..dcb84104e 100644 --- a/app/soapbox/utils/auth.ts +++ b/app/soapbox/utils/auth.ts @@ -30,11 +30,11 @@ export const isLoggedIn = (getState: () => RootState) => { return validId(getState().me); }; -export const getAppToken = (state: RootState) => state.auth.getIn(['app', 'access_token']); +export const getAppToken = (state: RootState) => state.auth.getIn(['app', 'access_token']) as string; export const getUserToken = (state: RootState, accountId?: string | false | null) => { const accountUrl = state.accounts.getIn([accountId, 'url']); - return state.auth.getIn(['users', accountUrl, 'access_token']); + return state.auth.getIn(['users', accountUrl, 'access_token']) as string; }; export const getAccessToken = (state: RootState) => { diff --git a/app/soapbox/utils/ethereum.js b/app/soapbox/utils/ethereum.js deleted file mode 100644 index 3bd8db7b1..000000000 --- a/app/soapbox/utils/ethereum.js +++ /dev/null @@ -1,26 +0,0 @@ -export const ethereum = () => window.ethereum; - -export const hasEthereum = () => Boolean(ethereum()); - -// Requests an Ethereum wallet from the browser -// Returns a Promise containing the Ethereum wallet address (string). -export const getWallet = () => { - return ethereum().request({ method: 'eth_requestAccounts' }) - .then(wallets => wallets[0]); -}; - -// Asks the browser to sign a message with Ethereum. -// Returns a Promise containing the signature (string). -export const signMessage = (wallet, message) => { - return ethereum().request({ method: 'personal_sign', params: [message, wallet] }); -}; - -// Combines the above functions. -// Returns an object with the `wallet` and `signature` -export const getWalletAndSign = message => { - return getWallet().then(wallet => { - return signMessage(wallet, message).then(signature => { - return { wallet, signature }; - }); - }); -}; diff --git a/app/soapbox/utils/ethereum.ts b/app/soapbox/utils/ethereum.ts new file mode 100644 index 000000000..63dace72d --- /dev/null +++ b/app/soapbox/utils/ethereum.ts @@ -0,0 +1,28 @@ +import type { MetaMaskInpageProvider } from '@metamask/providers'; + +export const ethereum: () => MetaMaskInpageProvider | undefined = () => (window as any).ethereum; + +export const hasEthereum = () => Boolean(ethereum()); + +// Requests an Ethereum wallet from the browser +// Returns a Promise containing the Ethereum wallet address (string). +export const getWallet: () => Promise = () => { + return ethereum()!.request({ method: 'eth_requestAccounts' }) + .then((wallets) => (wallets as Array)[0]); +}; + +// Asks the browser to sign a message with Ethereum. +// Returns a Promise containing the signature (string). +export const signMessage = (wallet: string, message: string) => { + return ethereum()!.request({ method: 'personal_sign', params: [message, wallet] }); +}; + +// Combines the above functions. +// Returns an object with the `wallet` and `signature` +export const getWalletAndSign = (message: string) => { + return getWallet().then(wallet => { + return signMessage(wallet, message).then(signature => { + return { wallet, signature }; + }); + }); +}; diff --git a/package.json b/package.json index 4399a14f2..dbced84ea 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@gamestdio/websocket": "^0.3.2", "@jest/globals": "^27.5.1", "@lcdp/offline-plugin": "^5.1.0", + "@metamask/providers": "^9.0.0", "@popperjs/core": "^2.4.4", "@reach/menu-button": "^0.16.2", "@reach/popover": "^0.16.2", diff --git a/yarn.lock b/yarn.lock index 16cba47d9..fbdd06355 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1848,6 +1848,38 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@metamask/object-multiplex@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-1.2.0.tgz#38fc15c142f61939391e1b9a8eed679696c7e4f4" + integrity sha512-hksV602d3NWE2Q30Mf2Np1WfVKaGqfJRy9vpHAmelbaD0OkDt06/0KQkRR6UVYdMbTbkuEu8xN5JDUU80inGwQ== + dependencies: + end-of-stream "^1.4.4" + once "^1.4.0" + readable-stream "^2.3.3" + +"@metamask/providers@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-9.0.0.tgz#644684f9eceb952138e80afb9103c7e39d8350fe" + integrity sha512-9qUkaFafZUROK0CAUBqjsut+7mqKOXFhBCpAhAPVRBqj5TfUTdPI4t8S7GYzPVaDbC7M6kH/YLNCgcfaFWAS+w== + dependencies: + "@metamask/object-multiplex" "^1.1.0" + "@metamask/safe-event-emitter" "^2.0.0" + "@types/chrome" "^0.0.136" + detect-browser "^5.2.0" + eth-rpc-errors "^4.0.2" + extension-port-stream "^2.0.1" + fast-deep-equal "^2.0.1" + is-stream "^2.0.0" + json-rpc-engine "^6.1.0" + json-rpc-middleware-stream "^3.0.0" + pump "^3.0.0" + webextension-polyfill-ts "^0.25.0" + +"@metamask/safe-event-emitter@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" + integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2252,6 +2284,14 @@ dependencies: "@types/node" "*" +"@types/chrome@^0.0.136": + version "0.0.136" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.136.tgz#7c011b9f997b0156f25a140188a0c5689d3f368f" + integrity sha512-XDEiRhLkMd+SB7Iw3ZUIj/fov3wLd4HyTdLltVszkgl1dBfc3Rb7oPMVZ2Mz2TLqnF7Ow+StbR8E7r9lqpb4DA== + dependencies: + "@types/filesystem" "*" + "@types/har-format" "*" + "@types/connect-history-api-fallback@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" @@ -2317,6 +2357,18 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/filesystem@*": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf" + integrity sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ== + dependencies: + "@types/filewriter" "*" + +"@types/filewriter@*": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee" + integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ== + "@types/fs-extra@^9.0.1": version "9.0.13" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" @@ -2331,6 +2383,11 @@ dependencies: "@types/node" "*" +"@types/har-format@*": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.8.tgz#e6908b76d4c88be3db642846bb8b455f0bfb1c4e" + integrity sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ== + "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" @@ -4480,6 +4537,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-browser@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca" + integrity sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w== + detect-it@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/detect-it/-/detect-it-4.0.1.tgz#3f8de6b8330f5086270571251bedf10aec049e18" @@ -4742,6 +4804,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +end-of-stream@^1.1.0, end-of-stream@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + enhanced-resolve@^5.0.0: version "5.9.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz#49ac24953ac8452ed8fed2ef1340fc8e043667ee" @@ -5149,6 +5218,13 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eth-rpc-errors@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" + integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== + dependencies: + fast-safe-stringify "^2.0.6" + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -5255,6 +5331,13 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extension-port-stream@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extension-port-stream/-/extension-port-stream-2.0.1.tgz#d374820c581418c2275d3c4439ade0b82c4cfac6" + integrity sha512-ltrv4Dh/979I04+D4Te6TFygfRSOc5EBzzlHRldWMS8v73V80qWluxH88hqF0qyUsBXTb8NmzlmSipcre6a+rg== + dependencies: + webextension-polyfill-ts "^0.22.0" + fake-indexeddb@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-3.1.7.tgz#d9efbeade113c15efbe862e4598a4b0a1797ed9f" @@ -5262,6 +5345,11 @@ fake-indexeddb@^3.1.7: dependencies: realistic-structured-clone "^2.0.1" +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5299,6 +5387,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.0.6: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -7052,6 +7145,22 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-rpc-engine@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz#bf5ff7d029e1c1bf20cb6c0e9f348dcd8be5a393" + integrity sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ== + dependencies: + "@metamask/safe-event-emitter" "^2.0.0" + eth-rpc-errors "^4.0.2" + +json-rpc-middleware-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-3.0.0.tgz#8540331d884f36b9e0ad31054cc68ac6b5a89b52" + integrity sha512-JmZmlehE0xF3swwORpLHny/GvW3MZxCsb2uFNBrn8TOqMqivzCfz232NSDLLOtIQlrPlgyEjiYpyzyOPFOzClw== + dependencies: + "@metamask/safe-event-emitter" "^2.0.0" + readable-stream "^2.3.3" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -8000,7 +8109,7 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -8738,6 +8847,14 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -9166,7 +9283,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.1: +readable-stream@^2.0.1, readable-stream@^2.3.3: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -10876,6 +10993,25 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +webextension-polyfill-ts@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.22.0.tgz#86cfd7bab4d9d779d98c8340983f4b691b2343f3" + integrity sha512-3P33ClMwZ/qiAT7UH1ROrkRC1KM78umlnPpRhdC/292UyoTTW9NcjJEqDsv83HbibcTB6qCtpVeuB2q2/oniHQ== + dependencies: + webextension-polyfill "^0.7.0" + +webextension-polyfill-ts@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.25.0.tgz#fff041626365dbd0e29c40b197e989a55ec221ca" + integrity sha512-ikQhwwHYkpBu00pFaUzIKY26I6L87DeRI+Q6jBT1daZUNuu8dSrg5U9l/ZbqdaQ1M/TTSPKeAa3kolP5liuedw== + dependencies: + webextension-polyfill "^0.7.0" + +webextension-polyfill@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.7.0.tgz#0df1120ff0266056319ce1a622b09ad8d4a56505" + integrity sha512-su48BkMLxqzTTvPSE1eWxKToPS2Tv5DLGxKexLEVpwFd6Po6N8hhSLIvG6acPAg7qERoEaDL+Y5HQJeJeml5Aw== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"