Merge branch 'ts' into 'develop'
TypeScript: ChatPanes, CryptoDonate, utils/ethereum See merge request soapbox-pub/soapbox-fe!1521
This commit is contained in:
@@ -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<string, string>, baseURL?: string) {
|
||||
return (dispatch: React.Dispatch<AnyAction>) => {
|
||||
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<string, string>;
|
||||
}).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<AnyAction>) => {
|
||||
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 });
|
||||
@@ -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<string, string>) =>
|
||||
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<string, string | number>) =>
|
||||
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<string, string | number>) => 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<string, string | number>) => 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 const loadCredentials = (token: string, accountUrl: string) =>
|
||||
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
|
||||
.then(() => {
|
||||
dispatch(verifyCredentials(token, accountUrl));
|
||||
})
|
||||
.catch(() => 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 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<string, string>) => {
|
||||
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<string, any>) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
params.fullname = params.username;
|
||||
|
||||
return dispatch(createAppAndToken())
|
||||
.then(() => dispatch(createAccount(params)))
|
||||
.then(({ token }) => {
|
||||
.then(({ token }: { token: Record<string, string | number> }) => {
|
||||
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<string, string | number>) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({ type: AUTH_LOGGED_IN, token });
|
||||
return token;
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
|
||||
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<string, string>;
|
||||
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<string, string | number>) => 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<string, string> = {
|
||||
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<string, string | number>) => 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 = '/');
|
||||
};
|
||||
}
|
||||
@@ -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<string, string>, 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<string, string>) =>
|
||||
(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;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -25,7 +25,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ 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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<ICaptchaField> = ({
|
||||
const [refresh, setRefresh] = useState<NodeJS.Timer | undefined>(undefined);
|
||||
|
||||
const getCaptcha = () => {
|
||||
dispatch(fetchCaptcha()).then(response => {
|
||||
dispatch(fetchCaptcha()).then((response: AxiosResponse) => {
|
||||
const captcha = ImmutableMap<string, any>(response.data);
|
||||
setCaptcha(captcha);
|
||||
onFetch(captcha);
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
@@ -32,8 +32,8 @@ const OtpAuthForm: React.FC<IOtpAuthForm> = ({ 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<string, any>) => {
|
||||
setShouldRedirect(true);
|
||||
return dispatch(switchAccount(account.id));
|
||||
}).catch(() => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ interface IChat {
|
||||
|
||||
const Chat: React.FC<IChat> = ({ 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;
|
||||
|
||||
|
||||
@@ -49,9 +49,9 @@ const ChatList: React.FC<IChatList> = ({ 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) {
|
||||
|
||||
@@ -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 = (
|
||||
<div className={`pane pane--main pane--${mainWindowState}`}>
|
||||
<div className='pane__header'>
|
||||
{unreadCount > 0 && (
|
||||
<div className='mr-2 flex-none'>
|
||||
<Counter count={unreadCount} />
|
||||
</div>
|
||||
)}
|
||||
<button className='pane__title' onClick={this.handleMainWindowToggle}>
|
||||
<FormattedMessage id='chat_panels.main_window.title' defaultMessage='Chats' />
|
||||
</button>
|
||||
<AudioToggle />
|
||||
</div>
|
||||
<div className='pane__content'>
|
||||
{open && (
|
||||
<>
|
||||
<ChatList
|
||||
onClickChat={this.handleClickChat}
|
||||
/>
|
||||
<AccountSearch
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
onSelected={this.handleSuggestion}
|
||||
resultsPosition='above'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='chat-panes'>
|
||||
{mainWindowPane}
|
||||
{panes.map((pane, i) => (
|
||||
<ChatWindow
|
||||
idx={i}
|
||||
key={pane.get('chat_id')}
|
||||
chatId={pane.get('chat_id')}
|
||||
windowState={pane.get('state')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
108
app/soapbox/features/chats/components/chat_panes.tsx
Normal file
108
app/soapbox/features/chats/components/chat_panes.tsx
Normal file
@@ -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<string, Chat>, panes = ImmutableList<ImmutableMap<string, any>>()) => (
|
||||
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 = (
|
||||
<div className={`pane pane--main pane--${mainWindowState}`}>
|
||||
<div className='pane__header'>
|
||||
{unreadCount > 0 && (
|
||||
<div className='mr-2 flex-none'>
|
||||
<Counter count={unreadCount} />
|
||||
</div>
|
||||
)}
|
||||
<button className='pane__title' onClick={handleMainWindowToggle}>
|
||||
<FormattedMessage id='chat_panels.main_window.title' defaultMessage='Chats' />
|
||||
</button>
|
||||
<AudioToggle />
|
||||
</div>
|
||||
<div className='pane__content'>
|
||||
{open && (
|
||||
<>
|
||||
<ChatList
|
||||
onClickChat={handleClickChat}
|
||||
/>
|
||||
<AccountSearch
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
onSelected={handleSuggestion}
|
||||
resultsPosition='above'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='chat-panes'>
|
||||
{mainWindowPane}
|
||||
{panes.map((pane, i) => (
|
||||
<ChatWindow
|
||||
idx={i}
|
||||
key={pane.get('chat_id')}
|
||||
chatId={pane.get('chat_id')}
|
||||
windowState={pane.get('state')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPanes;
|
||||
@@ -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();
|
||||
11
app/soapbox/features/crypto_donate/utils/manifest_map.ts
Normal file
11
app/soapbox/features/crypto_donate/utils/manifest_map.ts
Normal file
@@ -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<ImmutableMap<string, string>>).reduce((acc: ImmutableMap<string, ImmutableMap<string, string>>, entry: ImmutableMap<string, string>) => {
|
||||
return acc.set(entry.get('symbol')!.toLowerCase(), entry);
|
||||
}, ImmutableMap());
|
||||
|
||||
export default manifestMap.toJS();
|
||||
@@ -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)
|
||||
|
||||
@@ -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<React.MouseEvent> = (e) => {
|
||||
dispatch(logOut(intl));
|
||||
dispatch(logOut());
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ 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) => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { AppDispatch } from 'soapbox/store';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
@@ -27,6 +27,7 @@ describe('normalizeInstance()', () => {
|
||||
fedibird_capabilities: [],
|
||||
invites_enabled: false,
|
||||
languages: [],
|
||||
login_message: '',
|
||||
pleroma: {
|
||||
metadata: {
|
||||
account_activation_required: false,
|
||||
@@ -39,6 +39,7 @@ export const InstanceRecord = ImmutableRecord({
|
||||
fedibird_capabilities: ImmutableList(),
|
||||
invites_enabled: false,
|
||||
languages: ImmutableList(),
|
||||
login_message: '',
|
||||
pleroma: ImmutableMap<string, any>({
|
||||
metadata: ImmutableMap<string, any>({
|
||||
account_activation_required: false,
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
});
|
||||
};
|
||||
28
app/soapbox/utils/ethereum.ts
Normal file
28
app/soapbox/utils/ethereum.ts
Normal file
@@ -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<string> = () => {
|
||||
return ethereum()!.request({ method: 'eth_requestAccounts' })
|
||||
.then((wallets) => (wallets as Array<string>)[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 };
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user