Merge remote-tracking branch 'soapbox/develop' into lexical
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
5161b3cba9
|
@ -1,15 +1,12 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildRelationship } from 'soapbox/jest/factory';
|
||||
import { buildAccount, buildRelationship } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes';
|
||||
|
||||
import { normalizeAccount } from '../../normalizers';
|
||||
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
describe('submitAccountNote()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
|
@ -72,13 +69,13 @@ describe('initAccountNoteModal()', () => {
|
|||
});
|
||||
|
||||
it('dispatches the proper actions', async() => {
|
||||
const account = normalizeAccount({
|
||||
const account = buildAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
verified: true,
|
||||
}) as Account;
|
||||
});
|
||||
const expectedActions = [
|
||||
{ type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' },
|
||||
{ type: 'MODAL_CLOSE', modalType: 'ACCOUNT_NOTE' },
|
||||
|
|
|
@ -19,12 +19,10 @@ import {
|
|||
fetchFollowing,
|
||||
fetchFollowRequests,
|
||||
fetchRelationships,
|
||||
followAccount,
|
||||
muteAccount,
|
||||
removeFromFollowers,
|
||||
subscribeAccount,
|
||||
unblockAccount,
|
||||
unfollowAccount,
|
||||
unmuteAccount,
|
||||
unsubscribeAccount,
|
||||
} from '../accounts';
|
||||
|
@ -76,9 +74,14 @@ describe('fetchAccount()', () => {
|
|||
});
|
||||
|
||||
const state = rootState
|
||||
.set('accounts', ImmutableMap({
|
||||
[id]: account,
|
||||
}) as any);
|
||||
.set('entities', {
|
||||
'ACCOUNTS': {
|
||||
store: {
|
||||
[id]: account,
|
||||
},
|
||||
lists: {},
|
||||
},
|
||||
});
|
||||
|
||||
store = mockStore(state);
|
||||
|
||||
|
@ -168,9 +171,14 @@ describe('fetchAccountByUsername()', () => {
|
|||
});
|
||||
|
||||
state = rootState
|
||||
.set('accounts', ImmutableMap({
|
||||
[id]: account,
|
||||
}));
|
||||
.set('entities', {
|
||||
'ACCOUNTS': {
|
||||
store: {
|
||||
[id]: account,
|
||||
},
|
||||
lists: {},
|
||||
},
|
||||
});
|
||||
|
||||
store = mockStore(state);
|
||||
|
||||
|
@ -371,169 +379,6 @@ describe('fetchAccountByUsername()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('followAccount()', () => {
|
||||
describe('when logged out', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', null);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(followAccount('1'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logged in', () => {
|
||||
const id = '1';
|
||||
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onPost(`/api/v1/accounts/${id}/follow`).reply(200, { success: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'ACCOUNT_FOLLOW_REQUEST',
|
||||
id,
|
||||
locked: false,
|
||||
skipLoading: true,
|
||||
},
|
||||
{
|
||||
type: 'ACCOUNT_FOLLOW_SUCCESS',
|
||||
relationship: { success: true },
|
||||
alreadyFollowing: undefined,
|
||||
skipLoading: true,
|
||||
},
|
||||
];
|
||||
await store.dispatch(followAccount(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onPost(`/api/v1/accounts/${id}/follow`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'ACCOUNT_FOLLOW_REQUEST',
|
||||
id,
|
||||
locked: false,
|
||||
skipLoading: true,
|
||||
},
|
||||
{
|
||||
type: 'ACCOUNT_FOLLOW_FAIL',
|
||||
error: new Error('Network Error'),
|
||||
locked: false,
|
||||
skipLoading: true,
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
await store.dispatch(followAccount(id));
|
||||
} catch (e) {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toEqual(expectedActions);
|
||||
expect(e).toEqual(new Error('Network Error'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unfollowAccount()', () => {
|
||||
describe('when logged out', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', null);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(unfollowAccount('1'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logged in', () => {
|
||||
const id = '1';
|
||||
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onPost(`/api/v1/accounts/${id}/unfollow`).reply(200, { success: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'ACCOUNT_UNFOLLOW_REQUEST', id: '1', skipLoading: true },
|
||||
{
|
||||
type: 'ACCOUNT_UNFOLLOW_SUCCESS',
|
||||
relationship: { success: true },
|
||||
statuses: ImmutableMap({}),
|
||||
skipLoading: true,
|
||||
},
|
||||
];
|
||||
await store.dispatch(unfollowAccount(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onPost(`/api/v1/accounts/${id}/unfollow`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'ACCOUNT_UNFOLLOW_REQUEST',
|
||||
id,
|
||||
skipLoading: true,
|
||||
},
|
||||
{
|
||||
type: 'ACCOUNT_UNFOLLOW_FAIL',
|
||||
error: new Error('Network Error'),
|
||||
skipLoading: true,
|
||||
},
|
||||
];
|
||||
await store.dispatch(unfollowAccount(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('blockAccount()', () => {
|
||||
const id = '1';
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { AccountRecord } from 'soapbox/normalizers';
|
||||
import { AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth';
|
||||
|
||||
import { AuthUserRecord, ReducerRecord } from '../../reducers/auth';
|
||||
import {
|
||||
fetchMe, patchMe,
|
||||
} from '../me';
|
||||
import { fetchMe, patchMe } from '../me';
|
||||
|
||||
jest.mock('../../storage/kv-store', () => ({
|
||||
__esModule: true,
|
||||
|
@ -48,11 +46,15 @@ describe('fetchMe()', () => {
|
|||
}),
|
||||
}),
|
||||
}))
|
||||
.set('accounts', ImmutableMap({
|
||||
[accountUrl]: AccountRecord({
|
||||
url: accountUrl,
|
||||
}),
|
||||
}) as any);
|
||||
.set('entities', {
|
||||
'ACCOUNTS': {
|
||||
store: {
|
||||
[accountUrl]: buildAccount({ url: accountUrl }),
|
||||
},
|
||||
lists: {},
|
||||
},
|
||||
});
|
||||
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { rootState } from '../../jest/test-helpers';
|
||||
import { rootState } from 'soapbox/jest/test-helpers';
|
||||
import { RootState } from 'soapbox/store';
|
||||
|
||||
import { getSoapboxConfig } from '../soapbox';
|
||||
|
||||
const ASCII_HEART = '❤'; // '\u2764\uFE0F'
|
||||
|
@ -6,13 +8,13 @@ const RED_HEART_RGI = '❤️'; // '\u2764'
|
|||
|
||||
describe('getSoapboxConfig()', () => {
|
||||
it('returns RGI heart on Pleroma > 2.3', () => {
|
||||
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.3.0)');
|
||||
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.3.0)') as RootState;
|
||||
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(true);
|
||||
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns an ASCII heart on Pleroma < 2.3', () => {
|
||||
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.0.0)');
|
||||
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.0.0)') as RootState;
|
||||
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(true);
|
||||
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(false);
|
||||
});
|
||||
|
|
|
@ -4,8 +4,8 @@ import { openModal, closeModal } from './modals';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
||||
const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||
|
||||
|
@ -23,14 +25,6 @@ const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
|||
const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
||||
const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
|
||||
|
||||
const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
|
||||
const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
|
||||
const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
|
||||
|
||||
const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
|
||||
const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
|
||||
const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
|
||||
|
||||
const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
|
||||
const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
|
||||
const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
|
||||
|
@ -227,81 +221,6 @@ const fetchAccountFail = (id: string | null, error: AxiosError) => ({
|
|||
skipAlert: true,
|
||||
});
|
||||
|
||||
type FollowAccountOpts = {
|
||||
reblogs?: boolean
|
||||
notify?: boolean
|
||||
};
|
||||
|
||||
const followAccount = (id: string, options?: FollowAccountOpts) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
||||
const alreadyFollowing = getState().relationships.get(id)?.following || undefined;
|
||||
const locked = getState().accounts.get(id)?.locked || false;
|
||||
|
||||
dispatch(followAccountRequest(id, locked));
|
||||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/follow`, options)
|
||||
.then(response => dispatch(followAccountSuccess(response.data, alreadyFollowing)))
|
||||
.catch(error => {
|
||||
dispatch(followAccountFail(error, locked));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const unfollowAccount = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
||||
dispatch(unfollowAccountRequest(id));
|
||||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/unfollow`)
|
||||
.then(response => dispatch(unfollowAccountSuccess(response.data, getState().statuses)))
|
||||
.catch(error => dispatch(unfollowAccountFail(error)));
|
||||
};
|
||||
|
||||
const followAccountRequest = (id: string, locked: boolean) => ({
|
||||
type: ACCOUNT_FOLLOW_REQUEST,
|
||||
id,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const followAccountSuccess = (relationship: APIEntity, alreadyFollowing?: boolean) => ({
|
||||
type: ACCOUNT_FOLLOW_SUCCESS,
|
||||
relationship,
|
||||
alreadyFollowing,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const followAccountFail = (error: AxiosError, locked: boolean) => ({
|
||||
type: ACCOUNT_FOLLOW_FAIL,
|
||||
error,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const unfollowAccountRequest = (id: string) => ({
|
||||
type: ACCOUNT_UNFOLLOW_REQUEST,
|
||||
id,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const unfollowAccountSuccess = (relationship: APIEntity, statuses: ImmutableMap<string, Status>) => ({
|
||||
type: ACCOUNT_UNFOLLOW_SUCCESS,
|
||||
relationship,
|
||||
statuses,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const unfollowAccountFail = (error: AxiosError) => ({
|
||||
type: ACCOUNT_UNFOLLOW_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const blockAccount = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
@ -690,7 +609,10 @@ const fetchRelationships = (accountIds: string[]) =>
|
|||
|
||||
return api(getState)
|
||||
.get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`)
|
||||
.then(response => dispatch(fetchRelationshipsSuccess(response.data)))
|
||||
.then(response => {
|
||||
dispatch(importEntities(response.data, Entities.RELATIONSHIPS));
|
||||
dispatch(fetchRelationshipsSuccess(response.data));
|
||||
})
|
||||
.catch(error => dispatch(fetchRelationshipsFail(error)));
|
||||
};
|
||||
|
||||
|
@ -988,12 +910,6 @@ export {
|
|||
ACCOUNT_FETCH_REQUEST,
|
||||
ACCOUNT_FETCH_SUCCESS,
|
||||
ACCOUNT_FETCH_FAIL,
|
||||
ACCOUNT_FOLLOW_REQUEST,
|
||||
ACCOUNT_FOLLOW_SUCCESS,
|
||||
ACCOUNT_FOLLOW_FAIL,
|
||||
ACCOUNT_UNFOLLOW_REQUEST,
|
||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||
ACCOUNT_UNFOLLOW_FAIL,
|
||||
ACCOUNT_BLOCK_REQUEST,
|
||||
ACCOUNT_BLOCK_SUCCESS,
|
||||
ACCOUNT_BLOCK_FAIL,
|
||||
|
@ -1069,14 +985,6 @@ export {
|
|||
fetchAccountRequest,
|
||||
fetchAccountSuccess,
|
||||
fetchAccountFail,
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
followAccountRequest,
|
||||
followAccountSuccess,
|
||||
followAccountFail,
|
||||
unfollowAccountRequest,
|
||||
unfollowAccountSuccess,
|
||||
unfollowAccountFail,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
blockAccountRequest,
|
||||
|
|
|
@ -10,8 +10,8 @@ import { importFetchedAccounts } from './importer';
|
|||
import { patchMeSuccess } from './me';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Account } from 'soapbox/types/entities';
|
||||
|
||||
const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST';
|
||||
const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS';
|
||||
|
@ -56,7 +56,7 @@ const fetchAliasesRequest = () => ({
|
|||
type: ALIASES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchAliasesSuccess = (aliases: APIEntity[]) => ({
|
||||
const fetchAliasesSuccess = (aliases: unknown[]) => ({
|
||||
type: ALIASES_FETCH_SUCCESS,
|
||||
value: aliases,
|
||||
});
|
||||
|
@ -82,7 +82,7 @@ const fetchAliasesSuggestions = (q: string) =>
|
|||
}).catch(error => toast.showAlertForError(error));
|
||||
};
|
||||
|
||||
const fetchAliasesSuggestionsReady = (query: string, accounts: APIEntity[]) => ({
|
||||
const fetchAliasesSuggestionsReady = (query: string, accounts: unknown[]) => ({
|
||||
type: ALIASES_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
|
@ -111,7 +111,7 @@ const addToAliases = (account: Account) =>
|
|||
|
||||
dispatch(addToAliasesRequest());
|
||||
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma.get('ap_id')] })
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma?.ap_id] })
|
||||
.then((response => {
|
||||
toast.success(messages.createSuccess);
|
||||
dispatch(addToAliasesSuccess);
|
||||
|
|
|
@ -24,72 +24,71 @@ import { createStatus } from './statuses';
|
|||
import type { EditorState } from 'lexical';
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
import type { Account, Group } from 'soapbox/schemas';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
||||
import type { APIEntity, Status, Tag } from 'soapbox/types/entities';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
||||
const { CancelToken, isCancel } = axios;
|
||||
|
||||
let cancelFetchComposeSuggestions: Canceler;
|
||||
|
||||
const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
||||
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
||||
const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
||||
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY';
|
||||
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||
const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
||||
const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
||||
const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||
const COMPOSE_RESET = 'COMPOSE_RESET';
|
||||
const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
|
||||
const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
||||
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST';
|
||||
const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE';
|
||||
const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const;
|
||||
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const;
|
||||
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const;
|
||||
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const;
|
||||
const COMPOSE_REPLY = 'COMPOSE_REPLY' as const;
|
||||
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const;
|
||||
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const;
|
||||
const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const;
|
||||
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const;
|
||||
const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const;
|
||||
const COMPOSE_MENTION = 'COMPOSE_MENTION' as const;
|
||||
const COMPOSE_RESET = 'COMPOSE_RESET' as const;
|
||||
const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const;
|
||||
const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const;
|
||||
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const;
|
||||
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const;
|
||||
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const;
|
||||
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const;
|
||||
const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE' as const;
|
||||
|
||||
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||
const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
||||
const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
||||
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const;
|
||||
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY' as const;
|
||||
const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT' as const;
|
||||
const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE' as const;
|
||||
|
||||
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
||||
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE' as const;
|
||||
|
||||
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
||||
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||
const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||
const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||
const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
||||
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const;
|
||||
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE' as const;
|
||||
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const;
|
||||
const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const;
|
||||
const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const;
|
||||
|
||||
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
|
||||
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const;
|
||||
|
||||
const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
|
||||
const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
||||
const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
||||
const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const;
|
||||
const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' as const;
|
||||
const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const;
|
||||
|
||||
const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
|
||||
const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
|
||||
const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD';
|
||||
const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
|
||||
const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
|
||||
const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
||||
const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const;
|
||||
const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' as const;
|
||||
const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD' as const;
|
||||
const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' as const;
|
||||
const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const;
|
||||
const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE' as const;
|
||||
|
||||
const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
|
||||
const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
|
||||
const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
|
||||
const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD' as const;
|
||||
const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const;
|
||||
const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const;
|
||||
|
||||
const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
|
||||
const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
|
||||
const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const;
|
||||
const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS' as const;
|
||||
|
||||
const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||
const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const;
|
||||
|
||||
const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET';
|
||||
const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
|
||||
|
||||
const messages = defineMessages({
|
||||
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
||||
|
@ -105,12 +104,24 @@ const messages = defineMessages({
|
|||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
interface ComposeSetStatusAction {
|
||||
type: typeof COMPOSE_SET_STATUS
|
||||
id: string
|
||||
status: Status
|
||||
rawText: string
|
||||
explicitAddressing: boolean
|
||||
spoilerText?: string
|
||||
contentType?: string | false
|
||||
v: ReturnType<typeof parseVersion>
|
||||
withRedraft?: boolean
|
||||
}
|
||||
|
||||
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { instance } = getState();
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
const action: ComposeSetStatusAction = {
|
||||
type: COMPOSE_SET_STATUS,
|
||||
id: 'compose-modal',
|
||||
status,
|
||||
|
@ -120,7 +131,9 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin
|
|||
contentType,
|
||||
v: parseVersion(instance.version),
|
||||
withRedraft,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const changeCompose = (composeId: string, text: string) => ({
|
||||
|
@ -129,20 +142,29 @@ const changeCompose = (composeId: string, text: string) => ({
|
|||
text: text,
|
||||
});
|
||||
|
||||
interface ComposeReplyAction {
|
||||
type: typeof COMPOSE_REPLY
|
||||
id: string
|
||||
status: Status
|
||||
account: Account
|
||||
explicitAddressing: boolean
|
||||
}
|
||||
|
||||
const replyCompose = (status: Status) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
const action: ComposeReplyAction = {
|
||||
type: COMPOSE_REPLY,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
account: state.accounts.get(state.me)!,
|
||||
explicitAddressing,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
|
@ -151,20 +173,29 @@ const cancelReplyCompose = () => ({
|
|||
id: 'compose-modal',
|
||||
});
|
||||
|
||||
interface ComposeQuoteAction {
|
||||
type: typeof COMPOSE_QUOTE
|
||||
id: string
|
||||
status: Status
|
||||
account: Account | undefined
|
||||
explicitAddressing: boolean
|
||||
}
|
||||
|
||||
const quoteCompose = (status: Status) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
const action: ComposeQuoteAction = {
|
||||
type: COMPOSE_QUOTE,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
explicitAddressing,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
|
@ -186,38 +217,54 @@ const resetCompose = (composeId = 'compose-modal') => ({
|
|||
id: composeId,
|
||||
});
|
||||
|
||||
interface ComposeMentionAction {
|
||||
type: typeof COMPOSE_MENTION
|
||||
id: string
|
||||
account: Account
|
||||
}
|
||||
|
||||
const mentionCompose = (account: Account) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
const action: ComposeMentionAction = {
|
||||
type: COMPOSE_MENTION,
|
||||
id: 'compose-modal',
|
||||
account: account,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
interface ComposeDirectAction {
|
||||
type: typeof COMPOSE_DIRECT
|
||||
id: string
|
||||
account: Account
|
||||
}
|
||||
|
||||
const directCompose = (account: Account) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
const action: ComposeDirectAction = {
|
||||
type: COMPOSE_DIRECT,
|
||||
id: 'compose-modal',
|
||||
account: account,
|
||||
});
|
||||
account,
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
const directComposeById = (accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const account = getState().accounts.get(accountId);
|
||||
if (!account) return;
|
||||
|
||||
dispatch({
|
||||
const action: ComposeDirectAction = {
|
||||
type: COMPOSE_DIRECT,
|
||||
id: 'compose-modal',
|
||||
account: account,
|
||||
});
|
||||
account,
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
|
@ -494,14 +541,11 @@ const undoUploadCompose = (composeId: string, media_id: string) => ({
|
|||
media_id: media_id,
|
||||
});
|
||||
|
||||
const groupCompose = (composeId: string, groupId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_GROUP_POST,
|
||||
id: composeId,
|
||||
group_id: groupId,
|
||||
});
|
||||
};
|
||||
const groupCompose = (composeId: string, groupId: string) => ({
|
||||
type: COMPOSE_GROUP_POST,
|
||||
id: composeId,
|
||||
group_id: groupId,
|
||||
});
|
||||
|
||||
const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolean) => ({
|
||||
type: COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
|
||||
|
@ -598,6 +642,14 @@ const fetchComposeSuggestions = (composeId: string, token: string) =>
|
|||
}
|
||||
};
|
||||
|
||||
interface ComposeSuggestionsReadyAction {
|
||||
type: typeof COMPOSE_SUGGESTIONS_READY
|
||||
id: string
|
||||
token: string
|
||||
emojis?: Emoji[]
|
||||
accounts?: APIEntity[]
|
||||
}
|
||||
|
||||
const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({
|
||||
type: COMPOSE_SUGGESTIONS_READY,
|
||||
id: composeId,
|
||||
|
@ -612,6 +664,15 @@ const readyComposeSuggestionsAccounts = (composeId: string, token: string, accou
|
|||
accounts,
|
||||
});
|
||||
|
||||
interface ComposeSuggestionSelectAction {
|
||||
type: typeof COMPOSE_SUGGESTION_SELECT
|
||||
id: string
|
||||
position: number
|
||||
token: string | null
|
||||
completion: string
|
||||
path: Array<string | number>
|
||||
}
|
||||
|
||||
const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
let completion, startPosition;
|
||||
|
@ -629,14 +690,16 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
|||
startPosition = position;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
const action: ComposeSuggestionSelectAction = {
|
||||
type: COMPOSE_SUGGESTION_SELECT,
|
||||
id: composeId,
|
||||
position: startPosition,
|
||||
token,
|
||||
completion,
|
||||
path,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const updateSuggestionTags = (composeId: string, token: string, tags: ImmutableList<Tag>) => ({
|
||||
|
@ -746,7 +809,7 @@ const removePollOption = (composeId: string, index: number) => ({
|
|||
index,
|
||||
});
|
||||
|
||||
const changePollSettings = (composeId: string, expiresIn?: string | number, isMultiple?: boolean) => ({
|
||||
const changePollSettings = (composeId: string, expiresIn?: number, isMultiple?: boolean) => ({
|
||||
type: COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
id: composeId,
|
||||
expiresIn,
|
||||
|
@ -760,30 +823,54 @@ const openComposeWithText = (composeId: string, text = '') =>
|
|||
dispatch(changeCompose(composeId, text));
|
||||
};
|
||||
|
||||
interface ComposeAddToMentionsAction {
|
||||
type: typeof COMPOSE_ADD_TO_MENTIONS
|
||||
id: string
|
||||
account: string
|
||||
}
|
||||
|
||||
const addToMentions = (composeId: string, accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
|
||||
return dispatch({
|
||||
const action: ComposeAddToMentionsAction = {
|
||||
type: COMPOSE_ADD_TO_MENTIONS,
|
||||
id: composeId,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
|
||||
return dispatch(action);
|
||||
};
|
||||
|
||||
interface ComposeRemoveFromMentionsAction {
|
||||
type: typeof COMPOSE_REMOVE_FROM_MENTIONS
|
||||
id: string
|
||||
account: string
|
||||
}
|
||||
|
||||
const removeFromMentions = (composeId: string, accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
|
||||
return dispatch({
|
||||
const action: ComposeRemoveFromMentionsAction = {
|
||||
type: COMPOSE_REMOVE_FROM_MENTIONS,
|
||||
id: composeId,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
|
||||
return dispatch(action);
|
||||
};
|
||||
|
||||
interface ComposeEventReplyAction {
|
||||
type: typeof COMPOSE_EVENT_REPLY
|
||||
id: string
|
||||
status: Status
|
||||
account: Account
|
||||
explicitAddressing: boolean
|
||||
}
|
||||
|
||||
const eventDiscussionCompose = (composeId: string, status: Status) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
@ -805,6 +892,53 @@ const setEditorState = (composeId: string, editorState: EditorState | string | n
|
|||
editorState: editorState,
|
||||
});
|
||||
|
||||
type ComposeAction =
|
||||
ComposeSetStatusAction
|
||||
| ReturnType<typeof changeCompose>
|
||||
| ComposeReplyAction
|
||||
| ReturnType<typeof cancelReplyCompose>
|
||||
| ComposeQuoteAction
|
||||
| ReturnType<typeof cancelQuoteCompose>
|
||||
| ReturnType<typeof resetCompose>
|
||||
| ComposeMentionAction
|
||||
| ComposeDirectAction
|
||||
| ReturnType<typeof submitComposeRequest>
|
||||
| ReturnType<typeof submitComposeSuccess>
|
||||
| ReturnType<typeof submitComposeFail>
|
||||
| ReturnType<typeof changeUploadComposeRequest>
|
||||
| ReturnType<typeof changeUploadComposeSuccess>
|
||||
| ReturnType<typeof changeUploadComposeFail>
|
||||
| ReturnType<typeof uploadComposeRequest>
|
||||
| ReturnType<typeof uploadComposeProgress>
|
||||
| ReturnType<typeof uploadComposeSuccess>
|
||||
| ReturnType<typeof uploadComposeFail>
|
||||
| ReturnType<typeof undoUploadCompose>
|
||||
| ReturnType<typeof groupCompose>
|
||||
| ReturnType<typeof setGroupTimelineVisible>
|
||||
| ReturnType<typeof clearComposeSuggestions>
|
||||
| ComposeSuggestionsReadyAction
|
||||
| ComposeSuggestionSelectAction
|
||||
| ReturnType<typeof updateSuggestionTags>
|
||||
| ReturnType<typeof updateTagHistory>
|
||||
| ReturnType<typeof changeComposeSpoilerness>
|
||||
| ReturnType<typeof changeComposeContentType>
|
||||
| ReturnType<typeof changeComposeSpoilerText>
|
||||
| ReturnType<typeof changeComposeVisibility>
|
||||
| ReturnType<typeof insertEmojiCompose>
|
||||
| ReturnType<typeof addPoll>
|
||||
| ReturnType<typeof removePoll>
|
||||
| ReturnType<typeof addSchedule>
|
||||
| ReturnType<typeof setSchedule>
|
||||
| ReturnType<typeof removeSchedule>
|
||||
| ReturnType<typeof addPollOption>
|
||||
| ReturnType<typeof changePollOption>
|
||||
| ReturnType<typeof removePollOption>
|
||||
| ReturnType<typeof changePollSettings>
|
||||
| ComposeAddToMentionsAction
|
||||
| ComposeRemoveFromMentionsAction
|
||||
| ComposeEventReplyAction
|
||||
| ReturnType<typeof setEditorState>
|
||||
|
||||
export {
|
||||
COMPOSE_CHANGE,
|
||||
COMPOSE_SUBMIT_REQUEST,
|
||||
|
@ -834,7 +968,6 @@ export {
|
|||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
COMPOSE_VISIBILITY_CHANGE,
|
||||
COMPOSE_LISTABILITY_CHANGE,
|
||||
COMPOSE_COMPOSING_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
|
@ -907,4 +1040,5 @@ export {
|
|||
removeFromMentions,
|
||||
eventDiscussionCompose,
|
||||
setEditorState,
|
||||
type ComposeAction,
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@ import { isLoggedIn } from 'soapbox/utils/auth';
|
|||
import api, { getLinks } from '../api';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
|
||||
|
@ -30,8 +29,11 @@ const blockDomain = (domain: string) =>
|
|||
|
||||
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
|
||||
const at_domain = '@' + domain;
|
||||
const accounts = getState().accounts.filter(item => item.acct.endsWith(at_domain)).valueSeq().map(item => item.id);
|
||||
dispatch(blockDomainSuccess(domain, accounts.toList()));
|
||||
const accounts = getState().accounts
|
||||
.filter(item => item.acct.endsWith(at_domain))
|
||||
.map(item => item.id);
|
||||
|
||||
dispatch(blockDomainSuccess(domain, accounts));
|
||||
}).catch(err => {
|
||||
dispatch(blockDomainFail(domain, err));
|
||||
});
|
||||
|
@ -42,7 +44,7 @@ const blockDomainRequest = (domain: string) => ({
|
|||
domain,
|
||||
});
|
||||
|
||||
const blockDomainSuccess = (domain: string, accounts: ImmutableList<string>) => ({
|
||||
const blockDomainSuccess = (domain: string, accounts: string[]) => ({
|
||||
type: DOMAIN_BLOCK_SUCCESS,
|
||||
domain,
|
||||
accounts,
|
||||
|
@ -68,8 +70,8 @@ const unblockDomain = (domain: string) =>
|
|||
|
||||
api(getState).delete('/api/v1/domain_blocks', params).then(() => {
|
||||
const at_domain = '@' + domain;
|
||||
const accounts = getState().accounts.filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
|
||||
dispatch(unblockDomainSuccess(domain, accounts.toList()));
|
||||
const accounts = getState().accounts.filter(item => item.acct.endsWith(at_domain)).map(item => item.id);
|
||||
dispatch(unblockDomainSuccess(domain, accounts));
|
||||
}).catch(err => {
|
||||
dispatch(unblockDomainFail(domain, err));
|
||||
});
|
||||
|
@ -80,7 +82,7 @@ const unblockDomainRequest = (domain: string) => ({
|
|||
domain,
|
||||
});
|
||||
|
||||
const unblockDomainSuccess = (domain: string, accounts: ImmutableList<string>) => ({
|
||||
const unblockDomainSuccess = (domain: string, accounts: string[]) => ({
|
||||
type: DOMAIN_UNBLOCK_SUCCESS,
|
||||
domain,
|
||||
accounts,
|
||||
|
|
|
@ -77,7 +77,7 @@ const emojiReact = (status: Status, emoji: string, custom?: string) =>
|
|||
dispatch(emojiReactRequest(status, emoji, custom));
|
||||
|
||||
return api(getState)
|
||||
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||
.put(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`)
|
||||
.then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(emojiReactSuccess(status, emoji));
|
||||
|
@ -93,7 +93,7 @@ const unEmojiReact = (status: Status, emoji: string) =>
|
|||
dispatch(unEmojiReactRequest(status, emoji));
|
||||
|
||||
return api(getState)
|
||||
.delete(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||
.delete(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`)
|
||||
.then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unEmojiReactSuccess(status, emoji));
|
||||
|
|
|
@ -15,73 +15,74 @@ import {
|
|||
} from './statuses';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { ReducerStatus } from 'soapbox/reducers/statuses';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST';
|
||||
const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS';
|
||||
const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL';
|
||||
const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST' as const;
|
||||
const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS' as const;
|
||||
const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL' as const;
|
||||
|
||||
const EDIT_EVENT_NAME_CHANGE = 'EDIT_EVENT_NAME_CHANGE';
|
||||
const EDIT_EVENT_DESCRIPTION_CHANGE = 'EDIT_EVENT_DESCRIPTION_CHANGE';
|
||||
const EDIT_EVENT_START_TIME_CHANGE = 'EDIT_EVENT_START_TIME_CHANGE';
|
||||
const EDIT_EVENT_HAS_END_TIME_CHANGE = 'EDIT_EVENT_HAS_END_TIME_CHANGE';
|
||||
const EDIT_EVENT_END_TIME_CHANGE = 'EDIT_EVENT_END_TIME_CHANGE';
|
||||
const EDIT_EVENT_APPROVAL_REQUIRED_CHANGE = 'EDIT_EVENT_APPROVAL_REQUIRED_CHANGE';
|
||||
const EDIT_EVENT_LOCATION_CHANGE = 'EDIT_EVENT_LOCATION_CHANGE';
|
||||
const EDIT_EVENT_NAME_CHANGE = 'EDIT_EVENT_NAME_CHANGE' as const;
|
||||
const EDIT_EVENT_DESCRIPTION_CHANGE = 'EDIT_EVENT_DESCRIPTION_CHANGE' as const;
|
||||
const EDIT_EVENT_START_TIME_CHANGE = 'EDIT_EVENT_START_TIME_CHANGE' as const;
|
||||
const EDIT_EVENT_HAS_END_TIME_CHANGE = 'EDIT_EVENT_HAS_END_TIME_CHANGE' as const;
|
||||
const EDIT_EVENT_END_TIME_CHANGE = 'EDIT_EVENT_END_TIME_CHANGE' as const;
|
||||
const EDIT_EVENT_APPROVAL_REQUIRED_CHANGE = 'EDIT_EVENT_APPROVAL_REQUIRED_CHANGE' as const;
|
||||
const EDIT_EVENT_LOCATION_CHANGE = 'EDIT_EVENT_LOCATION_CHANGE' as const;
|
||||
|
||||
const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST';
|
||||
const EVENT_BANNER_UPLOAD_PROGRESS = 'EVENT_BANNER_UPLOAD_PROGRESS';
|
||||
const EVENT_BANNER_UPLOAD_SUCCESS = 'EVENT_BANNER_UPLOAD_SUCCESS';
|
||||
const EVENT_BANNER_UPLOAD_FAIL = 'EVENT_BANNER_UPLOAD_FAIL';
|
||||
const EVENT_BANNER_UPLOAD_UNDO = 'EVENT_BANNER_UPLOAD_UNDO';
|
||||
const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST' as const;
|
||||
const EVENT_BANNER_UPLOAD_PROGRESS = 'EVENT_BANNER_UPLOAD_PROGRESS' as const;
|
||||
const EVENT_BANNER_UPLOAD_SUCCESS = 'EVENT_BANNER_UPLOAD_SUCCESS' as const;
|
||||
const EVENT_BANNER_UPLOAD_FAIL = 'EVENT_BANNER_UPLOAD_FAIL' as const;
|
||||
const EVENT_BANNER_UPLOAD_UNDO = 'EVENT_BANNER_UPLOAD_UNDO' as const;
|
||||
|
||||
const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST';
|
||||
const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS';
|
||||
const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL';
|
||||
const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST' as const;
|
||||
const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS' as const;
|
||||
const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL' as const;
|
||||
|
||||
const EVENT_JOIN_REQUEST = 'EVENT_JOIN_REQUEST';
|
||||
const EVENT_JOIN_SUCCESS = 'EVENT_JOIN_SUCCESS';
|
||||
const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL';
|
||||
const EVENT_JOIN_REQUEST = 'EVENT_JOIN_REQUEST' as const;
|
||||
const EVENT_JOIN_SUCCESS = 'EVENT_JOIN_SUCCESS' as const;
|
||||
const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL' as const;
|
||||
|
||||
const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST';
|
||||
const EVENT_LEAVE_SUCCESS = 'EVENT_LEAVE_SUCCESS';
|
||||
const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL';
|
||||
const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST' as const;
|
||||
const EVENT_LEAVE_SUCCESS = 'EVENT_LEAVE_SUCCESS' as const;
|
||||
const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL' as const;
|
||||
|
||||
const EVENT_PARTICIPATIONS_FETCH_REQUEST = 'EVENT_PARTICIPATIONS_FETCH_REQUEST';
|
||||
const EVENT_PARTICIPATIONS_FETCH_SUCCESS = 'EVENT_PARTICIPATIONS_FETCH_SUCCESS';
|
||||
const EVENT_PARTICIPATIONS_FETCH_FAIL = 'EVENT_PARTICIPATIONS_FETCH_FAIL';
|
||||
const EVENT_PARTICIPATIONS_FETCH_REQUEST = 'EVENT_PARTICIPATIONS_FETCH_REQUEST' as const;
|
||||
const EVENT_PARTICIPATIONS_FETCH_SUCCESS = 'EVENT_PARTICIPATIONS_FETCH_SUCCESS' as const;
|
||||
const EVENT_PARTICIPATIONS_FETCH_FAIL = 'EVENT_PARTICIPATIONS_FETCH_FAIL' as const;
|
||||
|
||||
const EVENT_PARTICIPATIONS_EXPAND_REQUEST = 'EVENT_PARTICIPATIONS_EXPAND_REQUEST';
|
||||
const EVENT_PARTICIPATIONS_EXPAND_SUCCESS = 'EVENT_PARTICIPATIONS_EXPAND_SUCCESS';
|
||||
const EVENT_PARTICIPATIONS_EXPAND_FAIL = 'EVENT_PARTICIPATIONS_EXPAND_FAIL';
|
||||
const EVENT_PARTICIPATIONS_EXPAND_REQUEST = 'EVENT_PARTICIPATIONS_EXPAND_REQUEST' as const;
|
||||
const EVENT_PARTICIPATIONS_EXPAND_SUCCESS = 'EVENT_PARTICIPATIONS_EXPAND_SUCCESS' as const;
|
||||
const EVENT_PARTICIPATIONS_EXPAND_FAIL = 'EVENT_PARTICIPATIONS_EXPAND_FAIL' as const;
|
||||
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL = 'EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL';
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST' as const;
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS' as const;
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL = 'EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL' as const;
|
||||
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL';
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST' as const;
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS' as const;
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL' as const;
|
||||
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL';
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST' as const;
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS' as const;
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL' as const;
|
||||
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST = 'EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_FAIL = 'EVENT_PARTICIPATION_REQUEST_REJECT_FAIL';
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST = 'EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST' as const;
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS' as const;
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_FAIL = 'EVENT_PARTICIPATION_REQUEST_REJECT_FAIL' as const;
|
||||
|
||||
const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL';
|
||||
const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL' as const;
|
||||
|
||||
const EVENT_FORM_SET = 'EVENT_FORM_SET';
|
||||
const EVENT_FORM_SET = 'EVENT_FORM_SET' as const;
|
||||
|
||||
const RECENT_EVENTS_FETCH_REQUEST = 'RECENT_EVENTS_FETCH_REQUEST';
|
||||
const RECENT_EVENTS_FETCH_SUCCESS = 'RECENT_EVENTS_FETCH_SUCCESS';
|
||||
const RECENT_EVENTS_FETCH_FAIL = 'RECENT_EVENTS_FETCH_FAIL';
|
||||
const JOINED_EVENTS_FETCH_REQUEST = 'JOINED_EVENTS_FETCH_REQUEST';
|
||||
const JOINED_EVENTS_FETCH_SUCCESS = 'JOINED_EVENTS_FETCH_SUCCESS';
|
||||
const JOINED_EVENTS_FETCH_FAIL = 'JOINED_EVENTS_FETCH_FAIL';
|
||||
const RECENT_EVENTS_FETCH_REQUEST = 'RECENT_EVENTS_FETCH_REQUEST' as const;
|
||||
const RECENT_EVENTS_FETCH_SUCCESS = 'RECENT_EVENTS_FETCH_SUCCESS' as const;
|
||||
const RECENT_EVENTS_FETCH_FAIL = 'RECENT_EVENTS_FETCH_FAIL' as const;
|
||||
const JOINED_EVENTS_FETCH_REQUEST = 'JOINED_EVENTS_FETCH_REQUEST' as const;
|
||||
const JOINED_EVENTS_FETCH_SUCCESS = 'JOINED_EVENTS_FETCH_SUCCESS' as const;
|
||||
const JOINED_EVENTS_FETCH_FAIL = 'JOINED_EVENTS_FETCH_FAIL' as const;
|
||||
|
||||
const noOp = () => new Promise(f => f(undefined));
|
||||
|
||||
|
@ -576,6 +577,13 @@ const cancelEventCompose = () => ({
|
|||
type: EVENT_COMPOSE_CANCEL,
|
||||
});
|
||||
|
||||
interface EventFormSetAction {
|
||||
type: typeof EVENT_FORM_SET
|
||||
status: ReducerStatus
|
||||
text: string
|
||||
location: Record<string, any>
|
||||
}
|
||||
|
||||
const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const status = getState().statuses.get(id)!;
|
||||
|
||||
|
@ -637,6 +645,10 @@ const fetchJoinedEvents = () =>
|
|||
});
|
||||
};
|
||||
|
||||
type EventsAction =
|
||||
| ReturnType<typeof cancelEventCompose>
|
||||
| EventFormSetAction;
|
||||
|
||||
export {
|
||||
LOCATION_SEARCH_REQUEST,
|
||||
LOCATION_SEARCH_SUCCESS,
|
||||
|
@ -743,4 +755,5 @@ export {
|
|||
editEvent,
|
||||
fetchRecentEvents,
|
||||
fetchJoinedEvents,
|
||||
type EventsAction,
|
||||
};
|
||||
|
|
|
@ -2,8 +2,11 @@ import { AppDispatch, RootState } from 'soapbox/store';
|
|||
|
||||
import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
export const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST';
|
||||
export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS';
|
||||
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
|
||||
|
@ -19,6 +22,7 @@ export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: A
|
|||
const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts;
|
||||
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(fetchRelationships(accounts.map((item: APIEntity) => item.id)));
|
||||
dispatch({
|
||||
type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
|
||||
id: accountId,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { Group, groupSchema } from 'soapbox/schemas';
|
||||
import { Group, accountSchema, groupSchema } from 'soapbox/schemas';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
|
||||
import { getSettings } from '../settings';
|
||||
|
@ -17,11 +17,27 @@ const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
|||
const POLLS_IMPORT = 'POLLS_IMPORT';
|
||||
const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
|
||||
|
||||
const importAccount = (account: APIEntity) =>
|
||||
({ type: ACCOUNT_IMPORT, account });
|
||||
const importAccount = (data: APIEntity) =>
|
||||
(dispatch: AppDispatch, _getState: () => RootState) => {
|
||||
dispatch({ type: ACCOUNT_IMPORT, account: data });
|
||||
try {
|
||||
const account = accountSchema.parse(data);
|
||||
dispatch(importEntities([account], Entities.ACCOUNTS));
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const importAccounts = (accounts: APIEntity[]) =>
|
||||
({ type: ACCOUNTS_IMPORT, accounts });
|
||||
const importAccounts = (data: APIEntity[]) =>
|
||||
(dispatch: AppDispatch, _getState: () => RootState) => {
|
||||
dispatch({ type: ACCOUNTS_IMPORT, accounts: data });
|
||||
try {
|
||||
const accounts = filteredArray(accountSchema).parse(data);
|
||||
dispatch(importEntities(accounts, Entities.ACCOUNTS));
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const importGroup = (group: Group) =>
|
||||
importEntities([group], Entities.GROUPS);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { defineMessages } from 'react-intl';
|
|||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
|
@ -73,6 +73,12 @@ const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST';
|
|||
const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS';
|
||||
const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL';
|
||||
|
||||
const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
|
||||
const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
|
||||
|
||||
const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||
|
||||
const messages = defineMessages({
|
||||
bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
|
||||
bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
|
||||
|
@ -85,7 +91,7 @@ const reblog = (status: StatusEntity) =>
|
|||
|
||||
dispatch(reblogRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function(response) {
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/reblog`).then(function(response) {
|
||||
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
||||
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||
dispatch(importFetchedStatus(response.data.reblog));
|
||||
|
@ -101,7 +107,7 @@ const unreblog = (status: StatusEntity) =>
|
|||
|
||||
dispatch(unreblogRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(() => {
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/unreblog`).then(() => {
|
||||
dispatch(unreblogSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(unreblogFail(status, error));
|
||||
|
@ -234,7 +240,7 @@ const dislike = (status: StatusEntity) =>
|
|||
|
||||
dispatch(dislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() {
|
||||
api(getState).post(`/api/friendica/statuses/${status.id}/dislike`).then(function() {
|
||||
dispatch(dislikeSuccess(status));
|
||||
}).catch(function(error) {
|
||||
dispatch(dislikeFail(status, error));
|
||||
|
@ -247,7 +253,7 @@ const undislike = (status: StatusEntity) =>
|
|||
|
||||
dispatch(undislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => {
|
||||
api(getState).post(`/api/friendica/statuses/${status.id}/undislike`).then(() => {
|
||||
dispatch(undislikeSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(undislikeFail(status, error));
|
||||
|
@ -305,7 +311,7 @@ const bookmark = (status: StatusEntity) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/bookmark`).then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(bookmarkSuccess(status, response.data));
|
||||
toast.success(messages.bookmarkAdded, {
|
||||
|
@ -321,7 +327,7 @@ const unbookmark = (status: StatusEntity) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(unbookmarkRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/unbookmark`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unbookmarkSuccess(status, response.data));
|
||||
toast.success(messages.bookmarkRemoved);
|
||||
|
@ -380,9 +386,10 @@ const fetchReblogs = (id: string) =>
|
|||
dispatch(fetchReblogsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchReblogsSuccess(id, response.data));
|
||||
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReblogsFail(id, error));
|
||||
});
|
||||
|
@ -393,10 +400,11 @@ const fetchReblogsRequest = (id: string) => ({
|
|||
id,
|
||||
});
|
||||
|
||||
const fetchReblogsSuccess = (id: string, accounts: APIEntity[]) => ({
|
||||
const fetchReblogsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: REBLOGS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchReblogsFail = (id: string, error: AxiosError) => ({
|
||||
|
@ -405,6 +413,31 @@ const fetchReblogsFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const expandReblogs = (id: string, path: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
api(getState).get(path).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandReblogsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandReblogsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: REBLOGS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandReblogsFail = (id: string, error: AxiosError) => ({
|
||||
type: REBLOGS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchFavourites = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
@ -412,9 +445,10 @@ const fetchFavourites = (id: string) =>
|
|||
dispatch(fetchFavouritesRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchFavouritesFail(id, error));
|
||||
});
|
||||
|
@ -425,10 +459,11 @@ const fetchFavouritesRequest = (id: string) => ({
|
|||
id,
|
||||
});
|
||||
|
||||
const fetchFavouritesSuccess = (id: string, accounts: APIEntity[]) => ({
|
||||
const fetchFavouritesSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: FAVOURITES_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchFavouritesFail = (id: string, error: AxiosError) => ({
|
||||
|
@ -437,6 +472,31 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const expandFavourites = (id: string, path: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
api(getState).get(path).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandFavouritesFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandFavouritesSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: FAVOURITES_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandFavouritesFail = (id: string, error: AxiosError) => ({
|
||||
type: FAVOURITES_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchDislikes = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
@ -504,7 +564,7 @@ const pin = (status: StatusEntity) =>
|
|||
|
||||
dispatch(pinRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/pin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(pinSuccess(status));
|
||||
}).catch(error => {
|
||||
|
@ -515,14 +575,14 @@ const pin = (status: StatusEntity) =>
|
|||
const pinToGroup = (status: StatusEntity, group: Group) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
return api(getState)
|
||||
.post(`/api/v1/groups/${group.id}/statuses/${status.get('id')}/pin`)
|
||||
.post(`/api/v1/groups/${group.id}/statuses/${status.id}/pin`)
|
||||
.then(() => dispatch(expandGroupFeaturedTimeline(group.id)));
|
||||
};
|
||||
|
||||
const unpinFromGroup = (status: StatusEntity, group: Group) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
return api(getState)
|
||||
.post(`/api/v1/groups/${group.id}/statuses/${status.get('id')}/unpin`)
|
||||
.post(`/api/v1/groups/${group.id}/statuses/${status.id}/unpin`)
|
||||
.then(() => dispatch(expandGroupFeaturedTimeline(group.id)));
|
||||
};
|
||||
|
||||
|
@ -551,7 +611,7 @@ const unpin = (status: StatusEntity) =>
|
|||
|
||||
dispatch(unpinRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/unpin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unpinSuccess(status));
|
||||
}).catch(error => {
|
||||
|
@ -669,6 +729,10 @@ export {
|
|||
REMOTE_INTERACTION_REQUEST,
|
||||
REMOTE_INTERACTION_SUCCESS,
|
||||
REMOTE_INTERACTION_FAIL,
|
||||
FAVOURITES_EXPAND_SUCCESS,
|
||||
FAVOURITES_EXPAND_FAIL,
|
||||
REBLOGS_EXPAND_SUCCESS,
|
||||
REBLOGS_EXPAND_FAIL,
|
||||
reblog,
|
||||
unreblog,
|
||||
toggleReblog,
|
||||
|
@ -709,10 +773,12 @@ export {
|
|||
fetchReblogsRequest,
|
||||
fetchReblogsSuccess,
|
||||
fetchReblogsFail,
|
||||
expandReblogs,
|
||||
fetchFavourites,
|
||||
fetchFavouritesRequest,
|
||||
fetchFavouritesSuccess,
|
||||
fetchFavouritesFail,
|
||||
expandFavourites,
|
||||
fetchDislikes,
|
||||
fetchDislikesRequest,
|
||||
fetchDislikesSuccess,
|
||||
|
|
|
@ -10,14 +10,14 @@ import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
|
|||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST';
|
||||
const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS';
|
||||
const ME_FETCH_FAIL = 'ME_FETCH_FAIL';
|
||||
const ME_FETCH_SKIP = 'ME_FETCH_SKIP';
|
||||
const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST' as const;
|
||||
const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS' as const;
|
||||
const ME_FETCH_FAIL = 'ME_FETCH_FAIL' as const;
|
||||
const ME_FETCH_SKIP = 'ME_FETCH_SKIP' as const;
|
||||
|
||||
const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST';
|
||||
const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS';
|
||||
const ME_PATCH_FAIL = 'ME_PATCH_FAIL';
|
||||
const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST' as const;
|
||||
const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS' as const;
|
||||
const ME_PATCH_FAIL = 'ME_PATCH_FAIL' as const;
|
||||
|
||||
const noOp = () => new Promise(f => f(undefined));
|
||||
|
||||
|
@ -85,13 +85,10 @@ const fetchMeRequest = () => ({
|
|||
type: ME_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchMeSuccess = (me: APIEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: ME_FETCH_SUCCESS,
|
||||
me,
|
||||
});
|
||||
};
|
||||
const fetchMeSuccess = (me: APIEntity) => ({
|
||||
type: ME_FETCH_SUCCESS,
|
||||
me,
|
||||
});
|
||||
|
||||
const fetchMeFail = (error: APIEntity) => ({
|
||||
type: ME_FETCH_FAIL,
|
||||
|
@ -103,13 +100,20 @@ const patchMeRequest = () => ({
|
|||
type: ME_PATCH_REQUEST,
|
||||
});
|
||||
|
||||
interface MePatchSuccessAction {
|
||||
type: typeof ME_PATCH_SUCCESS
|
||||
me: APIEntity
|
||||
}
|
||||
|
||||
const patchMeSuccess = (me: APIEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch(importFetchedAccount(me));
|
||||
dispatch({
|
||||
const action: MePatchSuccessAction = {
|
||||
type: ME_PATCH_SUCCESS,
|
||||
me,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(importFetchedAccount(me));
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const patchMeFail = (error: AxiosError) => ({
|
||||
|
@ -118,6 +122,14 @@ const patchMeFail = (error: AxiosError) => ({
|
|||
skipAlert: true,
|
||||
});
|
||||
|
||||
type MeAction =
|
||||
| ReturnType<typeof fetchMeRequest>
|
||||
| ReturnType<typeof fetchMeSuccess>
|
||||
| ReturnType<typeof fetchMeFail>
|
||||
| ReturnType<typeof patchMeRequest>
|
||||
| MePatchSuccessAction
|
||||
| ReturnType<typeof patchMeFail>;
|
||||
|
||||
export {
|
||||
ME_FETCH_REQUEST,
|
||||
ME_FETCH_SUCCESS,
|
||||
|
@ -134,4 +146,5 @@ export {
|
|||
patchMeRequest,
|
||||
patchMeSuccess,
|
||||
patchMeFail,
|
||||
type MeAction,
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm =
|
|||
const message = (
|
||||
<Stack space={4}>
|
||||
<OutlineBox>
|
||||
<AccountContainer id={accountId} />
|
||||
<AccountContainer id={accountId} hideActions />
|
||||
</OutlineBox>
|
||||
|
||||
<Text>
|
||||
|
@ -83,7 +83,7 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
|||
const message = (
|
||||
<Stack space={4}>
|
||||
<OutlineBox>
|
||||
<AccountContainer id={accountId} />
|
||||
<AccountContainer id={accountId} hideActions />
|
||||
</OutlineBox>
|
||||
|
||||
<Text>
|
||||
|
|
|
@ -1,95 +1,14 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getNextLinkName } from 'soapbox/utils/quirks';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { openModal } from './modals';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||
const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||
const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
|
||||
|
||||
const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
|
||||
const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
||||
const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
||||
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
||||
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
|
||||
|
||||
const fetchMutes = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const nextLinkName = getNextLinkName(getState);
|
||||
|
||||
dispatch(fetchMutesRequest());
|
||||
|
||||
api(getState).get('/api/v1/mutes').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => dispatch(fetchMutesFail(error)));
|
||||
};
|
||||
|
||||
const fetchMutesRequest = () => ({
|
||||
type: MUTES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchMutesSuccess = (accounts: APIEntity[], next: string | null) => ({
|
||||
type: MUTES_FETCH_SUCCESS,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchMutesFail = (error: AxiosError) => ({
|
||||
type: MUTES_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandMutes = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const nextLinkName = getNextLinkName(getState);
|
||||
|
||||
const url = getState().user_lists.mutes.next;
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandMutesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => dispatch(expandMutesFail(error)));
|
||||
};
|
||||
|
||||
const expandMutesRequest = () => ({
|
||||
type: MUTES_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
const expandMutesSuccess = (accounts: APIEntity[], next: string | null) => ({
|
||||
type: MUTES_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandMutesFail = (error: AxiosError) => ({
|
||||
type: MUTES_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const initMuteModal = (account: AccountEntity) =>
|
||||
const initMuteModal = (account: AccountEntity | Account) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: MUTES_INIT_MODAL,
|
||||
|
@ -113,23 +32,9 @@ const changeMuteDuration = (duration: number) =>
|
|||
};
|
||||
|
||||
export {
|
||||
MUTES_FETCH_REQUEST,
|
||||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_FETCH_FAIL,
|
||||
MUTES_EXPAND_REQUEST,
|
||||
MUTES_EXPAND_SUCCESS,
|
||||
MUTES_EXPAND_FAIL,
|
||||
MUTES_INIT_MODAL,
|
||||
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||
MUTES_CHANGE_DURATION,
|
||||
fetchMutes,
|
||||
fetchMutesRequest,
|
||||
fetchMutesSuccess,
|
||||
fetchMutesFail,
|
||||
expandMutes,
|
||||
expandMutesRequest,
|
||||
expandMutesSuccess,
|
||||
expandMutesFail,
|
||||
initMuteModal,
|
||||
toggleHideNotifications,
|
||||
changeMuteDuration,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification';
|
|||
import { joinPublicPath } from 'soapbox/utils/static';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { fetchGroupRelationships } from './groups';
|
||||
import {
|
||||
importFetchedAccount,
|
||||
importFetchedAccounts,
|
||||
|
@ -23,7 +24,7 @@ import { getSettings, saveSettings } from './settings';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
import type { APIEntity, Status } from 'soapbox/types/entities';
|
||||
|
||||
const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||
|
@ -237,6 +238,9 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
|||
dispatch(importFetchedAccounts(Object.values(entries.accounts)));
|
||||
dispatch(importFetchedStatuses(Object.values(entries.statuses)));
|
||||
|
||||
const statusesFromGroups = (Object.values(entries.statuses) as Status[]).filter((status) => !!status.group);
|
||||
dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id)));
|
||||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
|
||||
fetchRelatedRelationships(dispatch, response.data);
|
||||
done();
|
||||
|
|
|
@ -3,8 +3,9 @@ import api from '../api';
|
|||
import { openModal } from './modals';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account, ChatMessage, Group, Status } from 'soapbox/types/entities';
|
||||
import type { ChatMessage, Group, Status } from 'soapbox/types/entities';
|
||||
|
||||
const REPORT_INIT = 'REPORT_INIT';
|
||||
const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
|
@ -83,7 +83,9 @@ const submitSearch = (filter?: SearchFilter) =>
|
|||
dispatch(importFetchedStatuses(response.data.statuses));
|
||||
}
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value, type, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchSearchFail(error));
|
||||
|
@ -95,11 +97,12 @@ const fetchSearchRequest = (value: string) => ({
|
|||
value,
|
||||
});
|
||||
|
||||
const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({
|
||||
const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({
|
||||
type: SEARCH_FETCH_SUCCESS,
|
||||
results,
|
||||
searchTerm,
|
||||
searchType,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchSearchFail = (error: AxiosError) => ({
|
||||
|
@ -125,17 +128,26 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
|||
|
||||
dispatch(expandSearchRequest(type));
|
||||
|
||||
const params: Record<string, any> = {
|
||||
q: value,
|
||||
type,
|
||||
offset,
|
||||
};
|
||||
let url = getState().search.next as string;
|
||||
let params: Record<string, any> = {};
|
||||
|
||||
if (accountId) params.account_id = accountId;
|
||||
// if no URL was extracted from the Link header,
|
||||
// fall back on querying with the offset
|
||||
if (!url) {
|
||||
url = '/api/v2/search';
|
||||
params = {
|
||||
q: value,
|
||||
type,
|
||||
offset,
|
||||
};
|
||||
if (accountId) params.account_id = accountId;
|
||||
}
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
api(getState).get(url, {
|
||||
params,
|
||||
}).then(({ data }) => {
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (data.accounts) {
|
||||
dispatch(importFetchedAccounts(data.accounts));
|
||||
}
|
||||
|
@ -144,7 +156,9 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
|||
dispatch(importFetchedStatuses(data.statuses));
|
||||
}
|
||||
|
||||
dispatch(expandSearchSuccess(data, value, type));
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(expandSearchSuccess(data, value, type, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandSearchFail(error));
|
||||
|
@ -156,11 +170,12 @@ const expandSearchRequest = (searchType: SearchFilter) => ({
|
|||
searchType,
|
||||
});
|
||||
|
||||
const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({
|
||||
const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({
|
||||
type: SEARCH_EXPAND_SUCCESS,
|
||||
results,
|
||||
searchTerm,
|
||||
searchType,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandSearchFail = (error: AxiosError) => ({
|
||||
|
|
|
@ -10,9 +10,9 @@ import { isLoggedIn } from 'soapbox/utils/auth';
|
|||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
const SETTING_CHANGE = 'SETTING_CHANGE';
|
||||
const SETTING_SAVE = 'SETTING_SAVE';
|
||||
const SETTINGS_UPDATE = 'SETTINGS_UPDATE';
|
||||
const SETTING_CHANGE = 'SETTING_CHANGE' as const;
|
||||
const SETTING_SAVE = 'SETTING_SAVE' as const;
|
||||
const SETTINGS_UPDATE = 'SETTINGS_UPDATE' as const;
|
||||
|
||||
const FE_NAME = 'soapbox_fe';
|
||||
|
||||
|
@ -181,25 +181,33 @@ const getSettings = createSelector([
|
|||
.mergeDeep(settings);
|
||||
});
|
||||
|
||||
interface SettingChangeAction {
|
||||
type: typeof SETTING_CHANGE
|
||||
path: string[]
|
||||
value: any
|
||||
}
|
||||
|
||||
const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
const action: SettingChangeAction = {
|
||||
type: SETTING_CHANGE,
|
||||
path,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
dispatch(saveSettingsImmediate(opts));
|
||||
};
|
||||
|
||||
const changeSetting = (path: string[], value: any, opts?: SettingOpts) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
const action: SettingChangeAction = {
|
||||
type: SETTING_CHANGE,
|
||||
path,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
return dispatch(saveSettings(opts));
|
||||
};
|
||||
|
||||
|
@ -236,6 +244,10 @@ const getLocale = (state: RootState, fallback = 'en') => {
|
|||
return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback;
|
||||
};
|
||||
|
||||
type SettingsAction =
|
||||
| SettingChangeAction
|
||||
| { type: typeof SETTING_SAVE }
|
||||
|
||||
export {
|
||||
SETTING_CHANGE,
|
||||
SETTING_SAVE,
|
||||
|
@ -248,4 +260,5 @@ export {
|
|||
saveSettingsImmediate,
|
||||
saveSettings,
|
||||
getLocale,
|
||||
type SettingsAction,
|
||||
};
|
||||
|
|
|
@ -6,29 +6,30 @@ import { shouldFilter } from 'soapbox/utils/timelines';
|
|||
|
||||
import api, { getNextLink, getPrevLink } from '../api';
|
||||
|
||||
import { fetchGroupRelationships } from './groups';
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Status } from 'soapbox/types/entities';
|
||||
|
||||
const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||
const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||
const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
|
||||
const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE';
|
||||
const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE';
|
||||
const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
||||
const TIMELINE_UPDATE = 'TIMELINE_UPDATE' as const;
|
||||
const TIMELINE_DELETE = 'TIMELINE_DELETE' as const;
|
||||
const TIMELINE_CLEAR = 'TIMELINE_CLEAR' as const;
|
||||
const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE' as const;
|
||||
const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE' as const;
|
||||
const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP' as const;
|
||||
|
||||
const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
|
||||
const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
|
||||
const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
|
||||
const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST' as const;
|
||||
const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS' as const;
|
||||
const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const;
|
||||
|
||||
const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||
const TIMELINE_CONNECT = 'TIMELINE_CONNECT' as const;
|
||||
const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT' as const;
|
||||
|
||||
const TIMELINE_REPLACE = 'TIMELINE_REPLACE';
|
||||
const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||
const TIMELINE_CLEAR_FEED_ACCOUNT_ID = 'TIMELINE_CLEAR_FEED_ACCOUNT_ID';
|
||||
const TIMELINE_REPLACE = 'TIMELINE_REPLACE' as const;
|
||||
const TIMELINE_INSERT = 'TIMELINE_INSERT' as const;
|
||||
const TIMELINE_CLEAR_FEED_ACCOUNT_ID = 'TIMELINE_CLEAR_FEED_ACCOUNT_ID' as const;
|
||||
|
||||
const MAX_QUEUED_ITEMS = 40;
|
||||
|
||||
|
@ -39,7 +40,7 @@ const processTimelineUpdate = (timeline: string, status: APIEntity, accept: ((st
|
|||
const hasPendingStatuses = !getState().pending_statuses.isEmpty();
|
||||
|
||||
const columnSettings = getSettings(getState()).get(timeline, ImmutableMap());
|
||||
const shouldSkipQueue = shouldFilter(normalizeStatus(status) as Status, columnSettings);
|
||||
const shouldSkipQueue = shouldFilter(normalizeStatus(status) as Status, columnSettings as any);
|
||||
|
||||
if (ownStatus && hasPendingStatuses) {
|
||||
// WebSockets push statuses without the Idempotency-Key,
|
||||
|
@ -110,19 +111,29 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string)
|
|||
}
|
||||
};
|
||||
|
||||
interface TimelineDeleteAction {
|
||||
type: typeof TIMELINE_DELETE
|
||||
id: string
|
||||
accountId: string
|
||||
references: ImmutableMap<string, readonly [statusId: string, accountId: string]>
|
||||
reblogOf: unknown
|
||||
}
|
||||
|
||||
const deleteFromTimelines = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const accountId = getState().statuses.get(id)?.account;
|
||||
const references = getState().statuses.filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
|
||||
const accountId = getState().statuses.get(id)?.account?.id!;
|
||||
const references = getState().statuses.filter(status => status.reblog === id).map(status => [status.id, status.account.id] as const);
|
||||
const reblogOf = getState().statuses.getIn([id, 'reblog'], null);
|
||||
|
||||
dispatch({
|
||||
const action: TimelineDeleteAction = {
|
||||
type: TIMELINE_DELETE,
|
||||
id,
|
||||
accountId,
|
||||
references,
|
||||
reblogOf,
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const clearTimeline = (timeline: string) =>
|
||||
|
@ -177,6 +188,10 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
|||
|
||||
return api(getState).get(path, { params }).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
|
||||
const statusesFromGroups = (response.data as Status[]).filter((status) => !!status.group);
|
||||
dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id)));
|
||||
|
||||
dispatch(expandTimelineSuccess(
|
||||
timelineId,
|
||||
response.data,
|
||||
|
@ -221,29 +236,29 @@ const expandHomeTimeline = ({ url, accountId, maxId }: ExpandHomeTimelineOpts =
|
|||
return expandTimeline('home', endpoint, params, done);
|
||||
};
|
||||
|
||||
const expandPublicTimeline = ({ maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
const expandPublicTimeline = ({ url, maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`public${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
|
||||
const expandRemoteTimeline = (instance: string, { maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
const expandRemoteTimeline = (instance: string, { url, maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, url || '/api/v1/timelines/public', url ? {} : { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
|
||||
const expandCommunityTimeline = ({ maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
const expandCommunityTimeline = ({ url, maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`community${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
|
||||
const expandDirectTimeline = ({ maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
||||
const expandDirectTimeline = ({ url, maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline('direct', url || '/api/v1/timelines/direct', url ? {} : { max_id: maxId }, done);
|
||||
|
||||
const expandAccountTimeline = (accountId: string, { maxId, withReplies }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId, with_muted: true });
|
||||
const expandAccountTimeline = (accountId: string, { url, maxId, withReplies }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { exclude_replies: !withReplies, max_id: maxId, with_muted: true });
|
||||
|
||||
const expandAccountFeaturedTimeline = (accountId: string) =>
|
||||
expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, with_muted: true });
|
||||
|
||||
const expandAccountMediaTimeline = (accountId: string | number, { maxId }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
|
||||
const expandAccountMediaTimeline = (accountId: string | number, { url, maxId }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}:media`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { max_id: maxId, only_media: true, limit: 40, with_muted: true });
|
||||
|
||||
const expandListTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
const expandListTimeline = (id: string, { url, maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`list:${id}`, url || `/api/v1/timelines/list/${id}`, url ? {} : { max_id: maxId }, done);
|
||||
|
||||
const expandGroupTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
|
||||
|
@ -257,8 +272,8 @@ const expandGroupTimelineFromTag = (id: string, tagName: string, { maxId }: Reco
|
|||
const expandGroupMediaTimeline = (id: string | number, { maxId }: Record<string, any> = {}) =>
|
||||
expandTimeline(`group:${id}:media`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
|
||||
|
||||
const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record<string, any> = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
const expandHashtagTimeline = (hashtag: string, { url, maxId, tags }: Record<string, any> = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, url || `/api/v1/timelines/tag/${hashtag}`, url ? {} : {
|
||||
max_id: maxId,
|
||||
any: parseTags(tags, 'any'),
|
||||
all: parseTags(tags, 'all'),
|
||||
|
@ -322,6 +337,9 @@ const clearFeedAccountId = () => (dispatch: AppDispatch, _getState: () => RootSt
|
|||
dispatch({ type: TIMELINE_CLEAR_FEED_ACCOUNT_ID });
|
||||
};
|
||||
|
||||
// TODO: other actions
|
||||
type TimelineAction = TimelineDeleteAction;
|
||||
|
||||
export {
|
||||
TIMELINE_UPDATE,
|
||||
TIMELINE_DELETE,
|
||||
|
@ -368,4 +386,5 @@ export {
|
|||
scrollTopTimeline,
|
||||
insertSuggestionsIntoTimeline,
|
||||
clearFeedAccountId,
|
||||
type TimelineAction,
|
||||
};
|
||||
|
|
|
@ -1,25 +1,41 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type Account, accountSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useRelationship } from './useRelationship';
|
||||
|
||||
import { useRelationships } from './useRelationships';
|
||||
interface UseAccountOpts {
|
||||
withRelationship?: boolean
|
||||
}
|
||||
|
||||
function useAccount(id: string) {
|
||||
function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
const { me } = useLoggedIn();
|
||||
const { withRelationship } = opts;
|
||||
|
||||
const { entity: account, ...result } = useEntity<Account>(
|
||||
[Entities.ACCOUNTS, id],
|
||||
() => api.get(`/api/v1/accounts/${id}`),
|
||||
{ schema: accountSchema },
|
||||
[Entities.ACCOUNTS, accountId!],
|
||||
() => api.get(`/api/v1/accounts/${accountId}`),
|
||||
{ schema: accountSchema, enabled: !!accountId },
|
||||
);
|
||||
const { relationships, isLoading } = useRelationships([account?.id as string]);
|
||||
|
||||
const {
|
||||
relationship,
|
||||
isLoading: isRelationshipLoading,
|
||||
} = useRelationship(accountId, { enabled: withRelationship });
|
||||
|
||||
const isBlocked = account?.relationship?.blocked_by === true;
|
||||
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||
|
||||
return {
|
||||
...result,
|
||||
isLoading: result.isLoading || isLoading,
|
||||
account: account ? { ...account, relationship: relationships[0] || null } : undefined,
|
||||
isLoading: result.isLoading,
|
||||
isRelationshipLoading,
|
||||
isUnavailable,
|
||||
account: account ? { ...account, relationship } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { Account, accountSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useRelationships } from './useRelationships';
|
||||
|
||||
import type { EntityFn } from 'soapbox/entity-store/hooks/types';
|
||||
|
||||
interface useAccountListOpts {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
function useAccountList(listKey: string[], entityFn: EntityFn<void>, opts: useAccountListOpts = {}) {
|
||||
const { entities, ...rest } = useEntities(
|
||||
[Entities.ACCOUNTS, ...listKey],
|
||||
entityFn,
|
||||
{ schema: accountSchema, enabled: opts.enabled },
|
||||
);
|
||||
|
||||
const { relationships } = useRelationships(
|
||||
listKey,
|
||||
entities.map(({ id }) => id),
|
||||
);
|
||||
|
||||
const accounts: Account[] = entities.map((account) => ({
|
||||
...account,
|
||||
relationship: relationships[account.id],
|
||||
}));
|
||||
|
||||
return { accounts, ...rest };
|
||||
}
|
||||
|
||||
function useBlocks() {
|
||||
const api = useApi();
|
||||
return useAccountList(['blocks'], () => api.get('/api/v1/blocks'));
|
||||
}
|
||||
|
||||
function useMutes() {
|
||||
const api = useApi();
|
||||
return useAccountList(['mutes'], () => api.get('/api/v1/mutes'));
|
||||
}
|
||||
|
||||
function useFollowing(accountId: string | undefined) {
|
||||
const api = useApi();
|
||||
|
||||
return useAccountList(
|
||||
[accountId!, 'following'],
|
||||
() => api.get(`/api/v1/accounts/${accountId}/following`),
|
||||
{ enabled: !!accountId },
|
||||
);
|
||||
}
|
||||
|
||||
function useFollowers(accountId: string | undefined) {
|
||||
const api = useApi();
|
||||
|
||||
return useAccountList(
|
||||
[accountId!, 'followers'],
|
||||
() => api.get(`/api/v1/accounts/${accountId}/followers`),
|
||||
{ enabled: !!accountId },
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
useAccountList,
|
||||
useBlocks,
|
||||
useMutes,
|
||||
useFollowing,
|
||||
useFollowers,
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntityLookup } from 'soapbox/entity-store/hooks';
|
||||
import { useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type Account, accountSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useRelationship } from './useRelationship';
|
||||
|
||||
interface UseAccountLookupOpts {
|
||||
withRelationship?: boolean
|
||||
}
|
||||
|
||||
function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
const { me } = useLoggedIn();
|
||||
const { withRelationship } = opts;
|
||||
|
||||
const { entity: account, ...result } = useEntityLookup<Account>(
|
||||
Entities.ACCOUNTS,
|
||||
(account) => account.acct === acct,
|
||||
() => api.get(`/api/v1/accounts/lookup?acct=${acct}`),
|
||||
{ schema: accountSchema, enabled: !!acct },
|
||||
);
|
||||
|
||||
const {
|
||||
relationship,
|
||||
isLoading: isRelationshipLoading,
|
||||
} = useRelationship(account?.id, { enabled: withRelationship });
|
||||
|
||||
const isBlocked = account?.relationship?.blocked_by === true;
|
||||
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||
|
||||
return {
|
||||
...result,
|
||||
isLoading: result.isLoading,
|
||||
isRelationshipLoading,
|
||||
isUnavailable,
|
||||
account: account ? { ...account, relationship } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export { useAccountLookup };
|
|
@ -0,0 +1,88 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||
import { useAppDispatch, useLoggedIn } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
interface FollowOpts {
|
||||
reblogs?: boolean
|
||||
notify?: boolean
|
||||
languages?: string[]
|
||||
}
|
||||
|
||||
function useFollow() {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const { transaction } = useTransaction();
|
||||
|
||||
function followEffect(accountId: string) {
|
||||
transaction({
|
||||
Accounts: {
|
||||
[accountId]: (account) => ({
|
||||
...account,
|
||||
followers_count: account.followers_count + 1,
|
||||
}),
|
||||
},
|
||||
Relationships: {
|
||||
[accountId]: (relationship) => ({
|
||||
...relationship,
|
||||
following: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function unfollowEffect(accountId: string) {
|
||||
transaction({
|
||||
Accounts: {
|
||||
[accountId]: (account) => ({
|
||||
...account,
|
||||
followers_count: Math.max(0, account.followers_count - 1),
|
||||
}),
|
||||
},
|
||||
Relationships: {
|
||||
[accountId]: (relationship) => ({
|
||||
...relationship,
|
||||
following: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function follow(accountId: string, options: FollowOpts = {}) {
|
||||
if (!isLoggedIn) return;
|
||||
followEffect(accountId);
|
||||
|
||||
try {
|
||||
const response = await api.post(`/api/v1/accounts/${accountId}/follow`, options);
|
||||
const result = relationshipSchema.safeParse(response.data);
|
||||
if (result.success) {
|
||||
dispatch(importEntities([result.data], Entities.RELATIONSHIPS));
|
||||
}
|
||||
} catch (e) {
|
||||
unfollowEffect(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
async function unfollow(accountId: string) {
|
||||
if (!isLoggedIn) return;
|
||||
unfollowEffect(accountId);
|
||||
|
||||
try {
|
||||
await api.post(`/api/v1/accounts/${accountId}/unfollow`);
|
||||
} catch (e) {
|
||||
followEffect(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
follow,
|
||||
unfollow,
|
||||
followEffect,
|
||||
unfollowEffect,
|
||||
};
|
||||
}
|
||||
|
||||
export { useFollow };
|
|
@ -0,0 +1,18 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type PatronUser, patronUserSchema } from 'soapbox/schemas';
|
||||
|
||||
function usePatronUser(url?: string) {
|
||||
const api = useApi();
|
||||
|
||||
const { entity: patronUser, ...result } = useEntity<PatronUser>(
|
||||
[Entities.PATRON_USERS, url || ''],
|
||||
() => api.get(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`),
|
||||
{ schema: patronUserSchema, enabled: !!url },
|
||||
);
|
||||
|
||||
return { patronUser, ...result };
|
||||
}
|
||||
|
||||
export { usePatronUser };
|
|
@ -0,0 +1,28 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
interface UseRelationshipOpts {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
function useRelationship(accountId: string | undefined, opts: UseRelationshipOpts = {}) {
|
||||
const api = useApi();
|
||||
const { enabled = false } = opts;
|
||||
|
||||
const { entity: relationship, ...result } = useEntity<Relationship>(
|
||||
[Entities.RELATIONSHIPS, accountId!],
|
||||
() => api.get(`/api/v1/accounts/relationships?id[]=${accountId}`),
|
||||
{
|
||||
enabled: enabled && !!accountId,
|
||||
schema: z.array(relationshipSchema).nonempty().transform(arr => arr[0]),
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship, ...result };
|
||||
}
|
||||
|
||||
export { useRelationship };
|
|
@ -1,21 +1,26 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities';
|
||||
import { useLoggedIn } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useRelationships(ids: string[]) {
|
||||
function useRelationships(listKey: string[], ids: string[]) {
|
||||
const api = useApi();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
|
||||
const { entities: relationships, ...result } = useEntities<Relationship>(
|
||||
[Entities.RELATIONSHIPS],
|
||||
() => api.get(`/api/v1/accounts/relationships?${ids.map(id => `id[]=${id}`).join('&')}`),
|
||||
{ schema: relationshipSchema, enabled: ids.filter(Boolean).length > 0 },
|
||||
function fetchRelationships(ids: string[]) {
|
||||
const q = ids.map((id) => `id[]=${id}`).join('&');
|
||||
return api.get(`/api/v1/accounts/relationships?${q}`);
|
||||
}
|
||||
|
||||
const { entityMap: relationships, ...result } = useBatchedEntities<Relationship>(
|
||||
[Entities.RELATIONSHIPS, ...listKey],
|
||||
ids,
|
||||
fetchRelationships,
|
||||
{ schema: relationshipSchema, enabled: isLoggedIn },
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
relationships,
|
||||
};
|
||||
return { relationships, ...result };
|
||||
}
|
||||
|
||||
export { useRelationships };
|
|
@ -0,0 +1,64 @@
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { buildAccount, buildGroup } from 'soapbox/jest/factory';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeInstance } from 'soapbox/normalizers';
|
||||
|
||||
import { usePendingGroups } from '../usePendingGroups';
|
||||
|
||||
const id = '1';
|
||||
const group = buildGroup({ id, display_name: 'soapbox' });
|
||||
const store = {
|
||||
instance: normalizeInstance({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||
}),
|
||||
me: '1',
|
||||
entities: {
|
||||
[Entities.ACCOUNTS]: {
|
||||
store: {
|
||||
[id]: buildAccount({
|
||||
id,
|
||||
acct: 'tiger',
|
||||
display_name: 'Tiger',
|
||||
avatar: 'test.jpg',
|
||||
verified: true,
|
||||
}),
|
||||
},
|
||||
lists: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('usePendingGroups hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').reply(200, [group]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(usePendingGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(usePendingGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,12 +1,12 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||
|
||||
import type { Group, GroupMember } from 'soapbox/schemas';
|
||||
import type { Account, Group, GroupMember } from 'soapbox/schemas';
|
||||
|
||||
function useBlockGroupMember(group: Group, groupMember: GroupMember) {
|
||||
function useBlockGroupMember(group: Group, account: Account) {
|
||||
const { createEntity } = useEntityActions<GroupMember>(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||
{ post: `/api/v1/groups/${group.id}/blocks` },
|
||||
[Entities.GROUP_MEMBERSHIPS, account.id],
|
||||
{ post: `/api/v1/groups/${group?.id}/blocks` },
|
||||
);
|
||||
|
||||
return createEntity;
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { Group } from 'soapbox/schemas';
|
|||
|
||||
function useCancelMembershipRequest(group: Group) {
|
||||
const api = useApi();
|
||||
const me = useOwnAccount();
|
||||
const { account: me } = useOwnAccount();
|
||||
|
||||
const { createEntity, isSubmitting } = useCreateEntity(
|
||||
[Entities.GROUP_RELATIONSHIPS],
|
||||
|
|
|
@ -11,9 +11,13 @@ function useGroup(groupId: string, refetch = true) {
|
|||
const { entity: group, ...result } = useEntity<Group>(
|
||||
[Entities.GROUPS, groupId],
|
||||
() => api.get(`/api/v1/groups/${groupId}`),
|
||||
{ schema: groupSchema, refetch },
|
||||
{
|
||||
schema: groupSchema,
|
||||
refetch,
|
||||
enabled: !!groupId,
|
||||
},
|
||||
);
|
||||
const { entity: relationship } = useGroupRelationship(groupId);
|
||||
const { groupRelationship: relationship } = useGroupRelationship(groupId);
|
||||
|
||||
return {
|
||||
...result,
|
||||
|
|
|
@ -15,7 +15,7 @@ function useGroupLookup(slug: string) {
|
|||
{ schema: groupSchema, enabled: !!slug },
|
||||
);
|
||||
|
||||
const { entity: relationship } = useGroupRelationship(group?.id);
|
||||
const { groupRelationship: relationship } = useGroupRelationship(group?.id);
|
||||
|
||||
return {
|
||||
...result,
|
||||
|
|
|
@ -12,7 +12,7 @@ function useGroupMembershipRequests(groupId: string) {
|
|||
const api = useApi();
|
||||
const path: ExpandedEntitiesPath = [Entities.ACCOUNTS, 'membership_requests', groupId];
|
||||
|
||||
const { entity: relationship } = useGroupRelationship(groupId);
|
||||
const { groupRelationship: relationship } = useGroupRelationship(groupId);
|
||||
|
||||
const { entities, invalidate, fetchEntities, ...rest } = useEntities(
|
||||
path,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useFeatures } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { groupSchema } from 'soapbox/schemas';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
function useGroupMutes() {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities, ...result } = useEntities<Group>(
|
||||
[Entities.GROUP_MUTES],
|
||||
() => api.get('/api/v1/groups/mutes'),
|
||||
{ schema: groupSchema, enabled: features.groupsMuting },
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
mutes: entities,
|
||||
};
|
||||
}
|
||||
|
||||
export { useGroupMutes };
|
|
@ -1,33 +1,24 @@
|
|||
import { useEffect } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useGroupRelationship(groupId: string | undefined) {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { entity: groupRelationship, ...result } = useEntity<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, groupId as string],
|
||||
[Entities.GROUP_RELATIONSHIPS, groupId!],
|
||||
() => api.get(`/api/v1/groups/relationships?id[]=${groupId}`),
|
||||
{
|
||||
enabled: !!groupId,
|
||||
schema: z.array(groupRelationshipSchema).transform(arr => arr[0]),
|
||||
schema: z.array(groupRelationshipSchema).nonempty().transform(arr => arr[0]),
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupRelationship?.id) {
|
||||
dispatch(fetchGroupRelationshipsSuccess([groupRelationship]));
|
||||
}
|
||||
}, [groupRelationship?.id]);
|
||||
|
||||
return {
|
||||
entity: groupRelationship,
|
||||
groupRelationship,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities';
|
||||
import { useApi, useLoggedIn } from 'soapbox/hooks';
|
||||
import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useGroupRelationships(groupIds: string[]) {
|
||||
function useGroupRelationships(listKey: string[], ids: string[]) {
|
||||
const api = useApi();
|
||||
const q = groupIds.map(id => `id[]=${id}`).join('&');
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
|
||||
const { entities, ...result } = useEntities<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, ...groupIds],
|
||||
() => api.get(`/api/v1/groups/relationships?${q}`),
|
||||
{ schema: groupRelationshipSchema, enabled: groupIds.length > 0 },
|
||||
function fetchGroupRelationships(ids: string[]) {
|
||||
const q = ids.map((id) => `id[]=${id}`).join('&');
|
||||
return api.get(`/api/v1/groups/relationships?${q}`);
|
||||
}
|
||||
|
||||
const { entityMap: relationships, ...result } = useBatchedEntities<GroupRelationship>(
|
||||
[Entities.RELATIONSHIPS, ...listKey],
|
||||
ids,
|
||||
fetchGroupRelationships,
|
||||
{ schema: groupRelationshipSchema, enabled: isLoggedIn },
|
||||
);
|
||||
|
||||
const relationships = entities.reduce<Record<string, GroupRelationship>>((map, relationship) => {
|
||||
map[relationship.id] = relationship;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...result,
|
||||
relationships,
|
||||
};
|
||||
return { relationships, ...result };
|
||||
}
|
||||
|
||||
export { useGroupRelationships };
|
|
@ -21,7 +21,10 @@ function useGroupSearch(search: string) {
|
|||
{ enabled: features.groupsDiscovery && !!search, schema: groupSchema },
|
||||
);
|
||||
|
||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||
const { relationships } = useGroupRelationships(
|
||||
['discover', 'search', search],
|
||||
entities.map(entity => entity.id),
|
||||
);
|
||||
|
||||
const groups = entities.map((group) => ({
|
||||
...group,
|
||||
|
|
|
@ -15,7 +15,10 @@ function useGroups(q: string = '') {
|
|||
() => api.get('/api/v1/groups', { params: { q } }),
|
||||
{ enabled: features.groups, schema: groupSchema },
|
||||
);
|
||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||
const { relationships } = useGroupRelationships(
|
||||
['search', q],
|
||||
entities.map(entity => entity.id),
|
||||
);
|
||||
|
||||
const groups = entities.map((group) => ({
|
||||
...group,
|
||||
|
|
|
@ -19,7 +19,10 @@ function useGroupsFromTag(tagId: string) {
|
|||
enabled: features.groupsDiscovery,
|
||||
},
|
||||
);
|
||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||
const { relationships } = useGroupRelationships(
|
||||
['tags', tagId],
|
||||
entities.map(entity => entity.id),
|
||||
);
|
||||
|
||||
const groups = entities.map((group) => ({
|
||||
...group,
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||
import { type Group, groupRelationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useMuteGroup(group?: Group) {
|
||||
const { createEntity, isSubmitting } = useEntityActions(
|
||||
[Entities.GROUP_RELATIONSHIPS, group?.id as string],
|
||||
{ post: `/api/v1/groups/${group?.id}/mute` },
|
||||
{ schema: groupRelationshipSchema },
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: createEntity,
|
||||
isSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
export { useMuteGroup };
|
|
@ -0,0 +1,30 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { Group, groupSchema } from 'soapbox/schemas';
|
||||
|
||||
function usePendingGroups() {
|
||||
const api = useApi();
|
||||
const { account } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities, ...result } = useEntities<Group>(
|
||||
[Entities.GROUPS, account?.id!, 'pending'],
|
||||
() => api.get('/api/v1/groups', {
|
||||
params: {
|
||||
pending: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
schema: groupSchema,
|
||||
enabled: !!account && features.groupsPending,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
groups: entities,
|
||||
};
|
||||
}
|
||||
|
||||
export { usePendingGroups };
|
|
@ -20,7 +20,7 @@ function usePopularGroups() {
|
|||
},
|
||||
);
|
||||
|
||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||
const { relationships } = useGroupRelationships(['popular'], entities.map(entity => entity.id));
|
||||
|
||||
const groups = entities.map((group) => ({
|
||||
...group,
|
||||
|
|
|
@ -8,7 +8,7 @@ import type { Group, GroupMember } from 'soapbox/schemas';
|
|||
|
||||
function usePromoteGroupMember(group: Group, groupMember: GroupMember) {
|
||||
const { createEntity } = useEntityActions<GroupMember>(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.account.id],
|
||||
{ post: `/api/v1/groups/${group.id}/promote` },
|
||||
{ schema: z.array(groupMemberSchema).transform((arr) => arr[0]) },
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ function useSuggestedGroups() {
|
|||
},
|
||||
);
|
||||
|
||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||
const { relationships } = useGroupRelationships(['suggested'], entities.map(entity => entity.id));
|
||||
|
||||
const groups = entities.map((group) => ({
|
||||
...group,
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||
import { type Group, groupRelationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useUnmuteGroup(group?: Group) {
|
||||
const { createEntity, isSubmitting } = useEntityActions(
|
||||
[Entities.GROUP_RELATIONSHIPS, group?.id as string],
|
||||
{ post: `/api/v1/groups/${group?.id}/unmute` },
|
||||
{ schema: groupRelationshipSchema },
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: createEntity,
|
||||
isSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
export { useUnmuteGroup };
|
|
@ -1,12 +1,18 @@
|
|||
|
||||
/**
|
||||
* Accounts
|
||||
*/
|
||||
// Accounts
|
||||
export { useAccount } from './accounts/useAccount';
|
||||
export { useAccountLookup } from './accounts/useAccountLookup';
|
||||
export {
|
||||
useBlocks,
|
||||
useMutes,
|
||||
useFollowers,
|
||||
useFollowing,
|
||||
} from './accounts/useAccountList';
|
||||
export { useFollow } from './accounts/useFollow';
|
||||
export { useRelationships } from './accounts/useRelationships';
|
||||
export { usePatronUser } from './accounts/usePatronUser';
|
||||
|
||||
/**
|
||||
* Groups
|
||||
*/
|
||||
// Groups
|
||||
export { useBlockGroupMember } from './groups/useBlockGroupMember';
|
||||
export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest';
|
||||
export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup';
|
||||
|
@ -17,6 +23,7 @@ export { useGroupLookup } from './groups/useGroupLookup';
|
|||
export { useGroupMedia } from './groups/useGroupMedia';
|
||||
export { useGroupMembers } from './groups/useGroupMembers';
|
||||
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
||||
export { useGroupMutes } from './groups/useGroupMutes';
|
||||
export { useGroupRelationship } from './groups/useGroupRelationship';
|
||||
export { useGroupRelationships } from './groups/useGroupRelationships';
|
||||
export { useGroupSearch } from './groups/useGroupSearch';
|
||||
|
@ -26,15 +33,13 @@ export { useGroupValidation } from './groups/useGroupValidation';
|
|||
export { useGroups } from './groups/useGroups';
|
||||
export { useGroupsFromTag } from './groups/useGroupsFromTag';
|
||||
export { useJoinGroup } from './groups/useJoinGroup';
|
||||
export { useMuteGroup } from './groups/useMuteGroup';
|
||||
export { useLeaveGroup } from './groups/useLeaveGroup';
|
||||
export { usePendingGroups } from './groups/usePendingGroups';
|
||||
export { usePopularGroups } from './groups/usePopularGroups';
|
||||
export { usePopularTags } from './groups/usePopularTags';
|
||||
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
||||
export { useSuggestedGroups } from './groups/useSuggestedGroups';
|
||||
export { useUnmuteGroup } from './groups/useUnmuteGroup';
|
||||
export { useUpdateGroup } from './groups/useUpdateGroup';
|
||||
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
export { useRelationships } from './accounts/useRelationships';
|
|
@ -1,25 +1,23 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import { normalizeAccount } from '../../normalizers';
|
||||
import Account from '../account';
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import Account from '../account';
|
||||
|
||||
describe('<Account />', () => {
|
||||
it('renders account name and username', () => {
|
||||
const account = normalizeAccount({
|
||||
const account = buildAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
}) as ReducerAccount;
|
||||
});
|
||||
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
accounts: {
|
||||
'1': account,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
render(<Account account={account} />, undefined, store);
|
||||
|
@ -29,18 +27,18 @@ describe('<Account />', () => {
|
|||
|
||||
describe('verification badge', () => {
|
||||
it('renders verification badge', () => {
|
||||
const account = normalizeAccount({
|
||||
const account = buildAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
verified: true,
|
||||
}) as ReducerAccount;
|
||||
});
|
||||
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
accounts: {
|
||||
'1': account,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
render(<Account account={account} />, undefined, store);
|
||||
|
@ -48,18 +46,18 @@ describe('<Account />', () => {
|
|||
});
|
||||
|
||||
it('does not render verification badge', () => {
|
||||
const account = normalizeAccount({
|
||||
const account = buildAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
verified: false,
|
||||
}) as ReducerAccount;
|
||||
});
|
||||
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
accounts: {
|
||||
'1': account,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
render(<Account account={account} />, undefined, store);
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import DisplayName from '../display-name';
|
||||
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
|
||||
describe('<DisplayName />', () => {
|
||||
it('renders display name + account name', () => {
|
||||
const account = normalizeAccount({ acct: 'bar@baz' }) as ReducerAccount;
|
||||
const account = buildAccount({ acct: 'bar@baz' });
|
||||
render(<DisplayName account={account} />);
|
||||
|
||||
expect(screen.getByTestId('display-name')).toHaveTextContent('bar@baz');
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen, rootState } from '../../jest/test-helpers';
|
||||
import { normalizeStatus, normalizeAccount } from '../../normalizers';
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
import { render, screen, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeStatus } from 'soapbox/normalizers';
|
||||
|
||||
import Status from '../status';
|
||||
|
||||
import type { ReducerStatus } from 'soapbox/reducers/statuses';
|
||||
|
||||
const account = normalizeAccount({
|
||||
const account = buildAccount({
|
||||
id: '1',
|
||||
acct: 'alex',
|
||||
});
|
||||
|
@ -34,7 +36,7 @@ describe('<Status />', () => {
|
|||
});
|
||||
|
||||
it('is not rendered if status is under review', () => {
|
||||
const inReviewStatus = normalizeStatus({ ...status, visibility: 'self' });
|
||||
const inReviewStatus = status.set('visibility', 'self');
|
||||
render(<Status status={inReviewStatus as ReducerStatus} />, undefined, state);
|
||||
expect(screen.queryAllByTestId('status-action-bar')).toHaveLength(0);
|
||||
});
|
||||
|
|
|
@ -15,10 +15,9 @@ import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
|||
|
||||
import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
|
||||
import type { Account as AccountSchema } from 'soapbox/schemas';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IInstanceFavicon {
|
||||
account: AccountEntity | AccountSchema
|
||||
account: AccountSchema
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
|
@ -42,13 +41,17 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
|||
}
|
||||
};
|
||||
|
||||
if (!account.pleroma?.favicon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className='h-4 w-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<img src={account.favicon} alt='' title={account.domain} className='max-h-full w-full' />
|
||||
<img src={account.pleroma.favicon} alt='' title={account.domain} className='max-h-full w-full' />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -68,7 +71,7 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
|
|||
};
|
||||
|
||||
export interface IAccount {
|
||||
account: AccountEntity | AccountSchema
|
||||
account: AccountSchema
|
||||
action?: React.ReactElement
|
||||
actionAlignment?: 'center' | 'top'
|
||||
actionIcon?: string
|
||||
|
@ -230,7 +233,7 @@ const Account = ({
|
|||
<HStack alignItems='center' space={1}>
|
||||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
||||
|
||||
{account.favicon && (
|
||||
{account.pleroma?.favicon && (
|
||||
<InstanceFavicon account={account} disabled={!withLinkToProfile} />
|
||||
)}
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ import { getAcct } from '../utils/accounts';
|
|||
import { HStack, Text } from './ui';
|
||||
import VerificationBadge from './verification-badge';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
|
||||
interface IDisplayName {
|
||||
account: Account
|
||||
account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>
|
||||
withSuffix?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = t
|
|||
|
||||
return (
|
||||
<span className='display-name' data-testid='display-name'>
|
||||
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||
<HoverRefWrapper accountId={account.id} inline>
|
||||
{displayName}
|
||||
</HoverRefWrapper>
|
||||
{withSuffix && suffix}
|
||||
|
|
|
@ -9,32 +9,33 @@ import {
|
|||
closeProfileHoverCard,
|
||||
updateProfileHoverCard,
|
||||
} from 'soapbox/actions/profile-hover-card';
|
||||
import { useAccount, usePatronUser } from 'soapbox/api/hooks';
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
||||
import { UserPanel } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
|
||||
import { showProfileHoverCard } from './hover-ref-wrapper';
|
||||
import { Card, CardBody, HStack, Icon, Stack, Text } from './ui';
|
||||
|
||||
import type { Account, PatronUser } from 'soapbox/schemas';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const getBadges = (account: Account): JSX.Element[] => {
|
||||
const getBadges = (
|
||||
account?: Pick<Account, 'admin' | 'moderator'>,
|
||||
patronUser?: Pick<PatronUser, 'is_patron'>,
|
||||
): JSX.Element[] => {
|
||||
const badges = [];
|
||||
|
||||
if (account.admin) {
|
||||
if (account?.admin) {
|
||||
badges.push(<Badge key='admin' slug='admin' title='Admin' />);
|
||||
} else if (account.moderator) {
|
||||
} else if (account?.moderator) {
|
||||
badges.push(<Badge key='moderator' slug='moderator' title='Moderator' />);
|
||||
}
|
||||
|
||||
if (account.getIn(['patron', 'is_patron'])) {
|
||||
if (patronUser?.is_patron) {
|
||||
badges.push(<Badge key='patron' slug='patron' title='Patron' />);
|
||||
}
|
||||
|
||||
|
@ -67,9 +68,10 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined);
|
||||
const account = useAppSelector(state => accountId && getAccount(state, accountId));
|
||||
const { account } = useAccount(accountId, { withRelationship: true });
|
||||
const { patronUser } = usePatronUser(account?.url);
|
||||
const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current);
|
||||
const badges = account ? getBadges(account) : [];
|
||||
const badges = getBadges(account, patronUser);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) dispatch(fetchRelationships([accountId]));
|
||||
|
@ -112,7 +114,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
<BundleContainer fetchComponent={UserPanel}>
|
||||
{Component => (
|
||||
<Component
|
||||
accountId={account.get('id')}
|
||||
accountId={account.id}
|
||||
action={<ActionButton account={account} small />}
|
||||
badges={badges}
|
||||
/>
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
|
||||
import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { closeSidebar } from 'soapbox/actions/sidebar';
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import Account from 'soapbox/components/account';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
|
||||
import { useAppDispatch, useAppSelector, useGroupsPath, useFeatures } from 'soapbox/hooks';
|
||||
import { makeGetAccount, makeGetOtherAccounts } from 'soapbox/selectors';
|
||||
import { makeGetOtherAccounts } from 'soapbox/selectors';
|
||||
|
||||
import { Divider, HStack, Icon, IconButton, Text } from './ui';
|
||||
|
||||
|
@ -27,6 +28,7 @@ const messages = defineMessages({
|
|||
domainBlocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||
followedTags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
soapboxConfig: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' },
|
||||
accountMigration: { id: 'navigation_bar.account_migration', defaultMessage: 'Move account' },
|
||||
accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
|
||||
|
@ -76,16 +78,14 @@ const SidebarLink: React.FC<ISidebarLink> = ({ href, to, icon, text, onClick })
|
|||
);
|
||||
};
|
||||
|
||||
const getOtherAccounts = makeGetOtherAccounts();
|
||||
|
||||
const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getOtherAccounts = useCallback(makeGetOtherAccounts(), []);
|
||||
const features = useFeatures();
|
||||
const getAccount = makeGetAccount();
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const account = useAppSelector((state) => me ? getAccount(state, me) : null);
|
||||
const { account } = useAccount(me || undefined);
|
||||
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state));
|
||||
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
|
||||
const settings = useAppSelector((state) => getSettings(state));
|
||||
|
@ -306,6 +306,15 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{features.followedHashtagsList && (
|
||||
<SidebarLink
|
||||
to='/followed_tags'
|
||||
icon={require('@tabler/icons/hash.svg')}
|
||||
text={intl.formatMessage(messages.followedTags)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{account.admin && (
|
||||
<SidebarLink
|
||||
to='/soapbox/config'
|
||||
|
|
|
@ -24,7 +24,7 @@ const SidebarNavigation = () => {
|
|||
|
||||
const features = useFeatures();
|
||||
const settings = useSettings();
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const groupsPath = useGroupsPath();
|
||||
|
||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { blockAccount } from 'soapbox/actions/accounts';
|
||||
import { launchChat } from 'soapbox/actions/chats';
|
||||
|
@ -14,6 +14,7 @@ import { initMuteModal } from 'soapbox/actions/mutes';
|
|||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||
import { deleteFromTimelines } from 'soapbox/actions/timelines';
|
||||
import { useBlockGroupMember, useGroup, useGroupRelationship, useMuteGroup, useUnmuteGroup } from 'soapbox/api/hooks';
|
||||
import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||
|
@ -35,6 +36,7 @@ const messages = defineMessages({
|
|||
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
|
||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
|
@ -56,6 +58,9 @@ const messages = defineMessages({
|
|||
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
||||
external: { id: 'status.external', defaultMessage: 'View post on {domain}' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Like' },
|
||||
groupBlockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban' },
|
||||
groupBlockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Ban From Group' },
|
||||
groupBlockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to ban @{name} from the group?' },
|
||||
groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' },
|
||||
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
|
||||
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove post from group' },
|
||||
|
@ -64,12 +69,16 @@ const messages = defineMessages({
|
|||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
muteConfirm: { id: 'confirmations.mute_group.confirm', defaultMessage: 'Mute' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
muteGroup: { id: 'group.mute.long_label', defaultMessage: 'Mute Group' },
|
||||
muteHeading: { id: 'confirmations.mute_group.heading', defaultMessage: 'Mute Group' },
|
||||
muteMessage: { id: 'confirmations.mute_group.message', defaultMessage: 'You are about to mute the group. Do you want to continue?' },
|
||||
muteSuccess: { id: 'group.mute.success', defaultMessage: 'Muted the group' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this post' },
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
pinToGroup: { id: 'status.pin_to_group', defaultMessage: 'Pin to Group' },
|
||||
pinToGroupSuccess: { id: 'status.pin_to_group.success', defaultMessage: 'Pinned to Group!' },
|
||||
unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' },
|
||||
quotePost: { id: 'status.quote', defaultMessage: 'Quote post' },
|
||||
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
|
||||
reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' },
|
||||
|
@ -92,7 +101,10 @@ const messages = defineMessages({
|
|||
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
unmuteGroup: { id: 'group.unmute.long_label', defaultMessage: 'Unmute Group' },
|
||||
unmuteSuccess: { id: 'group.unmute.success', defaultMessage: 'Unmuted the group' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' },
|
||||
});
|
||||
|
||||
interface IStatusActionBar {
|
||||
|
@ -113,17 +125,24 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug');
|
||||
|
||||
const { group } = useGroup((status.group as Group)?.id as string);
|
||||
const muteGroup = useMuteGroup(group as Group);
|
||||
const unmuteGroup = useUnmuteGroup(group as Group);
|
||||
const isMutingGroup = !!group?.relationship?.muting;
|
||||
const deleteGroupStatus = useDeleteGroupStatus(group as Group, status.id);
|
||||
const blockGroupMember = useBlockGroupMember(group as Group, status?.account as any);
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const groupRelationship = useAppSelector(state => status.group ? state.group_relationships.get((status.group as Group).id) : null);
|
||||
const { groupRelationship } = useGroupRelationship(status.group?.id);
|
||||
const features = useFeatures();
|
||||
const settings = useSettings();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const deleteGroupStatus = useDeleteGroupStatus(status?.group as Group, status.id);
|
||||
|
||||
const { allowedEmoji } = soapboxConfig;
|
||||
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const isStaff = account ? account.staff : false;
|
||||
const isAdmin = account ? account.admin : false;
|
||||
|
||||
|
@ -264,8 +283,29 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
dispatch(initMuteModal(status.account as Account));
|
||||
};
|
||||
|
||||
const handleMuteGroupClick: React.EventHandler<React.MouseEvent> = () =>
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.muteHeading),
|
||||
message: intl.formatMessage(messages.muteMessage),
|
||||
confirm: intl.formatMessage(messages.muteConfirm),
|
||||
confirmationTheme: 'primary',
|
||||
onConfirm: () => muteGroup.mutate(undefined, {
|
||||
onSuccess() {
|
||||
toast.success(intl.formatMessage(messages.muteSuccess));
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const handleUnmuteGroupClick: React.EventHandler<React.MouseEvent> = () => {
|
||||
unmuteGroup.mutate(undefined, {
|
||||
onSuccess() {
|
||||
toast.success(intl.formatMessage(messages.unmuteSuccess));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlockClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
const account = status.get('account') as Account;
|
||||
const account = status.account as Account;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/ban.svg'),
|
||||
|
@ -282,12 +322,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handleOpen: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}`);
|
||||
history.push(`/@${status.account.acct}/posts/${status.id}`);
|
||||
};
|
||||
|
||||
const handleEmbed = () => {
|
||||
dispatch(openModal('EMBED', {
|
||||
url: status.get('url'),
|
||||
url: status.url,
|
||||
onError: (error: any) => toast.showAlertForError(error),
|
||||
}));
|
||||
};
|
||||
|
@ -336,11 +376,26 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}));
|
||||
};
|
||||
|
||||
const handleBlockFromGroup = () => {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.groupBlockFromGroupHeading),
|
||||
message: intl.formatMessage(messages.groupBlockFromGroupMessage, { name: (status.account as any).username }),
|
||||
confirm: intl.formatMessage(messages.groupBlockConfirm),
|
||||
onConfirm: () => {
|
||||
blockGroupMember({ account_ids: [(status.account as any).id] }, {
|
||||
onSuccess() {
|
||||
toast.success(intl.formatMessage(messages.blocked, { name: account?.acct }));
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const _makeMenu = (publicStatus: boolean) => {
|
||||
const mutingConversation = status.muted;
|
||||
const ownAccount = status.getIn(['account', 'id']) === me;
|
||||
const username = String(status.getIn(['account', 'username']));
|
||||
const account = status.account as Account;
|
||||
const ownAccount = status.account.id === me;
|
||||
const username = status.account.username;
|
||||
const account = status.account;
|
||||
const domain = account.fqn.split('@')[1];
|
||||
|
||||
const menu: Menu = [];
|
||||
|
@ -456,7 +511,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
icon: require('@tabler/icons/at.svg'),
|
||||
});
|
||||
|
||||
if (status.getIn(['account', 'pleroma', 'accepts_chat_messages']) === true) {
|
||||
if (status.account.pleroma?.accepts_chat_messages === true) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.chat, { name: username }),
|
||||
action: handleChatClick,
|
||||
|
@ -471,6 +526,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}
|
||||
|
||||
menu.push(null);
|
||||
if (features.groupsMuting && status.group) {
|
||||
menu.push({
|
||||
text: isMutingGroup ? intl.formatMessage(messages.unmuteGroup) : intl.formatMessage(messages.muteGroup),
|
||||
icon: require('@tabler/icons/volume-3.svg'),
|
||||
action: isMutingGroup ? handleUnmuteGroupClick : handleMuteGroupClick,
|
||||
});
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mute, { name: username }),
|
||||
action: handleMuteClick,
|
||||
|
@ -494,10 +558,24 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER;
|
||||
const isGroupAdmin = groupRelationship?.role === GroupRoles.ADMIN;
|
||||
const isStatusFromOwner = group.owner.id === account.id;
|
||||
|
||||
const canBanUser = match?.isExact && (isGroupOwner || isGroupAdmin) && !isStatusFromOwner && !ownAccount;
|
||||
const canDeleteStatus = !ownAccount && (isGroupOwner || (isGroupAdmin && !isStatusFromOwner));
|
||||
|
||||
if (canDeleteStatus) {
|
||||
if (canBanUser || canDeleteStatus) {
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (canBanUser) {
|
||||
menu.push({
|
||||
text: 'Ban from Group',
|
||||
action: handleBlockFromGroup,
|
||||
icon: require('@tabler/icons/ban.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (canDeleteStatus) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.groupModDelete),
|
||||
action: handleDeleteFromGroup,
|
||||
|
|
|
@ -178,8 +178,15 @@ const StatusList: React.FC<IStatusList> = ({
|
|||
));
|
||||
};
|
||||
|
||||
const renderFeedSuggestions = (): React.ReactNode => {
|
||||
return <FeedSuggestions key='suggestions' />;
|
||||
const renderFeedSuggestions = (statusId: string): React.ReactNode => {
|
||||
return (
|
||||
<FeedSuggestions
|
||||
key='suggestions'
|
||||
statusId={statusId}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatuses = (): React.ReactNode[] => {
|
||||
|
@ -201,7 +208,7 @@ const StatusList: React.FC<IStatusList> = ({
|
|||
}
|
||||
} else if (statusId.startsWith('末suggestions-')) {
|
||||
if (soapboxConfig.feedInjection) {
|
||||
acc.push(renderFeedSuggestions());
|
||||
acc.push(renderFeedSuggestions(statusId));
|
||||
}
|
||||
} else if (statusId.startsWith('末pending-')) {
|
||||
acc.push(renderPendingStatus(statusId));
|
||||
|
|
|
@ -15,7 +15,7 @@ interface IStatusReactionWrapper {
|
|||
/** Provides emoji reaction functionality to the underlying button component */
|
||||
const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ownAccount = useOwnAccount();
|
||||
const { account: ownAccount } = useOwnAccount();
|
||||
const status = useAppSelector(state => state.statuses.get(statusId));
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import { Card, Icon, Stack, Text } from './ui';
|
|||
|
||||
import type {
|
||||
Account as AccountEntity,
|
||||
Group as GroupEntity,
|
||||
Status as StatusEntity,
|
||||
} from 'soapbox/types/entities';
|
||||
|
||||
|
@ -90,8 +89,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
const actualStatus = getActualStatus(status);
|
||||
const isReblog = status.reblog && typeof status.reblog === 'object';
|
||||
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
||||
const group = actualStatus.group as GroupEntity | null;
|
||||
const statusUrl = `/@${actualStatus.account.acct}/posts/${actualStatus.id}`;
|
||||
const group = actualStatus.group;
|
||||
|
||||
const filtered = (status.filtered.size || actualStatus.filtered.size) > 0;
|
||||
|
||||
|
@ -177,7 +176,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
};
|
||||
|
||||
const handleHotkeyOpenProfile = (): void => {
|
||||
history.push(`/@${actualStatus.getIn(['account', 'acct'])}`);
|
||||
history.push(`/@${actualStatus.account.acct}`);
|
||||
};
|
||||
|
||||
const handleHotkeyMoveUp = (e?: KeyboardEvent): void => {
|
||||
|
@ -224,25 +223,25 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
values={{
|
||||
name: (
|
||||
<Link
|
||||
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||
to={`/@${status.account.acct}`}
|
||||
className='hover:underline'
|
||||
>
|
||||
<bdi className='truncate'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: String(status.getIn(['account', 'display_name_html'])),
|
||||
__html: status.account.display_name_html,
|
||||
}}
|
||||
/>
|
||||
</bdi>
|
||||
</Link>
|
||||
),
|
||||
group: (
|
||||
<Link to={`/group/${(status.group as GroupEntity).slug}`} className='hover:underline'>
|
||||
<Link to={`/group/${group.slug}`} className='hover:underline'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: (status.group as GroupEntity).display_name_html,
|
||||
__html: group.display_name_html,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
|
@ -263,12 +262,12 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
defaultMessage='{name} reposted'
|
||||
values={{
|
||||
name: (
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}`} className='hover:underline'>
|
||||
<Link to={`/@${status.account.acct}`} className='hover:underline'>
|
||||
<bdi className='truncate'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: String(status.getIn(['account', 'display_name_html'])),
|
||||
__html: status.account.display_name_html,
|
||||
}}
|
||||
/>
|
||||
</bdi>
|
||||
|
@ -322,7 +321,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
return (
|
||||
<div ref={node}>
|
||||
<>
|
||||
{actualStatus.getIn(['account', 'display_name']) || actualStatus.getIn(['account', 'username'])}
|
||||
{actualStatus.account.display_name || actualStatus.account.username}
|
||||
{actualStatus.content}
|
||||
</>
|
||||
</div>
|
||||
|
@ -354,7 +353,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
if (status.reblog && typeof status.reblog === 'object') {
|
||||
rebloggedByText = intl.formatMessage(
|
||||
messages.reblogged_by,
|
||||
{ name: String(status.getIn(['account', 'acct'])) },
|
||||
{ name: status.account.acct },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -425,8 +424,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
{renderStatusInfo()}
|
||||
|
||||
<AccountContainer
|
||||
key={String(actualStatus.getIn(['account', 'id']))}
|
||||
id={String(actualStatus.getIn(['account', 'id']))}
|
||||
key={actualStatus.account.id}
|
||||
id={actualStatus.account.id}
|
||||
timestamp={actualStatus.created_at}
|
||||
timestampUrl={statusUrl}
|
||||
action={accountAction}
|
||||
|
|
|
@ -35,7 +35,7 @@ interface ISensitiveContentOverlay {
|
|||
const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveContentOverlay>((props, ref) => {
|
||||
const { onToggleVisibility, status } = props;
|
||||
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useStatContext } from 'soapbox/contexts/stat-context';
|
|||
import { useAppSelector, useFeatures, useGroupsPath, useOwnAccount } from 'soapbox/hooks';
|
||||
|
||||
const ThumbNavigation: React.FC = (): JSX.Element => {
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const groupsPath = useGroupsPath();
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ interface ICard {
|
|||
className?: string
|
||||
/** Elements inside the card. */
|
||||
children: React.ReactNode
|
||||
tabIndex?: number
|
||||
}
|
||||
|
||||
/** An opaque backdrop to hold a collection of related elements. */
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Account, { IAccount } from '../components/account';
|
||||
import { makeGetAccount } from '../selectors';
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import Account, { IAccount } from 'soapbox/components/account';
|
||||
|
||||
interface IAccountContainer extends Omit<IAccount, 'account'> {
|
||||
id: string
|
||||
withRelationship?: boolean
|
||||
}
|
||||
|
||||
const AccountContainer: React.FC<IAccountContainer> = ({ id, ...props }) => {
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
const account = useAppSelector(state => getAccount(state, id));
|
||||
const AccountContainer: React.FC<IAccountContainer> = ({ id, withRelationship, ...props }) => {
|
||||
const { account } = useAccount(id, { withRelationship });
|
||||
|
||||
return (
|
||||
<Account account={account!} {...props} />
|
||||
|
|
|
@ -90,12 +90,12 @@ const SoapboxMount = () => {
|
|||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const instance = useInstance();
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const features = useFeatures();
|
||||
const { pepeEnabled } = useRegistrationStatus();
|
||||
|
||||
const waitlisted = account && !account.source.get('approved', true);
|
||||
const waitlisted = account && account.source?.approved === false;
|
||||
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
||||
const showOnboarding = account && !waitlisted && needsOnboarding;
|
||||
const { redirectRootNoLogin } = soapboxConfig;
|
||||
|
@ -216,7 +216,7 @@ const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const swUpdating = useAppSelector(state => state.meta.swUpdating);
|
||||
const { locale } = useLocale();
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ const ChatProvider: React.FC<IChatProvider> = ({ children }) => {
|
|||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useSettings();
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const path = history.location.pathname;
|
||||
const isUsingMainChatPage = Boolean(path.match(/^\/chats/));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Entity, EntityListState, ImportPosition } from './types';
|
||||
import type { EntitiesTransaction, Entity, EntityListState, ImportPosition } from './types';
|
||||
|
||||
const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const;
|
||||
const ENTITIES_DELETE = 'ENTITIES_DELETE' as const;
|
||||
|
@ -8,6 +8,7 @@ const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const;
|
|||
const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const;
|
||||
const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
|
||||
const ENTITIES_INVALIDATE_LIST = 'ENTITIES_INVALIDATE_LIST' as const;
|
||||
const ENTITIES_TRANSACTION = 'ENTITIES_TRANSACTION' as const;
|
||||
|
||||
/** Action to import entities into the cache. */
|
||||
function importEntities(entities: Entity[], entityType: string, listKey?: string, pos?: ImportPosition) {
|
||||
|
@ -95,6 +96,13 @@ function invalidateEntityList(entityType: string, listKey: string) {
|
|||
};
|
||||
}
|
||||
|
||||
function entitiesTransaction(transaction: EntitiesTransaction) {
|
||||
return {
|
||||
type: ENTITIES_TRANSACTION,
|
||||
transaction,
|
||||
};
|
||||
}
|
||||
|
||||
/** Any action pertaining to entities. */
|
||||
type EntityAction =
|
||||
ReturnType<typeof importEntities>
|
||||
|
@ -104,7 +112,8 @@ type EntityAction =
|
|||
| ReturnType<typeof entitiesFetchRequest>
|
||||
| ReturnType<typeof entitiesFetchSuccess>
|
||||
| ReturnType<typeof entitiesFetchFail>
|
||||
| ReturnType<typeof invalidateEntityList>;
|
||||
| ReturnType<typeof invalidateEntityList>
|
||||
| ReturnType<typeof entitiesTransaction>;
|
||||
|
||||
export {
|
||||
ENTITIES_IMPORT,
|
||||
|
@ -115,6 +124,7 @@ export {
|
|||
ENTITIES_FETCH_SUCCESS,
|
||||
ENTITIES_FETCH_FAIL,
|
||||
ENTITIES_INVALIDATE_LIST,
|
||||
ENTITIES_TRANSACTION,
|
||||
importEntities,
|
||||
deleteEntities,
|
||||
dismissEntities,
|
||||
|
@ -123,7 +133,7 @@ export {
|
|||
entitiesFetchSuccess,
|
||||
entitiesFetchFail,
|
||||
invalidateEntityList,
|
||||
EntityAction,
|
||||
entitiesTransaction,
|
||||
};
|
||||
|
||||
export type { DeleteEntitiesOpts };
|
||||
export type { DeleteEntitiesOpts, EntityAction };
|
|
@ -1,9 +1,26 @@
|
|||
export enum Entities {
|
||||
import type * as Schemas from 'soapbox/schemas';
|
||||
|
||||
enum Entities {
|
||||
ACCOUNTS = 'Accounts',
|
||||
GROUPS = 'Groups',
|
||||
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
||||
GROUP_MUTES = 'GroupMutes',
|
||||
GROUP_RELATIONSHIPS = 'GroupRelationships',
|
||||
GROUP_TAGS = 'GroupTags',
|
||||
PATRON_USERS = 'PatronUsers',
|
||||
RELATIONSHIPS = 'Relationships',
|
||||
STATUSES = 'Statuses'
|
||||
}
|
||||
|
||||
interface EntityTypes {
|
||||
[Entities.ACCOUNTS]: Schemas.Account
|
||||
[Entities.GROUPS]: Schemas.Group
|
||||
[Entities.GROUP_MEMBERSHIPS]: Schemas.GroupMember
|
||||
[Entities.GROUP_RELATIONSHIPS]: Schemas.GroupRelationship
|
||||
[Entities.GROUP_TAGS]: Schemas.GroupTag
|
||||
[Entities.PATRON_USERS]: Schemas.PatronUser
|
||||
[Entities.RELATIONSHIPS]: Schemas.Relationship
|
||||
[Entities.STATUSES]: Schemas.Status
|
||||
}
|
||||
|
||||
export { Entities, type EntityTypes };
|
|
@ -6,3 +6,5 @@ export { useCreateEntity } from './useCreateEntity';
|
|||
export { useDeleteEntity } from './useDeleteEntity';
|
||||
export { useDismissEntity } from './useDismissEntity';
|
||||
export { useIncrementEntity } from './useIncrementEntity';
|
||||
export { useChangeEntity } from './useChangeEntity';
|
||||
export { useTransaction } from './useTransaction';
|
|
@ -0,0 +1,103 @@
|
|||
import { useEffect } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
|
||||
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions';
|
||||
import { selectCache, selectListState, useListState } from '../selectors';
|
||||
|
||||
import { parseEntitiesPath } from './utils';
|
||||
|
||||
import type { Entity } from '../types';
|
||||
import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
interface UseBatchedEntitiesOpts<TEntity extends Entity> {
|
||||
schema?: EntitySchema<TEntity>
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
function useBatchedEntities<TEntity extends Entity>(
|
||||
expandedPath: ExpandedEntitiesPath,
|
||||
ids: string[],
|
||||
entityFn: EntityFn<string[]>,
|
||||
opts: UseBatchedEntitiesOpts<TEntity> = {},
|
||||
) {
|
||||
const getState = useGetState();
|
||||
const dispatch = useAppDispatch();
|
||||
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
|
||||
const schema = opts.schema || z.custom<TEntity>();
|
||||
|
||||
const isEnabled = opts.enabled ?? true;
|
||||
const isFetching = useListState(path, 'fetching');
|
||||
const lastFetchedAt = useListState(path, 'lastFetchedAt');
|
||||
const isFetched = useListState(path, 'fetched');
|
||||
const isInvalid = useListState(path, 'invalid');
|
||||
const error = useListState(path, 'error');
|
||||
|
||||
/** Get IDs of entities not yet in the store. */
|
||||
const filteredIds = useAppSelector((state) => {
|
||||
const cache = selectCache(state, path);
|
||||
if (!cache) return ids;
|
||||
return ids.filter((id) => !cache.store[id]);
|
||||
});
|
||||
|
||||
const entityMap = useAppSelector((state) => selectEntityMap<TEntity>(state, path, ids));
|
||||
|
||||
async function fetchEntities() {
|
||||
const isFetching = selectListState(getState(), path, 'fetching');
|
||||
if (isFetching) return;
|
||||
|
||||
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||
try {
|
||||
const response = await entityFn(filteredIds);
|
||||
const entities = filteredArray(schema).parse(response.data);
|
||||
dispatch(entitiesFetchSuccess(entities, entityType, listKey, 'end', {
|
||||
next: undefined,
|
||||
prev: undefined,
|
||||
totalCount: undefined,
|
||||
fetching: false,
|
||||
fetched: true,
|
||||
error: null,
|
||||
lastFetchedAt: new Date(),
|
||||
invalid: false,
|
||||
}));
|
||||
} catch (e) {
|
||||
dispatch(entitiesFetchFail(entityType, listKey, e));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredIds.length && isEnabled) {
|
||||
fetchEntities();
|
||||
}
|
||||
}, [filteredIds.length]);
|
||||
|
||||
return {
|
||||
entityMap,
|
||||
isFetching,
|
||||
lastFetchedAt,
|
||||
isFetched,
|
||||
isError: !!error,
|
||||
isInvalid,
|
||||
};
|
||||
}
|
||||
|
||||
function selectEntityMap<TEntity extends Entity>(
|
||||
state: RootState,
|
||||
path: EntitiesPath,
|
||||
entityIds: string[],
|
||||
): Record<string, TEntity> {
|
||||
const cache = selectCache(state, path);
|
||||
|
||||
return entityIds.reduce<Record<string, TEntity>>((result, id) => {
|
||||
const entity = cache?.store[id];
|
||||
if (entity) {
|
||||
result[id] = entity as TEntity;
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export { useBatchedEntities };
|
|
@ -0,0 +1,24 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { type Entity } from 'soapbox/entity-store/types';
|
||||
import { useAppDispatch, useGetState } from 'soapbox/hooks';
|
||||
|
||||
type ChangeEntityFn<TEntity extends Entity> = (entity: TEntity) => TEntity
|
||||
|
||||
function useChangeEntity<TEntity extends Entity = Entity>(entityType: Entities) {
|
||||
const getState = useGetState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function changeEntity(entityId: string, change: ChangeEntityFn<TEntity>): void {
|
||||
if (!entityId) return;
|
||||
const entity = getState().entities[entityType]?.store[entityId] as TEntity | undefined;
|
||||
if (entity) {
|
||||
const newEntity = change(entity);
|
||||
dispatch(importEntities([newEntity], entityType));
|
||||
}
|
||||
}
|
||||
|
||||
return { changeEntity };
|
||||
}
|
||||
|
||||
export { useChangeEntity, type ChangeEntityFn };
|
|
@ -7,12 +7,12 @@ import { filteredArray } from 'soapbox/schemas/utils';
|
|||
import { realNumberSchema } from 'soapbox/utils/numbers';
|
||||
|
||||
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions';
|
||||
import { selectEntities, selectListState, useListState } from '../selectors';
|
||||
|
||||
import { parseEntitiesPath } from './utils';
|
||||
|
||||
import type { Entity, EntityListState } from '../types';
|
||||
import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
import type { Entity } from '../types';
|
||||
import type { EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
||||
|
||||
/** Additional options for the hook. */
|
||||
interface UseEntitiesOpts<TEntity extends Entity> {
|
||||
|
@ -42,6 +42,7 @@ function useEntities<TEntity extends Entity>(
|
|||
|
||||
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
|
||||
const entities = useAppSelector(state => selectEntities<TEntity>(state, path));
|
||||
const schema = opts.schema || z.custom<TEntity>();
|
||||
|
||||
const isEnabled = opts.enabled ?? true;
|
||||
const isFetching = useListState(path, 'fetching');
|
||||
|
@ -62,7 +63,6 @@ function useEntities<TEntity extends Entity>(
|
|||
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||
try {
|
||||
const response = await req();
|
||||
const schema = opts.schema || z.custom<TEntity>();
|
||||
const entities = filteredArray(schema).parse(response.data);
|
||||
const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']);
|
||||
const totalCount = parsedCount.success ? parsedCount.data : undefined;
|
||||
|
@ -133,46 +133,6 @@ function useEntities<TEntity extends Entity>(
|
|||
};
|
||||
}
|
||||
|
||||
/** Get cache at path from Redux. */
|
||||
const selectCache = (state: RootState, path: EntitiesPath) => state.entities[path[0]];
|
||||
|
||||
/** Get list at path from Redux. */
|
||||
const selectList = (state: RootState, path: EntitiesPath) => {
|
||||
const [, ...listKeys] = path;
|
||||
const listKey = listKeys.join(':');
|
||||
|
||||
return selectCache(state, path)?.lists[listKey];
|
||||
};
|
||||
|
||||
/** Select a particular item from a list state. */
|
||||
function selectListState<K extends keyof EntityListState>(state: RootState, path: EntitiesPath, key: K) {
|
||||
const listState = selectList(state, path)?.state;
|
||||
return listState ? listState[key] : undefined;
|
||||
}
|
||||
|
||||
/** Hook to get a particular item from a list state. */
|
||||
function useListState<K extends keyof EntityListState>(path: EntitiesPath, key: K) {
|
||||
return useAppSelector(state => selectListState(state, path, key));
|
||||
}
|
||||
|
||||
/** Get list of entities from Redux. */
|
||||
function selectEntities<TEntity extends Entity>(state: RootState, path: EntitiesPath): readonly TEntity[] {
|
||||
const cache = selectCache(state, path);
|
||||
const list = selectList(state, path);
|
||||
|
||||
const entityIds = list?.ids;
|
||||
|
||||
return entityIds ? (
|
||||
Array.from(entityIds).reduce<TEntity[]>((result, id) => {
|
||||
const entity = cache?.store[id];
|
||||
if (entity) {
|
||||
result.push(entity as TEntity);
|
||||
}
|
||||
return result;
|
||||
}, [])
|
||||
) : [];
|
||||
}
|
||||
|
||||
export {
|
||||
useEntities,
|
||||
};
|
|
@ -26,7 +26,7 @@ function useEntityActions<TEntity extends Entity = Entity, Data = any>(
|
|||
const { entityType, path } = parseEntitiesPath(expandedPath);
|
||||
|
||||
const { deleteEntity, isSubmitting: deleteSubmitting } =
|
||||
useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId)));
|
||||
useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replace(/:id/g, entityId)));
|
||||
|
||||
const { createEntity, isSubmitting: createSubmitting } =
|
||||
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { entitiesTransaction } from 'soapbox/entity-store/actions';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import type { EntityTypes } from 'soapbox/entity-store/entities';
|
||||
import type { EntitiesTransaction, Entity } from 'soapbox/entity-store/types';
|
||||
|
||||
type Updater<TEntity extends Entity> = Record<string, (entity: TEntity) => TEntity>
|
||||
|
||||
type Changes = Partial<{
|
||||
[K in keyof EntityTypes]: Updater<EntityTypes[K]>
|
||||
}>
|
||||
|
||||
function useTransaction() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function transaction(changes: Changes): void {
|
||||
dispatch(entitiesTransaction(changes as EntitiesTransaction));
|
||||
}
|
||||
|
||||
return { transaction };
|
||||
}
|
||||
|
||||
export { useTransaction };
|
|
@ -10,11 +10,12 @@ import {
|
|||
EntityAction,
|
||||
ENTITIES_INVALIDATE_LIST,
|
||||
ENTITIES_INCREMENT,
|
||||
ENTITIES_TRANSACTION,
|
||||
} from './actions';
|
||||
import { createCache, createList, updateStore, updateList } from './utils';
|
||||
|
||||
import type { DeleteEntitiesOpts } from './actions';
|
||||
import type { Entity, EntityCache, EntityListState, ImportPosition } from './types';
|
||||
import type { EntitiesTransaction, Entity, EntityCache, EntityListState, ImportPosition } from './types';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
|
@ -156,6 +157,20 @@ const invalidateEntityList = (state: State, entityType: string, listKey: string)
|
|||
});
|
||||
};
|
||||
|
||||
const doTransaction = (state: State, transaction: EntitiesTransaction) => {
|
||||
return produce(state, draft => {
|
||||
for (const [entityType, changes] of Object.entries(transaction)) {
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
for (const [id, change] of Object.entries(changes)) {
|
||||
const entity = cache.store[id];
|
||||
if (entity) {
|
||||
cache.store[id] = change(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** Stores various entity data and lists in a one reducer. */
|
||||
function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
||||
switch (action.type) {
|
||||
|
@ -175,6 +190,8 @@ function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
|||
return setFetching(state, action.entityType, action.listKey, false, action.error);
|
||||
case ENTITIES_INVALIDATE_LIST:
|
||||
return invalidateEntityList(state, action.entityType, action.listKey);
|
||||
case ENTITIES_TRANSACTION:
|
||||
return doTransaction(state, action.transaction);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { EntitiesPath } from './hooks/types';
|
||||
import type { Entity, EntityListState } from './types';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
/** Get cache at path from Redux. */
|
||||
const selectCache = (state: RootState, path: EntitiesPath) => state.entities[path[0]];
|
||||
|
||||
/** Get list at path from Redux. */
|
||||
const selectList = (state: RootState, path: EntitiesPath) => {
|
||||
const [, ...listKeys] = path;
|
||||
const listKey = listKeys.join(':');
|
||||
|
||||
return selectCache(state, path)?.lists[listKey];
|
||||
};
|
||||
|
||||
/** Select a particular item from a list state. */
|
||||
function selectListState<K extends keyof EntityListState>(state: RootState, path: EntitiesPath, key: K) {
|
||||
const listState = selectList(state, path)?.state;
|
||||
return listState ? listState[key] : undefined;
|
||||
}
|
||||
|
||||
/** Hook to get a particular item from a list state. */
|
||||
function useListState<K extends keyof EntityListState>(path: EntitiesPath, key: K) {
|
||||
return useAppSelector(state => selectListState(state, path, key));
|
||||
}
|
||||
|
||||
/** Get list of entities from Redux. */
|
||||
function selectEntities<TEntity extends Entity>(state: RootState, path: EntitiesPath): readonly TEntity[] {
|
||||
const cache = selectCache(state, path);
|
||||
const list = selectList(state, path);
|
||||
|
||||
const entityIds = list?.ids;
|
||||
|
||||
return entityIds ? (
|
||||
Array.from(entityIds).reduce<TEntity[]>((result, id) => {
|
||||
const entity = cache?.store[id];
|
||||
if (entity) {
|
||||
result.push(entity as TEntity);
|
||||
}
|
||||
return result;
|
||||
}, [])
|
||||
) : [];
|
||||
}
|
||||
|
||||
export {
|
||||
selectCache,
|
||||
selectList,
|
||||
selectListState,
|
||||
useListState,
|
||||
selectEntities,
|
||||
};
|
|
@ -26,7 +26,7 @@ interface EntityListState {
|
|||
/** Total number of items according to the API. */
|
||||
totalCount: number | undefined
|
||||
/** Error returned from the API, if any. */
|
||||
error: any
|
||||
error: unknown
|
||||
/** Whether data has already been fetched */
|
||||
fetched: boolean
|
||||
/** Whether data for this list is currently being fetched. */
|
||||
|
@ -50,11 +50,19 @@ interface EntityCache<TEntity extends Entity = Entity> {
|
|||
/** Whether to import items at the start or end of the list. */
|
||||
type ImportPosition = 'start' | 'end'
|
||||
|
||||
export {
|
||||
/** Map of entity mutation functions to perform at once on the store. */
|
||||
interface EntitiesTransaction {
|
||||
[entityType: string]: {
|
||||
[entityId: string]: <TEntity extends Entity>(entity: TEntity) => TEntity
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
Entity,
|
||||
EntityStore,
|
||||
EntityList,
|
||||
EntityListState,
|
||||
EntityCache,
|
||||
ImportPosition,
|
||||
EntitiesTransaction,
|
||||
};
|
|
@ -2,17 +2,14 @@ import React, { useEffect, useRef } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchAccountByUsername,
|
||||
} from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { expandAccountMediaTimeline } from 'soapbox/actions/timelines';
|
||||
import { useAccountLookup } from 'soapbox/api/hooks';
|
||||
import LoadMore from 'soapbox/components/load-more';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { getAccountGallery } from 'soapbox/selectors';
|
||||
|
||||
import MediaItem from './components/media-item';
|
||||
|
||||
|
@ -37,33 +34,17 @@ const LoadMoreMedia: React.FC<ILoadMoreMedia> = ({ maxId, onLoadMore }) => {
|
|||
const AccountGallery = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { username } = useParams<{ username: string }>();
|
||||
const features = useFeatures();
|
||||
|
||||
const { accountId, unavailable, accountUsername } = useAppSelector((state) => {
|
||||
const me = state.me;
|
||||
const accountFetchError = (state.accounts.get(-1)?.username || '').toLowerCase() === username.toLowerCase();
|
||||
const {
|
||||
account,
|
||||
isLoading: accountLoading,
|
||||
isUnavailable,
|
||||
} = useAccountLookup(username, { withRelationship: true });
|
||||
|
||||
let accountId: string | -1 | null = -1;
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
const account = findAccountByUsername(state, username);
|
||||
accountId = account ? (account.id || null) : -1;
|
||||
accountUsername = account?.acct || '';
|
||||
}
|
||||
|
||||
const isBlocked = state.relationships.get(String(accountId))?.blocked_by || false;
|
||||
return {
|
||||
accountId,
|
||||
unavailable: (me === accountId) ? false : (isBlocked && !features.blockersVisible),
|
||||
accountUsername,
|
||||
};
|
||||
});
|
||||
const isAccount = useAppSelector((state) => !!state.accounts.get(accountId));
|
||||
const attachments: ImmutableList<Attachment> = useAppSelector((state) => getAccountGallery(state, accountId as string));
|
||||
const isLoading = useAppSelector((state) => state.timelines.get(`account:${accountId}:media`)?.isLoading);
|
||||
const hasMore = useAppSelector((state) => state.timelines.get(`account:${accountId}:media`)?.hasMore);
|
||||
const attachments: ImmutableList<Attachment> = useAppSelector((state) => getAccountGallery(state, account!.id));
|
||||
const isLoading = useAppSelector((state) => state.timelines.get(`account:${account?.id}:media`)?.isLoading);
|
||||
const hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:media`)?.hasMore);
|
||||
const next = useAppSelector(state => state.timelines.get(`account:${account?.id}:media`)?.next);
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -74,8 +55,8 @@ const AccountGallery = () => {
|
|||
};
|
||||
|
||||
const handleLoadMore = (maxId: string | null) => {
|
||||
if (accountId && accountId !== -1) {
|
||||
dispatch(expandAccountMediaTimeline(accountId, { maxId }));
|
||||
if (account) {
|
||||
dispatch(expandAccountMediaTimeline(account.id, { url: next, maxId }));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -96,21 +77,12 @@ const AccountGallery = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId && accountId !== -1) {
|
||||
dispatch(fetchAccount(accountId));
|
||||
dispatch(expandAccountMediaTimeline(accountId));
|
||||
} else {
|
||||
dispatch(fetchAccountByUsername(username));
|
||||
if (account) {
|
||||
dispatch(expandAccountMediaTimeline(account.id));
|
||||
}
|
||||
}, [accountId]);
|
||||
}, [account?.id]);
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (accountId === -1 || (!attachments && isLoading)) {
|
||||
if (accountLoading || (!attachments && isLoading)) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
|
@ -118,13 +90,19 @@ const AccountGallery = () => {
|
|||
);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
||||
loadOlder = <LoadMore className='my-auto' visible={!isLoading} onClick={handleLoadOlder} />;
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
if (isUnavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
|
@ -135,7 +113,7 @@ const AccountGallery = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Column label={`@${accountUsername}`} transparent withHeader={false}>
|
||||
<Column label={`@${account.acct}`} transparent withHeader={false}>
|
||||
<div role='feed' className='grid grid-cols-2 gap-2 sm:grid-cols-3' ref={node}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.get(index + 1)?.id} maxId={index > 0 ? (attachments.get(index - 1)?.id || null) : null} onLoadMore={handleLoadMore} />
|
||||
|
|
|
@ -5,7 +5,7 @@ import Account from 'soapbox/components/account';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import { HStack, Text } from 'soapbox/components/ui';
|
||||
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
import type { Account as AccountEntity } from 'soapbox/schemas';
|
||||
|
||||
interface IMovedNote {
|
||||
from: AccountEntity
|
||||
|
|
|
@ -5,11 +5,12 @@ import { useHistory } from 'react-router-dom';
|
|||
import { fetchAccountByUsername } from 'soapbox/actions/accounts';
|
||||
import { fetchPatronAccount } from 'soapbox/actions/patron';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'soapbox/actions/timelines';
|
||||
import { useAccountLookup } from 'soapbox/api/hooks';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import StatusList from 'soapbox/components/status-list';
|
||||
import { Card, CardBody, Spinner, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
|
||||
import { makeGetStatusIds } from 'soapbox/selectors';
|
||||
|
||||
const getStatusIds = makeGetStatusIds();
|
||||
|
||||
|
@ -27,7 +28,7 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
|
|||
const settings = useSettings();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const account = useAppSelector(state => findAccountByUsername(state, params.username));
|
||||
const { account } = useAccountLookup(params.username, { withRelationship: true });
|
||||
const [accountLoading, setAccountLoading] = useState<boolean>(!account);
|
||||
|
||||
const path = withReplies ? `${account?.id}:with_replies` : account?.id;
|
||||
|
@ -40,6 +41,7 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
|
|||
const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true;
|
||||
const isLoading = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'isLoading']) === true);
|
||||
const hasMore = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'hasMore']) === true);
|
||||
const next = useAppSelector(state => state.timelines.get(`account:${path}`)?.next);
|
||||
|
||||
const accountUsername = account?.username || params.username;
|
||||
|
||||
|
@ -69,7 +71,7 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
|
|||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
if (account) {
|
||||
dispatch(expandAccountTimeline(account.id, { maxId, withReplies }));
|
||||
dispatch(expandAccountTimeline(account.id, { url: next, maxId, withReplies }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { blockAccount, followAccount, pinAccount, removeFromFollowers, unblockAccount, unmuteAccount, unpinAccount } from 'soapbox/actions/accounts';
|
||||
import { blockAccount, pinAccount, removeFromFollowers, unblockAccount, unmuteAccount, unpinAccount } from 'soapbox/actions/accounts';
|
||||
import { mentionCompose, directCompose } from 'soapbox/actions/compose';
|
||||
import { blockDomain, unblockDomain } from 'soapbox/actions/domain-blocks';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
|
@ -15,6 +15,7 @@ import { initMuteModal } from 'soapbox/actions/mutes';
|
|||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||
import { setSearchAccount } from 'soapbox/actions/search';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { useFollow } from 'soapbox/api/hooks';
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
|
@ -27,8 +28,8 @@ import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soap
|
|||
import { normalizeAttachment } from 'soapbox/normalizers';
|
||||
import { ChatKeys, useChats } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { Account } from 'soapbox/schemas';
|
||||
import toast from 'soapbox/toast';
|
||||
import { Account } from 'soapbox/types/entities';
|
||||
import { isDefaultHeader, isLocal, isRemote } from 'soapbox/utils/accounts';
|
||||
import copy from 'soapbox/utils/copy';
|
||||
import { MASTODON, parseVersion } from 'soapbox/utils/features';
|
||||
|
@ -86,7 +87,8 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const features = useFeatures();
|
||||
const ownAccount = useOwnAccount();
|
||||
const { account: ownAccount } = useOwnAccount();
|
||||
const { follow } = useFollow();
|
||||
|
||||
const { software } = useAppSelector((state) => parseVersion(state.instance.version));
|
||||
|
||||
|
@ -154,9 +156,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
|
||||
const onReblogToggle = () => {
|
||||
if (account.relationship?.showing_reblogs) {
|
||||
dispatch(followAccount(account.id, { reblogs: false }));
|
||||
follow(account.id, { reblogs: false });
|
||||
} else {
|
||||
dispatch(followAccount(account.id, { reblogs: true }));
|
||||
follow(account.id, { reblogs: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -574,7 +576,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
disabled={createAndNavigateToChat.isLoading}
|
||||
/>
|
||||
);
|
||||
} else if (account.getIn(['pleroma', 'accepts_chat_messages']) === true) {
|
||||
} else if (account.pleroma?.accepts_chat_messages) {
|
||||
return (
|
||||
<IconButton
|
||||
src={require('@tabler/icons/messages.svg')}
|
||||
|
@ -615,7 +617,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
return (
|
||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
||||
{(account.moved && typeof account.moved === 'object') && (
|
||||
<MovedNote from={account} to={account.moved} />
|
||||
<MovedNote from={account} to={account.moved as Account} />
|
||||
)}
|
||||
|
||||
<div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { approveUsers, deleteUsers } from 'soapbox/actions/admin';
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons';
|
||||
import { Stack, HStack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
interface IUnapprovedAccount {
|
||||
accountId: string
|
||||
|
@ -13,9 +13,8 @@ interface IUnapprovedAccount {
|
|||
/** Displays an unapproved account for moderation purposes. */
|
||||
const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
|
||||
const account = useAppSelector(state => getAccount(state, accountId));
|
||||
const { account } = useAccount(accountId);
|
||||
const adminAccount = useAppSelector(state => state.admin.users.get(accountId));
|
||||
|
||||
if (!account) return null;
|
||||
|
@ -27,7 +26,7 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
|
|||
<HStack space={4} justifyContent='between'>
|
||||
<Stack space={1}>
|
||||
<Text weight='semibold'>
|
||||
@{account.get('acct')}
|
||||
@{account.acct}
|
||||
</Text>
|
||||
<Text tag='blockquote' size='sm'>
|
||||
{adminAccount?.invite_request || ''}
|
||||
|
|
|
@ -16,7 +16,7 @@ const messages = defineMessages({
|
|||
|
||||
const Admin: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const Dashboard: React.FC = () => {
|
|||
const history = useHistory();
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const handleSubscribersClick: React.MouseEventHandler = e => {
|
||||
dispatch(getSubscribersCsv()).then(({ data }) => {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { addToAliases } from 'soapbox/actions/aliases';
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import AccountComponent from 'soapbox/components/account';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
add: { id: 'aliases.account.add', defaultMessage: 'Create alias' },
|
||||
|
@ -16,7 +14,7 @@ const messages = defineMessages({
|
|||
|
||||
interface IAccount {
|
||||
accountId: string
|
||||
aliases: ImmutableList<string>
|
||||
aliases: string[]
|
||||
}
|
||||
|
||||
const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
|
||||
|
@ -24,17 +22,12 @@ const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
|
|||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const { account } = useAccount(accountId);
|
||||
|
||||
const added = useAppSelector((state) => {
|
||||
const account = getAccount(state, accountId);
|
||||
const apId = account?.pleroma.get('ap_id');
|
||||
const name = features.accountMoving ? account?.acct : apId;
|
||||
|
||||
return aliases.includes(name);
|
||||
});
|
||||
const apId = account?.pleroma?.ap_id;
|
||||
const name = features.accountMoving ? account?.acct : apId;
|
||||
const added = name ? aliases.includes(name) : false;
|
||||
|
||||
const handleOnAdd = () => dispatch(addToAliases(account!));
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -24,15 +23,15 @@ const Aliases = () => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const aliases = useAppSelector((state) => {
|
||||
if (features.accountMoving) {
|
||||
return state.aliases.aliases.items;
|
||||
return [...state.aliases.aliases.items];
|
||||
} else {
|
||||
return account!.pleroma.get('also_known_as');
|
||||
return account?.pleroma?.also_known_as ?? [];
|
||||
}
|
||||
}) as ImmutableList<string>;
|
||||
});
|
||||
|
||||
const searchAccountIds = useAppSelector((state) => state.aliases.suggestions.items);
|
||||
const loaded = useAppSelector((state) => state.aliases.suggestions.loaded);
|
||||
|
|
|
@ -26,7 +26,7 @@ const AuthLayout = () => {
|
|||
const history = useHistory();
|
||||
const { search } = useLocation();
|
||||
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const instance = useInstance();
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const isLoginPage = history.location.pathname === '/login';
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import AccountComponent from 'soapbox/components/account';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
birthday: { id: 'account.birthday', defaultMessage: 'Born {date}' },
|
||||
|
@ -17,13 +16,11 @@ interface IAccount {
|
|||
|
||||
const Account: React.FC<IAccount> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
|
||||
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||
const { account } = useAccount(accountId);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const birthday = account.birthday;
|
||||
const birthday = account.pleroma?.birthday;
|
||||
if (!birthday) return null;
|
||||
|
||||
const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
|
|
|
@ -1,33 +1,26 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchBlocks, expandBlocks } from 'soapbox/actions/blocks';
|
||||
import { useBlocks } from 'soapbox/api/hooks';
|
||||
import Account from 'soapbox/components/account';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
|
||||
heading: { id: 'column.blocks', defaultMessage: 'Blocks' },
|
||||
});
|
||||
|
||||
const handleLoadMore = debounce((dispatch) => {
|
||||
dispatch(expandBlocks());
|
||||
}, 300, { leading: true });
|
||||
|
||||
const Blocks: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const accountIds = useAppSelector((state) => state.user_lists.blocks.items);
|
||||
const hasMore = useAppSelector((state) => !!state.user_lists.blocks.next);
|
||||
const {
|
||||
accounts,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isLoading,
|
||||
} = useBlocks();
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchBlocks());
|
||||
}, []);
|
||||
|
||||
if (!accountIds) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
|
@ -41,14 +34,15 @@ const Blocks: React.FC = () => {
|
|||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
scrollKey='blocks'
|
||||
onLoadMore={() => handleLoadMore(dispatch)}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={fetchNextPage}
|
||||
hasMore={hasNextPage}
|
||||
emptyMessage={emptyMessage}
|
||||
itemClassName='pb-4'
|
||||
emptyMessageCard={false}
|
||||
itemClassName='pb-4 last:pb-0'
|
||||
>
|
||||
{accountIds.map((id) =>
|
||||
<AccountContainer key={id} id={id} actionType='blocking' />,
|
||||
)}
|
||||
{accounts.map((account) => (
|
||||
<Account key={account.id} account={account} actionType='blocking' />
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
|||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
|
||||
import { ChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers';
|
||||
import { IAccount } from 'soapbox/queries/accounts';
|
||||
import { ChatMessage } from 'soapbox/types/entities';
|
||||
|
||||
import { __stub } from '../../../../api';
|
||||
|
@ -14,7 +14,7 @@ import ChatMessageList from '../chat-message-list';
|
|||
|
||||
const chat: IChat = {
|
||||
accepted: true,
|
||||
account: {
|
||||
account: buildAccount({
|
||||
username: 'username',
|
||||
verified: true,
|
||||
id: '1',
|
||||
|
@ -22,7 +22,7 @@ const chat: IChat = {
|
|||
avatar: 'avatar',
|
||||
avatar_static: 'avatar',
|
||||
display_name: 'my name',
|
||||
} as IAccount,
|
||||
}),
|
||||
chat_type: 'direct',
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
created_by_account: '2',
|
||||
|
|
|
@ -1,26 +1,32 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
|
||||
import { render, rootState } from '../../../../jest/test-helpers';
|
||||
import ChatWidget from '../chat-widget/chat-widget';
|
||||
|
||||
const id = '1';
|
||||
const account = normalizeAccount({
|
||||
const account = buildAccount({
|
||||
id,
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
chats_onboarded: true,
|
||||
source: {
|
||||
chats_onboarded: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = rootState
|
||||
.set('me', id)
|
||||
.set('accounts', ImmutableMap({
|
||||
[id]: account,
|
||||
}) as any);
|
||||
.set('entities', {
|
||||
'ACCOUNTS': {
|
||||
store: {
|
||||
[id]: account,
|
||||
},
|
||||
lists: {},
|
||||
},
|
||||
});
|
||||
|
||||
describe('<ChatWidget />', () => {
|
||||
describe('when on the /chats endpoint', () => {
|
||||
|
@ -43,28 +49,35 @@ describe('<ChatWidget />', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the user has not onboarded chats', () => {
|
||||
it('hides the widget', async () => {
|
||||
const accountWithoutChats = normalizeAccount({
|
||||
id,
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
chats_onboarded: false,
|
||||
});
|
||||
const newStore = store.set('accounts', ImmutableMap({
|
||||
[id]: accountWithoutChats,
|
||||
}) as any);
|
||||
// describe('when the user has not onboarded chats', () => {
|
||||
// it('hides the widget', async () => {
|
||||
// const accountWithoutChats = buildAccount({
|
||||
// id,
|
||||
// acct: 'justin-username',
|
||||
// display_name: 'Justin L',
|
||||
// avatar: 'test.jpg',
|
||||
// source: {
|
||||
// chats_onboarded: false,
|
||||
// },
|
||||
// });
|
||||
// const newStore = store.set('entities', {
|
||||
// 'ACCOUNTS': {
|
||||
// store: {
|
||||
// [id]: accountWithoutChats,
|
||||
// },
|
||||
// lists: {},
|
||||
// },
|
||||
// });
|
||||
|
||||
const screen = render(
|
||||
<ChatWidget />,
|
||||
{},
|
||||
newStore,
|
||||
);
|
||||
// const screen = render(
|
||||
// <ChatWidget />,
|
||||
// {},
|
||||
// newStore,
|
||||
// );
|
||||
|
||||
expect(screen.queryAllByTestId('pane')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
// expect(screen.queryAllByTestId('pane')).toHaveLength(0);
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('when the user is onboarded and the endpoint is not /chats', () => {
|
||||
it('shows the widget', async () => {
|
||||
|
|
|
@ -69,7 +69,7 @@ interface IChatMessageList {
|
|||
/** Scrollable list of chat messages. */
|
||||
const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
|
||||
const intl = useIntl();
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const myLastReadMessageDateString = chat.latest_read_message_by_account?.find((latest) => latest.id === account?.id)?.date;
|
||||
const myLastReadMessageTimestamp = myLastReadMessageDateString ? new Date(myLastReadMessageDateString) : null;
|
||||
|
@ -109,9 +109,13 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
|
|||
return [];
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return chatMessages.reduce((acc: any, curr: any, idx: number) => {
|
||||
const lastMessage = formattedChatMessages[idx - 1];
|
||||
|
||||
const messageDate = new Date(curr.created_at);
|
||||
|
||||
if (lastMessage) {
|
||||
switch (timeChange(lastMessage, curr)) {
|
||||
case 'today':
|
||||
|
@ -123,7 +127,14 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
|
|||
case 'date':
|
||||
acc.push({
|
||||
type: 'divider',
|
||||
text: intl.formatDate(new Date(curr.created_at), { weekday: 'short', hour: 'numeric', minute: '2-digit', month: 'short', day: 'numeric' }),
|
||||
text: intl.formatDate(messageDate, {
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: messageDate.getFullYear() !== currentYear ? '2-digit' : undefined,
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import emojify from 'soapbox/features/emoji';
|
|||
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { stripHTML } from 'soapbox/utils/html';
|
||||
|
@ -24,7 +23,7 @@ import ChatMessageReactionWrapper from './chat-message-reaction-wrapper/chat-mes
|
|||
|
||||
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
|
||||
import type { IMediaGallery } from 'soapbox/components/media-gallery';
|
||||
import type { Account, ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
|
||||
import type { ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' },
|
||||
|
@ -178,7 +177,7 @@ const ChatMessage = (props: IChatMessage) => {
|
|||
if (features.reportChats) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.report),
|
||||
action: () => dispatch(initReport(ReportableEntities.CHAT_MESSAGE, normalizeAccount(chat.account) as Account, { chatMessage })),
|
||||
action: () => dispatch(initReport(ReportableEntities.CHAT_MESSAGE, chat.account, { chatMessage })),
|
||||
icon: require('@tabler/icons/flag.svg'),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,90 +1,94 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
test.skip('skip', () => {});
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
// import userEvent from '@testing-library/user-event';
|
||||
// import React from 'react';
|
||||
|
||||
import { render, screen, waitFor } from '../../../../../jest/test-helpers';
|
||||
import ChatPage from '../chat-page';
|
||||
// import { __stub } from 'soapbox/api';
|
||||
// import { buildAccount } from 'soapbox/jest/factory';
|
||||
// import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
describe('<ChatPage />', () => {
|
||||
let store: any;
|
||||
// import ChatPage from '../chat-page';
|
||||
|
||||
describe('before you finish onboarding', () => {
|
||||
it('renders the Welcome component', () => {
|
||||
render(<ChatPage />);
|
||||
// describe('<ChatPage />', () => {
|
||||
// let store: any;
|
||||
|
||||
expect(screen.getByTestId('chats-welcome')).toBeInTheDocument();
|
||||
});
|
||||
// describe('before you finish onboarding', () => {
|
||||
// it('renders the Welcome component', () => {
|
||||
// render(<ChatPage />);
|
||||
|
||||
describe('when you complete onboarding', () => {
|
||||
const id = '1';
|
||||
// expect(screen.getByTestId('chats-welcome')).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
me: id,
|
||||
accounts: ImmutableMap({
|
||||
[id]: normalizeAccount({
|
||||
id,
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
chats_onboarded: false,
|
||||
}) as ReducerAccount,
|
||||
}),
|
||||
};
|
||||
// describe('when you complete onboarding', () => {
|
||||
// const id = '1';
|
||||
|
||||
__stub((mock) => {
|
||||
mock
|
||||
.onPatch('/api/v1/accounts/update_credentials')
|
||||
.reply(200, { chats_onboarded: true, id });
|
||||
});
|
||||
});
|
||||
// beforeEach(() => {
|
||||
// store = {
|
||||
// me: id,
|
||||
// accounts: {
|
||||
// [id]: buildAccount({
|
||||
// id,
|
||||
// acct: 'justin-username',
|
||||
// display_name: 'Justin L',
|
||||
// avatar: 'test.jpg',
|
||||
// source: {
|
||||
// chats_onboarded: false,
|
||||
// },
|
||||
// }),
|
||||
// },
|
||||
// };
|
||||
|
||||
it('renders the Chats', async () => {
|
||||
render(<ChatPage />, undefined, store);
|
||||
await userEvent.click(screen.getByTestId('button'));
|
||||
// __stub((mock) => {
|
||||
// mock
|
||||
// .onPatch('/api/v1/accounts/update_credentials')
|
||||
// .reply(200, { chats_onboarded: true, id });
|
||||
// });
|
||||
// });
|
||||
|
||||
expect(screen.getByTestId('chat-page')).toBeInTheDocument();
|
||||
// it('renders the Chats', async () => {
|
||||
// render(<ChatPage />, undefined, store);
|
||||
// await userEvent.click(screen.getByTestId('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings updated successfully');
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(screen.getByTestId('chat-page')).toBeInTheDocument();
|
||||
|
||||
describe('when the API returns an error', () => {
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
me: '1',
|
||||
accounts: ImmutableMap({
|
||||
'1': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
chats_onboarded: false,
|
||||
}) as ReducerAccount,
|
||||
}),
|
||||
};
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings updated successfully');
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
__stub((mock) => {
|
||||
mock
|
||||
.onPatch('/api/v1/accounts/update_credentials')
|
||||
.networkError();
|
||||
});
|
||||
});
|
||||
// describe('when the API returns an error', () => {
|
||||
// beforeEach(() => {
|
||||
// store = {
|
||||
// me: '1',
|
||||
// accounts: {
|
||||
// '1': buildAccount({
|
||||
// id: '1',
|
||||
// acct: 'justin-username',
|
||||
// display_name: 'Justin L',
|
||||
// avatar: 'test.jpg',
|
||||
// source: {
|
||||
// chats_onboarded: false,
|
||||
// },
|
||||
// }),
|
||||
// },
|
||||
// };
|
||||
|
||||
it('renders the Chats', async () => {
|
||||
render(<ChatPage />, undefined, store);
|
||||
await userEvent.click(screen.getByTestId('button'));
|
||||
// __stub((mock) => {
|
||||
// mock
|
||||
// .onPatch('/api/v1/accounts/update_credentials')
|
||||
// .networkError();
|
||||
// });
|
||||
// });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings failed to update.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// it('renders the Chats', async () => {
|
||||
// render(<ChatPage />, undefined, store);
|
||||
// await userEvent.click(screen.getByTestId('button'));
|
||||
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings failed to update.');
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
|
|
@ -16,10 +16,10 @@ interface IChatPage {
|
|||
}
|
||||
|
||||
const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const history = useHistory();
|
||||
|
||||
const isOnboarded = account?.chats_onboarded;
|
||||
const isOnboarded = account?.source?.chats_onboarded ?? true;
|
||||
|
||||
const path = history.location.pathname;
|
||||
const isSidebarHidden = matchPath(path, {
|
||||
|
|
|
@ -24,7 +24,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const ChatPageSettings = () => {
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -33,7 +33,7 @@ const ChatPageSettings = () => {
|
|||
|
||||
const [data, setData] = useState<FormData>({
|
||||
chats_onboarded: true,
|
||||
accepts_chat_messages: account?.accepts_chat_messages,
|
||||
accepts_chat_messages: account?.pleroma?.accepts_chat_messages === true,
|
||||
});
|
||||
|
||||
const onToggleChange = (key: string[], checked: boolean) => {
|
||||
|
|
|
@ -20,13 +20,13 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const Welcome = () => {
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const intl = useIntl();
|
||||
const updateCredentials = useUpdateCredentials();
|
||||
|
||||
const [data, setData] = useState<FormData>({
|
||||
chats_onboarded: true,
|
||||
accepts_chat_messages: account?.accepts_chat_messages,
|
||||
accepts_chat_messages: account?.pleroma?.accepts_chat_messages === true,
|
||||
});
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
|
|
|
@ -7,13 +7,14 @@ import { useOwnAccount } from 'soapbox/hooks';
|
|||
import ChatPane from '../chat-pane/chat-pane';
|
||||
|
||||
const ChatWidget = () => {
|
||||
const account = useOwnAccount();
|
||||
const { account } = useOwnAccount();
|
||||
const history = useHistory();
|
||||
|
||||
const path = history.location.pathname;
|
||||
const shouldHideWidget = Boolean(path.match(/^\/chats/));
|
||||
const isChatsPath = Boolean(path.match(/^\/chats/));
|
||||
const isOnboarded = account?.source?.chats_onboarded ?? true;
|
||||
|
||||
if (!account?.chats_onboarded || shouldHideWidget) {
|
||||
if (!isOnboarded || isChatsPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { connectCommunityStream } from 'soapbox/actions/streaming';
|
|||
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
|
@ -19,11 +19,12 @@ const CommunityTimeline = () => {
|
|||
|
||||
const settings = useSettings();
|
||||
const onlyMedia = settings.getIn(['community', 'other', 'onlyMedia']);
|
||||
const next = useAppSelector(state => state.timelines.get('community')?.next);
|
||||
|
||||
const timelineId = 'community';
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||
dispatch(expandCommunityTimeline({ url: next, maxId, onlyMedia }));
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
test.skip('skip', () => {});
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
// import userEvent from '@testing-library/user-event';
|
||||
// import React from 'react';
|
||||
|
||||
import { render, screen, waitFor } from '../../../../jest/test-helpers';
|
||||
import Search from '../search';
|
||||
// import { __stub } from 'soapbox/api';
|
||||
|
||||
describe('<Search />', () => {
|
||||
it('successfully renders', async() => {
|
||||
render(<Search autosuggest />);
|
||||
expect(screen.getByLabelText('Search')).toBeInTheDocument();
|
||||
});
|
||||
// import { render, screen, waitFor } from '../../../../jest/test-helpers';
|
||||
// import Search from '../search';
|
||||
|
||||
it('handles onChange', async() => {
|
||||
__stub(mock => {
|
||||
mock.onGet('/api/v1/accounts/search').reply(200, [{ id: 1 }]);
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
// describe('<Search />', () => {
|
||||
// it('successfully renders', async() => {
|
||||
// render(<Search autosuggest />);
|
||||
// expect(screen.getByLabelText('Search')).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
render(<Search autosuggest />);
|
||||
// it('handles onChange', async() => {
|
||||
// __stub(mock => {
|
||||
// mock.onGet('/api/v1/accounts/search').reply(200, [{ id: 1 }]);
|
||||
// });
|
||||
// const user = userEvent.setup();
|
||||
|
||||
await user.type(screen.getByLabelText('Search'), '@jus');
|
||||
// render(<Search autosuggest />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Search')).toHaveValue('@jus');
|
||||
expect(screen.getByTestId('account')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
// await user.type(screen.getByLabelText('Search'), '@jus');
|
||||
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByLabelText('Search')).toHaveValue('@jus');
|
||||
// expect(screen.getByTestId('account')).toBeInTheDocument();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue