Merge tag 'v4.3.0-rc.1'
This commit is contained in:
commit
26c9b9ba39
3459 changed files with 130932 additions and 69993 deletions
|
|
@ -1,18 +1,9 @@
|
|||
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||
import { apiSubmitAccountNote } from 'mastodon/api/accounts';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
export const submitAccountNote = createAppAsyncThunk(
|
||||
export const submitAccountNote = createDataLoadingThunk(
|
||||
'account_note/submit',
|
||||
async (args: { id: string; value: string }, { getState }) => {
|
||||
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
|
||||
const response = await api(getState).post<unknown>(
|
||||
`/api/v1/accounts/${args.id}/note`,
|
||||
{
|
||||
comment: args.value,
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship: response.data };
|
||||
},
|
||||
({ accountId, note }: { accountId: string; note: string }) =>
|
||||
apiSubmitAccountNote(accountId, note),
|
||||
(relationship) => ({ relationship }),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,18 @@
|
|||
import { browserHistory } from 'mastodon/components/router';
|
||||
import { debounceWithDispatchAndArguments } from 'mastodon/utils/debounce';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import {
|
||||
followAccountSuccess, unfollowAccountSuccess,
|
||||
authorizeFollowRequestSuccess, rejectFollowRequestSuccess,
|
||||
followAccountRequest, followAccountFail,
|
||||
unfollowAccountRequest, unfollowAccountFail,
|
||||
muteAccountSuccess, unmuteAccountSuccess,
|
||||
blockAccountSuccess, unblockAccountSuccess,
|
||||
pinAccountSuccess, unpinAccountSuccess,
|
||||
fetchRelationshipsSuccess,
|
||||
} from './accounts_typed';
|
||||
import { importFetchedAccount, importFetchedAccounts } from './importer';
|
||||
|
||||
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
||||
|
|
@ -10,36 +23,22 @@ export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
|
|||
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
|
||||
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
|
||||
|
||||
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
|
||||
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
|
||||
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
|
||||
|
||||
export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
|
||||
export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
|
||||
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
|
||||
|
||||
export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
|
||||
export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
|
||||
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
|
||||
|
||||
export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
|
||||
export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
|
||||
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
|
||||
|
||||
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
|
||||
export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
|
||||
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
|
||||
|
||||
export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
|
||||
export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
|
||||
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
|
||||
|
||||
export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
|
||||
export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS';
|
||||
export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
|
||||
|
||||
export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
|
||||
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
|
||||
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
|
||||
|
||||
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
|
||||
|
|
@ -59,7 +58,6 @@ export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
|
|||
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
|
||||
|
||||
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
|
||||
export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
|
||||
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
|
||||
|
||||
export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
|
||||
|
|
@ -71,21 +69,21 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
|
|||
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
|
||||
|
||||
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
|
||||
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
|
||||
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
|
||||
|
||||
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
|
||||
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
||||
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
||||
|
||||
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
|
||||
|
||||
export * from './accounts_typed';
|
||||
|
||||
export function fetchAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchRelationships([id]));
|
||||
dispatch(fetchAccountRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||
api().get(`/api/v1/accounts/${id}`).then(response => {
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
dispatch(fetchAccountSuccess());
|
||||
}).catch(error => {
|
||||
|
|
@ -94,10 +92,10 @@ export function fetchAccount(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export const lookupAccount = acct => (dispatch, getState) => {
|
||||
export const lookupAccount = acct => (dispatch) => {
|
||||
dispatch(lookupAccountRequest(acct));
|
||||
|
||||
api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
|
||||
api().get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
|
||||
dispatch(fetchRelationships([response.data.id]));
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
dispatch(lookupAccountSuccess());
|
||||
|
|
@ -149,12 +147,12 @@ export function followAccount(id, options = { reblogs: true }) {
|
|||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||
|
||||
dispatch(followAccountRequest(id, locked));
|
||||
dispatch(followAccountRequest({ id, locked }));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||
api().post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||
dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing}));
|
||||
}).catch(error => {
|
||||
dispatch(followAccountFail(error, locked));
|
||||
dispatch(followAccountFail({ id, error, locked }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -163,87 +161,35 @@ export function unfollowAccount(id) {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(unfollowAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
|
||||
dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
|
||||
api().post(`/api/v1/accounts/${id}/unfollow`).then(response => {
|
||||
dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')}));
|
||||
}).catch(error => {
|
||||
dispatch(unfollowAccountFail(error));
|
||||
dispatch(unfollowAccountFail({ id, error }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function followAccountRequest(id, locked) {
|
||||
return {
|
||||
type: ACCOUNT_FOLLOW_REQUEST,
|
||||
id,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function followAccountSuccess(relationship, alreadyFollowing) {
|
||||
return {
|
||||
type: ACCOUNT_FOLLOW_SUCCESS,
|
||||
relationship,
|
||||
alreadyFollowing,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function followAccountFail(error, locked) {
|
||||
return {
|
||||
type: ACCOUNT_FOLLOW_FAIL,
|
||||
error,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unfollowAccountRequest(id) {
|
||||
return {
|
||||
type: ACCOUNT_UNFOLLOW_REQUEST,
|
||||
id,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unfollowAccountSuccess(relationship, statuses) {
|
||||
return {
|
||||
type: ACCOUNT_UNFOLLOW_SUCCESS,
|
||||
relationship,
|
||||
statuses,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unfollowAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_UNFOLLOW_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function blockAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(blockAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
|
||||
api().post(`/api/v1/accounts/${id}/block`).then(response => {
|
||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||
dispatch(blockAccountSuccess(response.data, getState().get('statuses')));
|
||||
dispatch(blockAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
|
||||
}).catch(error => {
|
||||
dispatch(blockAccountFail(id, error));
|
||||
dispatch(blockAccountFail({ id, error }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unblockAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unblockAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
|
||||
dispatch(unblockAccountSuccess(response.data));
|
||||
api().post(`/api/v1/accounts/${id}/unblock`).then(response => {
|
||||
dispatch(unblockAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(unblockAccountFail(id, error));
|
||||
dispatch(unblockAccountFail({ id, error }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -254,15 +200,6 @@ export function blockAccountRequest(id) {
|
|||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function blockAccountSuccess(relationship, statuses) {
|
||||
return {
|
||||
type: ACCOUNT_BLOCK_SUCCESS,
|
||||
relationship,
|
||||
statuses,
|
||||
};
|
||||
}
|
||||
|
||||
export function blockAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_BLOCK_FAIL,
|
||||
|
|
@ -277,13 +214,6 @@ export function unblockAccountRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function unblockAccountSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_UNBLOCK_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
export function unblockAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_UNBLOCK_FAIL,
|
||||
|
|
@ -296,23 +226,23 @@ export function muteAccount(id, notifications, duration=0) {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(muteAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
|
||||
api().post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
|
||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
|
||||
dispatch(muteAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
|
||||
}).catch(error => {
|
||||
dispatch(muteAccountFail(id, error));
|
||||
dispatch(muteAccountFail({ id, error }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unmuteAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unmuteAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
|
||||
dispatch(unmuteAccountSuccess(response.data));
|
||||
api().post(`/api/v1/accounts/${id}/unmute`).then(response => {
|
||||
dispatch(unmuteAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(unmuteAccountFail(id, error));
|
||||
dispatch(unmuteAccountFail({ id, error }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -324,14 +254,6 @@ export function muteAccountRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function muteAccountSuccess(relationship, statuses) {
|
||||
return {
|
||||
type: ACCOUNT_MUTE_SUCCESS,
|
||||
relationship,
|
||||
statuses,
|
||||
};
|
||||
}
|
||||
|
||||
export function muteAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_MUTE_FAIL,
|
||||
|
|
@ -346,13 +268,6 @@ export function unmuteAccountRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function unmuteAccountSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_UNMUTE_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
export function unmuteAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_UNMUTE_FAIL,
|
||||
|
|
@ -362,10 +277,10 @@ export function unmuteAccountFail(error) {
|
|||
|
||||
|
||||
export function fetchFollowers(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchFollowersRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
|
||||
api().get(`/api/v1/accounts/${id}/followers`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
|
|
@ -412,7 +327,7 @@ export function expandFollowers(id) {
|
|||
|
||||
dispatch(expandFollowersRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
|
|
@ -449,10 +364,10 @@ export function expandFollowersFail(id, error) {
|
|||
}
|
||||
|
||||
export function fetchFollowing(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchFollowingRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
|
||||
api().get(`/api/v1/accounts/${id}/following`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
|
|
@ -499,7 +414,7 @@ export function expandFollowing(id) {
|
|||
|
||||
dispatch(expandFollowingRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
|
|
@ -535,6 +450,20 @@ export function expandFollowingFail(id, error) {
|
|||
};
|
||||
}
|
||||
|
||||
const debouncedFetchRelationships = debounceWithDispatchAndArguments((dispatch, ...newAccountIds) => {
|
||||
if (newAccountIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchRelationshipsRequest(newAccountIds));
|
||||
|
||||
api().get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(fetchRelationshipsFail(error));
|
||||
});
|
||||
}, { delay: 500 });
|
||||
|
||||
export function fetchRelationships(accountIds) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
|
@ -546,13 +475,7 @@ export function fetchRelationships(accountIds) {
|
|||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchRelationshipsRequest(newAccountIds));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||
dispatch(fetchRelationshipsSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchRelationshipsFail(error));
|
||||
});
|
||||
debouncedFetchRelationships(dispatch, ...newAccountIds);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -564,14 +487,6 @@ export function fetchRelationshipsRequest(ids) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchRelationshipsSuccess(relationships) {
|
||||
return {
|
||||
type: RELATIONSHIPS_FETCH_SUCCESS,
|
||||
relationships,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchRelationshipsFail(error) {
|
||||
return {
|
||||
type: RELATIONSHIPS_FETCH_FAIL,
|
||||
|
|
@ -582,10 +497,10 @@ export function fetchRelationshipsFail(error) {
|
|||
}
|
||||
|
||||
export function fetchFollowRequests() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchFollowRequestsRequest());
|
||||
|
||||
api(getState).get('/api/v1/follow_requests').then(response => {
|
||||
api().get('/api/v1/follow_requests').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -624,7 +539,7 @@ export function expandFollowRequests() {
|
|||
|
||||
dispatch(expandFollowRequestsRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -654,12 +569,12 @@ export function expandFollowRequestsFail(error) {
|
|||
}
|
||||
|
||||
export function authorizeFollowRequest(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(authorizeFollowRequestRequest(id));
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.post(`/api/v1/follow_requests/${id}/authorize`)
|
||||
.then(() => dispatch(authorizeFollowRequestSuccess(id)))
|
||||
.then(() => dispatch(authorizeFollowRequestSuccess({ id })))
|
||||
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
|
@ -671,13 +586,6 @@ export function authorizeFollowRequestRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function authorizeFollowRequestSuccess(id) {
|
||||
return {
|
||||
type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function authorizeFollowRequestFail(id, error) {
|
||||
return {
|
||||
type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
|
||||
|
|
@ -688,12 +596,12 @@ export function authorizeFollowRequestFail(id, error) {
|
|||
|
||||
|
||||
export function rejectFollowRequest(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(rejectFollowRequestRequest(id));
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.post(`/api/v1/follow_requests/${id}/reject`)
|
||||
.then(() => dispatch(rejectFollowRequestSuccess(id)))
|
||||
.then(() => dispatch(rejectFollowRequestSuccess({ id })))
|
||||
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
|
@ -705,13 +613,6 @@ export function rejectFollowRequestRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function rejectFollowRequestSuccess(id) {
|
||||
return {
|
||||
type: FOLLOW_REQUEST_REJECT_SUCCESS,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function rejectFollowRequestFail(id, error) {
|
||||
return {
|
||||
type: FOLLOW_REQUEST_REJECT_FAIL,
|
||||
|
|
@ -721,11 +622,11 @@ export function rejectFollowRequestFail(id, error) {
|
|||
}
|
||||
|
||||
export function pinAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(pinAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
|
||||
dispatch(pinAccountSuccess(response.data));
|
||||
api().post(`/api/v1/accounts/${id}/pin`).then(response => {
|
||||
dispatch(pinAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(pinAccountFail(error));
|
||||
});
|
||||
|
|
@ -733,11 +634,11 @@ export function pinAccount(id) {
|
|||
}
|
||||
|
||||
export function unpinAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unpinAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
|
||||
dispatch(unpinAccountSuccess(response.data));
|
||||
api().post(`/api/v1/accounts/${id}/unpin`).then(response => {
|
||||
dispatch(unpinAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(unpinAccountFail(error));
|
||||
});
|
||||
|
|
@ -751,13 +652,6 @@ export function pinAccountRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function pinAccountSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_PIN_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
export function pinAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_PIN_FAIL,
|
||||
|
|
@ -772,13 +666,6 @@ export function unpinAccountRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function unpinAccountSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_UNPIN_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
export function unpinAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_UNPIN_FAIL,
|
||||
|
|
@ -786,7 +673,27 @@ export function unpinAccountFail(error) {
|
|||
};
|
||||
}
|
||||
|
||||
export const revealAccount = id => ({
|
||||
type: ACCOUNT_REVEAL,
|
||||
id,
|
||||
});
|
||||
export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch) => {
|
||||
const data = new FormData();
|
||||
|
||||
data.append('display_name', displayName);
|
||||
data.append('note', note);
|
||||
if (avatar) data.append('avatar', avatar);
|
||||
if (header) data.append('header', header);
|
||||
data.append('discoverable', discoverable);
|
||||
data.append('indexable', indexable);
|
||||
|
||||
return api().patch('/api/v1/accounts/update_credentials', data).then(response => {
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
});
|
||||
};
|
||||
|
||||
export const navigateToProfile = (accountId) => {
|
||||
return (_dispatch, getState) => {
|
||||
const acct = getState().accounts.getIn([accountId, 'acct']);
|
||||
|
||||
if (acct) {
|
||||
browserHistory.push(`/@${acct}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
97
app/javascript/mastodon/actions/accounts_typed.ts
Normal file
97
app/javascript/mastodon/actions/accounts_typed.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||
|
||||
export const revealAccount = createAction<{
|
||||
id: string;
|
||||
}>('accounts/revealAccount');
|
||||
|
||||
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
|
||||
'accounts/importAccounts',
|
||||
);
|
||||
|
||||
function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
|
||||
return {
|
||||
payload: {
|
||||
...args,
|
||||
skipLoading: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const followAccountSuccess = createAction(
|
||||
'accounts/followAccount/SUCCESS',
|
||||
actionWithSkipLoadingTrue<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
alreadyFollowing: boolean;
|
||||
}>,
|
||||
);
|
||||
|
||||
export const unfollowAccountSuccess = createAction(
|
||||
'accounts/unfollowAccount/SUCCESS',
|
||||
actionWithSkipLoadingTrue<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
statuses: unknown;
|
||||
alreadyFollowing?: boolean;
|
||||
}>,
|
||||
);
|
||||
|
||||
export const authorizeFollowRequestSuccess = createAction<{ id: string }>(
|
||||
'accounts/followRequestAuthorize/SUCCESS',
|
||||
);
|
||||
|
||||
export const rejectFollowRequestSuccess = createAction<{ id: string }>(
|
||||
'accounts/followRequestReject/SUCCESS',
|
||||
);
|
||||
|
||||
export const followAccountRequest = createAction(
|
||||
'accounts/follow/REQUEST',
|
||||
actionWithSkipLoadingTrue<{ id: string; locked: boolean }>,
|
||||
);
|
||||
|
||||
export const followAccountFail = createAction(
|
||||
'accounts/follow/FAIL',
|
||||
actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>,
|
||||
);
|
||||
|
||||
export const unfollowAccountRequest = createAction(
|
||||
'accounts/unfollow/REQUEST',
|
||||
actionWithSkipLoadingTrue<{ id: string }>,
|
||||
);
|
||||
|
||||
export const unfollowAccountFail = createAction(
|
||||
'accounts/unfollow/FAIL',
|
||||
actionWithSkipLoadingTrue<{ id: string; error: string }>,
|
||||
);
|
||||
|
||||
export const blockAccountSuccess = createAction<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
statuses: unknown;
|
||||
}>('accounts/block/SUCCESS');
|
||||
|
||||
export const unblockAccountSuccess = createAction<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
}>('accounts/unblock/SUCCESS');
|
||||
|
||||
export const muteAccountSuccess = createAction<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
statuses: unknown;
|
||||
}>('accounts/mute/SUCCESS');
|
||||
|
||||
export const unmuteAccountSuccess = createAction<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
}>('accounts/unmute/SUCCESS');
|
||||
|
||||
export const pinAccountSuccess = createAction<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
}>('accounts/pin/SUCCESS');
|
||||
|
||||
export const unpinAccountSuccess = createAction<{
|
||||
relationship: ApiRelationshipJSON;
|
||||
}>('accounts/unpin/SUCCESS');
|
||||
|
||||
export const fetchRelationshipsSuccess = createAction(
|
||||
'relationships/fetch/SUCCESS',
|
||||
actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>,
|
||||
);
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
||||
|
|
@ -50,10 +52,15 @@ export const showAlertForError = (error, skipNotFound = false) => {
|
|||
});
|
||||
}
|
||||
|
||||
// An aborted request, e.g. due to reloading the browser window, it not really error
|
||||
if (error.code === AxiosError.ECONNABORTED) {
|
||||
return { type: ALERT_NOOP };
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
|
||||
return showAlert({
|
||||
title: messages.unexpectedTitle,
|
||||
message: messages.unexpectedMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW';
|
|||
|
||||
const noOp = () => {};
|
||||
|
||||
export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => {
|
||||
export const fetchAnnouncements = (done = noOp) => (dispatch) => {
|
||||
dispatch(fetchAnnouncementsRequest());
|
||||
|
||||
api(getState).get('/api/v1/announcements').then(response => {
|
||||
api().get('/api/v1/announcements').then(response => {
|
||||
dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x))));
|
||||
}).catch(error => {
|
||||
dispatch(fetchAnnouncementsFail(error));
|
||||
|
|
@ -61,10 +61,10 @@ export const updateAnnouncements = announcement => ({
|
|||
announcement: normalizeAnnouncement(announcement),
|
||||
});
|
||||
|
||||
export const dismissAnnouncement = announcementId => (dispatch, getState) => {
|
||||
export const dismissAnnouncement = announcementId => (dispatch) => {
|
||||
dispatch(dismissAnnouncementRequest(announcementId));
|
||||
|
||||
api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
|
||||
api().post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
|
||||
dispatch(dismissAnnouncementSuccess(announcementId));
|
||||
}).catch(error => {
|
||||
dispatch(dismissAnnouncementFail(announcementId, error));
|
||||
|
|
@ -103,7 +103,7 @@ export const addReaction = (announcementId, name) => (dispatch, getState) => {
|
|||
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
|
||||
}
|
||||
|
||||
api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
|
||||
api().put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
|
||||
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
|
||||
}).catch(err => {
|
||||
if (!alreadyAdded) {
|
||||
|
|
@ -134,10 +134,10 @@ export const addReactionFail = (announcementId, name, error) => ({
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const removeReaction = (announcementId, name) => (dispatch, getState) => {
|
||||
export const removeReaction = (announcementId, name) => (dispatch) => {
|
||||
dispatch(removeReactionRequest(announcementId, name));
|
||||
|
||||
api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
|
||||
api().delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
|
||||
dispatch(removeReactionSuccess(announcementId, name));
|
||||
}).catch(err => {
|
||||
dispatch(removeReactionFail(announcementId, name, err));
|
||||
|
|
|
|||
|
|
@ -12,13 +12,11 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
|
|||
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
||||
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
||||
|
||||
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
|
||||
|
||||
export function fetchBlocks() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchBlocksRequest());
|
||||
|
||||
api(getState).get('/api/v1/blocks').then(response => {
|
||||
api().get('/api/v1/blocks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -58,7 +56,7 @@ export function expandBlocks() {
|
|||
|
||||
dispatch(expandBlocksRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
|
|||
|
||||
export function initBlockModal(account) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: BLOCKS_INIT_MODAL,
|
||||
account,
|
||||
});
|
||||
|
||||
dispatch(openModal({ modalType: 'BLOCK' }));
|
||||
dispatch(openModal({
|
||||
modalType: 'BLOCK',
|
||||
modalProps: {
|
||||
accountId: account.get('id'),
|
||||
acct: account.get('acct'),
|
||||
},
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export function fetchBookmarkedStatuses() {
|
|||
|
||||
dispatch(fetchBookmarkedStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/bookmarks').then(response => {
|
||||
api().get('/api/v1/bookmarks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -59,7 +59,7 @@ export function expandBookmarkedStatuses() {
|
|||
|
||||
dispatch(expandBookmarkedStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import { openModal } from './modal';
|
||||
|
||||
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
|
||||
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
|
||||
|
||||
export function initBoostModal(props) {
|
||||
return (dispatch, getState) => {
|
||||
const default_privacy = getState().getIn(['compose', 'default_privacy']);
|
||||
|
||||
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
|
||||
|
||||
dispatch({
|
||||
type: BOOSTS_INIT_MODAL,
|
||||
privacy,
|
||||
});
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'BOOST',
|
||||
modalProps: props,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function changeBoostPrivacy(privacy) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: BOOSTS_CHANGE_PRIVACY,
|
||||
privacy,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
|||
import { throttle } from 'lodash';
|
||||
|
||||
import api from 'mastodon/api';
|
||||
import { browserHistory } from 'mastodon/components/router';
|
||||
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
|
||||
import { tagHistory } from 'mastodon/settings';
|
||||
|
||||
|
|
@ -75,6 +76,7 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
|
|||
|
||||
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
||||
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||
export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER';
|
||||
|
||||
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
||||
|
|
@ -87,9 +89,9 @@ const messages = defineMessages({
|
|||
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||
});
|
||||
|
||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||
export const ensureComposeIsVisible = (getState) => {
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
routerHistory.push('/publish');
|
||||
browserHistory.push('/publish');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -109,14 +111,26 @@ export function changeCompose(text) {
|
|||
};
|
||||
}
|
||||
|
||||
export function replyCompose(status, routerHistory) {
|
||||
export function replyCompose(status) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_REPLY,
|
||||
status: status,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function replyComposeById(statusId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const status = state.statuses.get(statusId);
|
||||
|
||||
if (status) {
|
||||
const account = state.accounts.get(status.get('account'));
|
||||
dispatch(replyCompose(status.set('account', account)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -132,38 +146,44 @@ export function resetCompose() {
|
|||
};
|
||||
}
|
||||
|
||||
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
|
||||
export const focusCompose = (defaultText) => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_FOCUS,
|
||||
defaultText,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
};
|
||||
|
||||
export function mentionCompose(account, routerHistory) {
|
||||
export function mentionCompose(account) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_MENTION,
|
||||
account: account,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function directCompose(account, routerHistory) {
|
||||
export function mentionComposeById(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(mentionCompose(getState().accounts.get(accountId)));
|
||||
};
|
||||
}
|
||||
|
||||
export function directCompose(account) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_DIRECT,
|
||||
account: account,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function submitCompose(routerHistory) {
|
||||
export function submitCompose() {
|
||||
return function (dispatch, getState) {
|
||||
const status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
|
|
@ -195,7 +215,7 @@ export function submitCompose(routerHistory) {
|
|||
});
|
||||
}
|
||||
|
||||
api(getState).request({
|
||||
api().request({
|
||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||
method: statusId === null ? 'post' : 'put',
|
||||
data: {
|
||||
|
|
@ -213,8 +233,8 @@ export function submitCompose(routerHistory) {
|
|||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
}).then(function (response) {
|
||||
if (routerHistory && (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new') && window.history.state) {
|
||||
routerHistory.goBack();
|
||||
if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new') && window.history.state) {
|
||||
browserHistory.goBack();
|
||||
}
|
||||
|
||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||
|
|
@ -248,7 +268,7 @@ export function submitCompose(routerHistory) {
|
|||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
dismissAfter: 10000,
|
||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||
onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||
}));
|
||||
}).catch(function (error) {
|
||||
dispatch(submitComposeFail(error));
|
||||
|
|
@ -278,7 +298,7 @@ export function submitComposeFail(error) {
|
|||
|
||||
export function uploadCompose(files) {
|
||||
return function (dispatch, getState) {
|
||||
const uploadLimit = 4;
|
||||
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||
const progress = new Array(files.length).fill(0);
|
||||
|
|
@ -298,12 +318,12 @@ export function uploadCompose(files) {
|
|||
dispatch(uploadComposeRequest());
|
||||
|
||||
for (const [i, file] of Array.from(files).entries()) {
|
||||
if (media.size + i > 3) break;
|
||||
if (media.size + i > (uploadLimit - 1)) break;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
|
||||
api(getState).post('/api/v2/media', data, {
|
||||
api().post('/api/v2/media', data, {
|
||||
onUploadProgress: function({ loaded }){
|
||||
progress[i] = loaded;
|
||||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||
|
|
@ -320,7 +340,7 @@ export function uploadCompose(files) {
|
|||
let tryCount = 1;
|
||||
|
||||
const poll = () => {
|
||||
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
|
||||
api().get(`/api/v1/media/${data.id}`).then(response => {
|
||||
if (response.status === 200) {
|
||||
dispatch(uploadComposeSuccess(response.data, file));
|
||||
} else if (response.status === 206) {
|
||||
|
|
@ -342,7 +362,7 @@ export const uploadComposeProcessing = () => ({
|
|||
type: COMPOSE_UPLOAD_PROCESSING,
|
||||
});
|
||||
|
||||
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
|
||||
export const uploadThumbnail = (id, file) => (dispatch) => {
|
||||
dispatch(uploadThumbnailRequest());
|
||||
|
||||
const total = file.size;
|
||||
|
|
@ -350,7 +370,7 @@ export const uploadThumbnail = (id, file) => (dispatch, getState) => {
|
|||
|
||||
data.append('thumbnail', file);
|
||||
|
||||
api(getState).put(`/api/v1/media/${id}`, data, {
|
||||
api().put(`/api/v1/media/${id}`, data, {
|
||||
onUploadProgress: ({ loaded }) => {
|
||||
dispatch(uploadThumbnailProgress(loaded, total));
|
||||
},
|
||||
|
|
@ -433,7 +453,7 @@ export function changeUploadCompose(id, params) {
|
|||
|
||||
dispatch(changeUploadComposeSuccess(data, true));
|
||||
} else {
|
||||
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
|
||||
api().put(`/api/v1/media/${id}`, params).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(response.data, false));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(id, error));
|
||||
|
|
@ -521,7 +541,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
|
|||
|
||||
fetchComposeSuggestionsAccountsController = new AbortController();
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', {
|
||||
api().get('/api/v1/accounts/search', {
|
||||
signal: fetchComposeSuggestionsAccountsController.signal,
|
||||
|
||||
params: {
|
||||
|
|
@ -555,7 +575,7 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
|||
|
||||
fetchComposeSuggestionsTagsController = new AbortController();
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
api().get('/api/v2/search', {
|
||||
signal: fetchComposeSuggestionsTagsController.signal,
|
||||
|
||||
params: {
|
||||
|
|
@ -786,11 +806,12 @@ export function addPollOption(title) {
|
|||
};
|
||||
}
|
||||
|
||||
export function changePollOption(index, title) {
|
||||
export function changePollOption(index, title, maxOptions) {
|
||||
return {
|
||||
type: COMPOSE_POLL_OPTION_CHANGE,
|
||||
index,
|
||||
title,
|
||||
maxOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -808,3 +829,9 @@ export function changePollSettings(expiresIn, isMultiple) {
|
|||
isMultiple,
|
||||
};
|
||||
}
|
||||
|
||||
export const changeMediaOrder = (a, b) => ({
|
||||
type: COMPOSE_CHANGE_MEDIA_ORDER,
|
||||
a,
|
||||
b,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ export const unmountConversations = () => ({
|
|||
type: CONVERSATIONS_UNMOUNT,
|
||||
});
|
||||
|
||||
export const markConversationRead = conversationId => (dispatch, getState) => {
|
||||
export const markConversationRead = conversationId => (dispatch) => {
|
||||
dispatch({
|
||||
type: CONVERSATIONS_READ,
|
||||
id: conversationId,
|
||||
});
|
||||
|
||||
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
|
||||
api().post(`/api/v1/conversations/${conversationId}/read`);
|
||||
};
|
||||
|
||||
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
|
||||
|
|
@ -48,7 +48,7 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
|
|||
|
||||
const isLoadingRecent = !!params.since_id;
|
||||
|
||||
api(getState).get('/api/v1/conversations', { params })
|
||||
api().get('/api/v1/conversations', { params })
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
|
|
@ -88,10 +88,10 @@ export const updateConversations = conversation => dispatch => {
|
|||
});
|
||||
};
|
||||
|
||||
export const deleteConversation = conversationId => (dispatch, getState) => {
|
||||
export const deleteConversation = conversationId => (dispatch) => {
|
||||
dispatch(deleteConversationRequest(conversationId));
|
||||
|
||||
api(getState).delete(`/api/v1/conversations/${conversationId}`)
|
||||
api().delete(`/api/v1/conversations/${conversationId}`)
|
||||
.then(() => dispatch(deleteConversationSuccess(conversationId)))
|
||||
.catch(error => dispatch(deleteConversationFail(conversationId, error)));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
|
|||
export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
|
||||
|
||||
export function fetchCustomEmojis() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchCustomEmojisRequest());
|
||||
|
||||
api(getState).get('/api/v1/custom_emojis').then(response => {
|
||||
api().get('/api/v1/custom_emojis').then(response => {
|
||||
dispatch(fetchCustomEmojisSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchCustomEmojisFail(error));
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
|
||||
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
|
||||
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
|
||||
|
||||
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
|
||||
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
|
||||
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
|
||||
|
||||
export const fetchDirectory = params => (dispatch, getState) => {
|
||||
dispatch(fetchDirectoryRequest());
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||
}).catch(error => dispatch(fetchDirectoryFail(error)));
|
||||
};
|
||||
|
||||
export const fetchDirectoryRequest = () => ({
|
||||
type: DIRECTORY_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchDirectorySuccess = accounts => ({
|
||||
type: DIRECTORY_FETCH_SUCCESS,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const fetchDirectoryFail = error => ({
|
||||
type: DIRECTORY_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const expandDirectory = params => (dispatch, getState) => {
|
||||
dispatch(expandDirectoryRequest());
|
||||
|
||||
const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||
}).catch(error => dispatch(expandDirectoryFail(error)));
|
||||
};
|
||||
|
||||
export const expandDirectoryRequest = () => ({
|
||||
type: DIRECTORY_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
export const expandDirectorySuccess = accounts => ({
|
||||
type: DIRECTORY_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const expandDirectoryFail = error => ({
|
||||
type: DIRECTORY_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
||||
37
app/javascript/mastodon/actions/directory.ts
Normal file
37
app/javascript/mastodon/actions/directory.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { apiGetDirectory } from 'mastodon/api/directory';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
export const fetchDirectory = createDataLoadingThunk(
|
||||
'directory/fetch',
|
||||
async (params: Parameters<typeof apiGetDirectory>[0]) =>
|
||||
apiGetDirectory(params),
|
||||
(data, { dispatch }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchRelationships(data.map((x) => x.id)));
|
||||
|
||||
return { accounts: data };
|
||||
},
|
||||
);
|
||||
|
||||
export const expandDirectory = createDataLoadingThunk(
|
||||
'directory/expand',
|
||||
async (params: Parameters<typeof apiGetDirectory>[0], { getState }) => {
|
||||
const loadedItems = getState().user_lists.getIn([
|
||||
'directory',
|
||||
'items',
|
||||
]) as ImmutableList<unknown>;
|
||||
|
||||
return apiGetDirectory({ ...params, offset: loadedItems.size }, 20);
|
||||
},
|
||||
(data, { dispatch }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchRelationships(data.map((x) => x.id)));
|
||||
|
||||
return { accounts: data };
|
||||
},
|
||||
);
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import api, { getLinks } from '../api';
|
||||
|
||||
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
|
||||
import { openModal } from './modal';
|
||||
|
||||
|
||||
export * from "./domain_blocks_typed";
|
||||
|
||||
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
|
||||
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
|
||||
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
|
||||
|
||||
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
|
||||
export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS';
|
||||
export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
|
||||
|
||||
export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
|
||||
|
|
@ -20,11 +24,11 @@ export function blockDomain(domain) {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(blockDomainRequest(domain));
|
||||
|
||||
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
|
||||
api().post('/api/v1/domain_blocks', { domain }).then(() => {
|
||||
const at_domain = '@' + domain;
|
||||
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
|
||||
|
||||
dispatch(blockDomainSuccess(domain, accounts));
|
||||
dispatch(blockDomainSuccess({ domain, accounts }));
|
||||
}).catch(err => {
|
||||
dispatch(blockDomainFail(domain, err));
|
||||
});
|
||||
|
|
@ -38,14 +42,6 @@ export function blockDomainRequest(domain) {
|
|||
};
|
||||
}
|
||||
|
||||
export function blockDomainSuccess(domain, accounts) {
|
||||
return {
|
||||
type: DOMAIN_BLOCK_SUCCESS,
|
||||
domain,
|
||||
accounts,
|
||||
};
|
||||
}
|
||||
|
||||
export function blockDomainFail(domain, error) {
|
||||
return {
|
||||
type: DOMAIN_BLOCK_FAIL,
|
||||
|
|
@ -58,10 +54,10 @@ export function unblockDomain(domain) {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(unblockDomainRequest(domain));
|
||||
|
||||
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
|
||||
api().delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
|
||||
const at_domain = '@' + domain;
|
||||
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
|
||||
dispatch(unblockDomainSuccess(domain, accounts));
|
||||
dispatch(unblockDomainSuccess({ domain, accounts }));
|
||||
}).catch(err => {
|
||||
dispatch(unblockDomainFail(domain, err));
|
||||
});
|
||||
|
|
@ -75,14 +71,6 @@ export function unblockDomainRequest(domain) {
|
|||
};
|
||||
}
|
||||
|
||||
export function unblockDomainSuccess(domain, accounts) {
|
||||
return {
|
||||
type: DOMAIN_UNBLOCK_SUCCESS,
|
||||
domain,
|
||||
accounts,
|
||||
};
|
||||
}
|
||||
|
||||
export function unblockDomainFail(domain, error) {
|
||||
return {
|
||||
type: DOMAIN_UNBLOCK_FAIL,
|
||||
|
|
@ -92,10 +80,10 @@ export function unblockDomainFail(domain, error) {
|
|||
}
|
||||
|
||||
export function fetchDomainBlocks() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchDomainBlocksRequest());
|
||||
|
||||
api(getState).get('/api/v1/domain_blocks').then(response => {
|
||||
api().get('/api/v1/domain_blocks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(err => {
|
||||
|
|
@ -135,7 +123,7 @@ export function expandDomainBlocks() {
|
|||
|
||||
dispatch(expandDomainBlocksRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(err => {
|
||||
|
|
@ -164,3 +152,12 @@ export function expandDomainBlocksFail(error) {
|
|||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
|
||||
modalType: 'DOMAIN_BLOCK',
|
||||
modalProps: {
|
||||
domain: account.get('acct').split('@')[1],
|
||||
acct: account.get('acct'),
|
||||
accountId: account.get('id'),
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
13
app/javascript/mastodon/actions/domain_blocks_typed.ts
Normal file
13
app/javascript/mastodon/actions/domain_blocks_typed.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
export const blockDomainSuccess = createAction<{
|
||||
domain: string;
|
||||
accounts: Account[];
|
||||
}>('domain_blocks/block/SUCCESS');
|
||||
|
||||
export const unblockDomainSuccess = createAction<{
|
||||
domain: string;
|
||||
accounts: Account[];
|
||||
}>('domain_blocks/unblock/SUCCESS');
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
return { type: DROPDOWN_MENU_CLOSE, id };
|
||||
}
|
||||
11
app/javascript/mastodon/actions/dropdown_menu.ts
Normal file
11
app/javascript/mastodon/actions/dropdown_menu.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const openDropdownMenu = createAction<{
|
||||
id: string;
|
||||
keyboard: boolean;
|
||||
scrollKey: string;
|
||||
}>('dropdownMenu/open');
|
||||
|
||||
export const closeDropdownMenu = createAction<{ id: string }>(
|
||||
'dropdownMenu/close',
|
||||
);
|
||||
|
|
@ -18,7 +18,7 @@ export function fetchFavouritedStatuses() {
|
|||
|
||||
dispatch(fetchFavouritedStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/favourites').then(response => {
|
||||
api().get('/api/v1/favourites').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -62,7 +62,7 @@ export function expandFavouritedStatuses() {
|
|||
|
||||
dispatch(expandFavouritedStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const fetchFeaturedTags = (id) => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchFeaturedTagsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
|
||||
api().get(`/api/v1/accounts/${id}/featured_tags`)
|
||||
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
|
||||
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ export const initAddFilter = (status, { contextType }) => dispatch =>
|
|||
},
|
||||
}));
|
||||
|
||||
export const fetchFilters = () => (dispatch, getState) => {
|
||||
export const fetchFilters = () => (dispatch) => {
|
||||
dispatch({
|
||||
type: FILTERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.get('/api/v2/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
|
|
@ -44,10 +44,10 @@ export const fetchFilters = () => (dispatch, getState) => {
|
|||
}));
|
||||
};
|
||||
|
||||
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch) => {
|
||||
dispatch(createFilterStatusRequest());
|
||||
|
||||
api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
|
||||
api().post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
|
||||
dispatch(createFilterStatusSuccess(response.data));
|
||||
if (onSuccess) onSuccess();
|
||||
}).catch(error => {
|
||||
|
|
@ -70,10 +70,10 @@ export const createFilterStatusFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const createFilter = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||
export const createFilter = (params, onSuccess, onFail) => (dispatch) => {
|
||||
dispatch(createFilterRequest());
|
||||
|
||||
api(getState).post('/api/v2/filters', params).then(response => {
|
||||
api().post('/api/v2/filters', params).then(response => {
|
||||
dispatch(createFilterSuccess(response.data));
|
||||
if (onSuccess) onSuccess(response.data);
|
||||
}).catch(error => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const fetchHistory = statusId => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchHistoryRequest(statusId));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
|
||||
api().get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data.map(x => x.account)));
|
||||
dispatch(fetchHistorySuccess(statusId, data));
|
||||
}).catch(error => dispatch(fetchHistoryFail(error)));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer';
|
||||
import { importAccounts } from '../accounts_typed';
|
||||
|
||||
import { normalizeStatus, normalizePoll } from './normalizer';
|
||||
|
||||
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
||||
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
||||
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
||||
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
||||
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
||||
|
|
@ -13,14 +13,6 @@ function pushUnique(array, object) {
|
|||
}
|
||||
}
|
||||
|
||||
export function importAccount(account) {
|
||||
return { type: ACCOUNT_IMPORT, account };
|
||||
}
|
||||
|
||||
export function importAccounts(accounts) {
|
||||
return { type: ACCOUNTS_IMPORT, accounts };
|
||||
}
|
||||
|
||||
export function importStatus(status) {
|
||||
return { type: STATUS_IMPORT, status };
|
||||
}
|
||||
|
|
@ -45,7 +37,7 @@ export function importFetchedAccounts(accounts) {
|
|||
const normalAccounts = [];
|
||||
|
||||
function processAccount(account) {
|
||||
pushUnique(normalAccounts, normalizeAccount(account));
|
||||
pushUnique(normalAccounts, account);
|
||||
|
||||
if (account.moved) {
|
||||
processAccount(account.moved);
|
||||
|
|
@ -54,7 +46,7 @@ export function importFetchedAccounts(accounts) {
|
|||
|
||||
accounts.forEach(processAccount);
|
||||
|
||||
return importAccounts(normalAccounts);
|
||||
return importAccounts({ accounts: normalAccounts });
|
||||
}
|
||||
|
||||
export function importFetchedStatus(status) {
|
||||
|
|
@ -76,13 +68,17 @@ export function importFetchedStatuses(statuses) {
|
|||
status.filtered.forEach(result => pushUnique(filters, result.filter));
|
||||
}
|
||||
|
||||
if (status.reblog && status.reblog.id) {
|
||||
if (status.reblog?.id) {
|
||||
processStatus(status.reblog);
|
||||
}
|
||||
|
||||
if (status.poll && status.poll.id) {
|
||||
if (status.poll?.id) {
|
||||
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
|
||||
}
|
||||
|
||||
if (status.card) {
|
||||
status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
|
||||
}
|
||||
}
|
||||
|
||||
statuses.forEach(processStatus);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import escapeTextContentForBrowser from 'escape-html';
|
|||
|
||||
import emojify from '../../features/emoji/emoji';
|
||||
import { expandSpoilers } from '../../initial_state';
|
||||
import { unescapeHTML } from '../../utils/html';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
|
|
@ -17,32 +16,6 @@ export function searchTextFromRawStatus (status) {
|
|||
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||
}
|
||||
|
||||
export function normalizeAccount(account) {
|
||||
account = { ...account };
|
||||
|
||||
const emojiMap = makeEmojiMap(account.emojis);
|
||||
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
|
||||
|
||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
||||
account.note_emojified = emojify(account.note, emojiMap);
|
||||
account.note_plain = unescapeHTML(account.note);
|
||||
|
||||
if (account.fields) {
|
||||
account.fields = account.fields.map(pair => ({
|
||||
...pair,
|
||||
name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap),
|
||||
value_emojified: emojify(pair.value, emojiMap),
|
||||
value_plain: unescapeHTML(pair.value),
|
||||
}));
|
||||
}
|
||||
|
||||
if (account.moved) {
|
||||
account.moved = account.moved.id;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
export function normalizeFilterResult(result) {
|
||||
const normalResult = { ...result };
|
||||
|
||||
|
|
@ -63,6 +36,17 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||
normalStatus.poll = status.poll.id;
|
||||
}
|
||||
|
||||
if (status.card) {
|
||||
normalStatus.card = {
|
||||
...status.card,
|
||||
authors: status.card.authors.map(author => ({
|
||||
...author,
|
||||
accountId: author.account?.id,
|
||||
account: undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (status.filtered) {
|
||||
normalStatus.filtered = status.filtered.map(normalizeFilterResult);
|
||||
}
|
||||
|
|
@ -104,7 +88,7 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||
normalStatus.media_attachments.forEach(item => {
|
||||
const oldItem = list.find(i => i.get('id') === item.id);
|
||||
if (oldItem && oldItem.get('description') === item.description) {
|
||||
item.translation = oldItem.get('translation')
|
||||
item.translation = oldItem.get('translation');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -137,13 +121,13 @@ export function normalizePoll(poll, normalOldPoll) {
|
|||
...option,
|
||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||
}
|
||||
};
|
||||
|
||||
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
|
||||
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
|
||||
}
|
||||
|
||||
return normalOption
|
||||
return normalOption;
|
||||
});
|
||||
|
||||
return normalPoll;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { boostModal } from 'mastodon/initial_state';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
|
||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||
import { unreblog, reblog } from './interactions_typed';
|
||||
import { openModal } from './modal';
|
||||
|
||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
|
|
@ -15,10 +15,6 @@ export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
|||
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||
|
||||
export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
|
||||
export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
|
||||
export const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
|
||||
|
||||
export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
||||
export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||
export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||
|
|
@ -51,89 +47,13 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
|||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||
|
||||
export function reblog(status, visibility) {
|
||||
return function (dispatch, getState) {
|
||||
dispatch(reblogRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).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));
|
||||
dispatch(reblogSuccess(status));
|
||||
}).catch(function (error) {
|
||||
dispatch(reblogFail(status, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblog(status) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(unreblogRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unreblogSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(unreblogFail(status, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function reblogRequest(status) {
|
||||
return {
|
||||
type: REBLOG_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function reblogSuccess(status) {
|
||||
return {
|
||||
type: REBLOG_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function reblogFail(status, error) {
|
||||
return {
|
||||
type: REBLOG_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblogRequest(status) {
|
||||
return {
|
||||
type: UNREBLOG_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblogSuccess(status) {
|
||||
return {
|
||||
type: UNREBLOG_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblogFail(status, error) {
|
||||
return {
|
||||
type: UNREBLOG_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
export * from "./interactions_typed";
|
||||
|
||||
export function favourite(status) {
|
||||
return function (dispatch, getState) {
|
||||
return function (dispatch) {
|
||||
dispatch(favouriteRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(favouriteSuccess(status));
|
||||
}).catch(function (error) {
|
||||
|
|
@ -143,10 +63,10 @@ export function favourite(status) {
|
|||
}
|
||||
|
||||
export function unfavourite(status) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unfavouriteRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unfavouriteSuccess(status));
|
||||
}).catch(error => {
|
||||
|
|
@ -206,10 +126,10 @@ export function unfavouriteFail(status, error) {
|
|||
}
|
||||
|
||||
export function bookmark(status) {
|
||||
return function (dispatch, getState) {
|
||||
return function (dispatch) {
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(bookmarkSuccess(status, response.data));
|
||||
}).catch(function (error) {
|
||||
|
|
@ -219,10 +139,10 @@ export function bookmark(status) {
|
|||
}
|
||||
|
||||
export function unbookmark(status) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unbookmarkRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unbookmarkSuccess(status, response.data));
|
||||
}).catch(error => {
|
||||
|
|
@ -278,10 +198,10 @@ export function unbookmarkFail(status, error) {
|
|||
}
|
||||
|
||||
export function fetchReblogs(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchReblogsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||
api().get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
|
|
@ -325,7 +245,7 @@ export function expandReblogs(id) {
|
|||
|
||||
dispatch(expandReblogsRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
|
|
@ -360,10 +280,10 @@ export function expandReblogsFail(id, error) {
|
|||
}
|
||||
|
||||
export function fetchFavourites(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchFavouritesRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||
api().get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
|
|
@ -407,7 +327,7 @@ export function expandFavourites(id) {
|
|||
|
||||
dispatch(expandFavouritesRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
|
|
@ -442,10 +362,10 @@ export function expandFavouritesFail(id, error) {
|
|||
}
|
||||
|
||||
export function pin(status) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(pinRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(pinSuccess(status));
|
||||
}).catch(error => {
|
||||
|
|
@ -480,10 +400,10 @@ export function pinFail(status, error) {
|
|||
}
|
||||
|
||||
export function unpin (status) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unpinRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unpinSuccess(status));
|
||||
}).catch(error => {
|
||||
|
|
@ -516,3 +436,49 @@ export function unpinFail(status, error) {
|
|||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleReblogWithoutConfirmation(status, visibility) {
|
||||
return (dispatch) => {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else {
|
||||
dispatch(reblog({ statusId: status.get('id'), visibility }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleReblog(statusId, skipModal = false) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
let status = state.statuses.get(statusId);
|
||||
|
||||
if (!status)
|
||||
return;
|
||||
|
||||
// The reblog modal expects a pre-filled account in status
|
||||
// TODO: fix this by having the reblog modal get a statusId and do the work itself
|
||||
status = status.set('account', state.accounts.get(status.get('account')));
|
||||
|
||||
if (boostModal && !skipModal) {
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)) } }));
|
||||
} else {
|
||||
dispatch(toggleReblogWithoutConfirmation(status));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleFavourite(statusId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const status = state.statuses.get(statusId);
|
||||
|
||||
if (!status)
|
||||
return;
|
||||
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
35
app/javascript/mastodon/actions/interactions_typed.ts
Normal file
35
app/javascript/mastodon/actions/interactions_typed.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { apiReblog, apiUnreblog } from 'mastodon/api/interactions';
|
||||
import type { StatusVisibility } from 'mastodon/models/status';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import { importFetchedStatus } from './importer';
|
||||
|
||||
export const reblog = createDataLoadingThunk(
|
||||
'status/reblog',
|
||||
({
|
||||
statusId,
|
||||
visibility,
|
||||
}: {
|
||||
statusId: string;
|
||||
visibility: StatusVisibility;
|
||||
}) => apiReblog(statusId, visibility),
|
||||
(data, { dispatch, discardLoadData }) => {
|
||||
// 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(data.reblog));
|
||||
|
||||
// The payload is not used in any actions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
||||
|
||||
export const unreblog = createDataLoadingThunk(
|
||||
'status/unreblog',
|
||||
({ statusId }: { statusId: string }) => apiUnreblog(statusId),
|
||||
(data, { dispatch, discardLoadData }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
|
||||
// The payload is not used in any actions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { saveSettings } from './settings';
|
||||
|
||||
export const LANGUAGE_USE = 'LANGUAGE_USE';
|
||||
|
||||
export const useLanguage = language => dispatch => {
|
||||
dispatch({
|
||||
type: LANGUAGE_USE,
|
||||
language,
|
||||
});
|
||||
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
|
|
@ -57,7 +57,7 @@ export const fetchList = id => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchListRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/lists/${id}`)
|
||||
api().get(`/api/v1/lists/${id}`)
|
||||
.then(({ data }) => dispatch(fetchListSuccess(data)))
|
||||
.catch(err => dispatch(fetchListFail(id, err)));
|
||||
};
|
||||
|
|
@ -78,10 +78,10 @@ export const fetchListFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchLists = () => (dispatch, getState) => {
|
||||
export const fetchLists = () => (dispatch) => {
|
||||
dispatch(fetchListsRequest());
|
||||
|
||||
api(getState).get('/api/v1/lists')
|
||||
api().get('/api/v1/lists')
|
||||
.then(({ data }) => dispatch(fetchListsSuccess(data)))
|
||||
.catch(err => dispatch(fetchListsFail(err)));
|
||||
};
|
||||
|
|
@ -125,10 +125,10 @@ export const changeListEditorTitle = value => ({
|
|||
value,
|
||||
});
|
||||
|
||||
export const createList = (title, shouldReset) => (dispatch, getState) => {
|
||||
export const createList = (title, shouldReset) => (dispatch) => {
|
||||
dispatch(createListRequest());
|
||||
|
||||
api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
|
||||
api().post('/api/v1/lists', { title }).then(({ data }) => {
|
||||
dispatch(createListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
|
|
@ -151,10 +151,10 @@ export const createListFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
|
||||
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch) => {
|
||||
dispatch(updateListRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
||||
api().put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
||||
dispatch(updateListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
|
|
@ -183,10 +183,10 @@ export const resetListEditor = () => ({
|
|||
type: LIST_EDITOR_RESET,
|
||||
});
|
||||
|
||||
export const deleteList = id => (dispatch, getState) => {
|
||||
export const deleteList = id => (dispatch) => {
|
||||
dispatch(deleteListRequest(id));
|
||||
|
||||
api(getState).delete(`/api/v1/lists/${id}`)
|
||||
api().delete(`/api/v1/lists/${id}`)
|
||||
.then(() => dispatch(deleteListSuccess(id)))
|
||||
.catch(err => dispatch(deleteListFail(id, err)));
|
||||
};
|
||||
|
|
@ -207,10 +207,10 @@ export const deleteListFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchListAccounts = listId => (dispatch, getState) => {
|
||||
export const fetchListAccounts = listId => (dispatch) => {
|
||||
dispatch(fetchListAccountsRequest(listId));
|
||||
|
||||
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
api().get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListAccountsSuccess(listId, data));
|
||||
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
||||
|
|
@ -234,7 +234,7 @@ export const fetchListAccountsFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchListSuggestions = q => (dispatch, getState) => {
|
||||
export const fetchListSuggestions = q => (dispatch) => {
|
||||
const params = {
|
||||
q,
|
||||
resolve: false,
|
||||
|
|
@ -242,7 +242,7 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
|
|||
following: true,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
api().get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListSuggestionsReady(q, data));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
|
|
@ -267,10 +267,10 @@ export const addToListEditor = accountId => (dispatch, getState) => {
|
|||
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const addToList = (listId, accountId) => (dispatch, getState) => {
|
||||
export const addToList = (listId, accountId) => (dispatch) => {
|
||||
dispatch(addToListRequest(listId, accountId));
|
||||
|
||||
api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
|
||||
api().post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
|
||||
.then(() => dispatch(addToListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(addToListFail(listId, accountId, err)));
|
||||
};
|
||||
|
|
@ -298,10 +298,10 @@ export const removeFromListEditor = accountId => (dispatch, getState) => {
|
|||
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const removeFromList = (listId, accountId) => (dispatch, getState) => {
|
||||
export const removeFromList = (listId, accountId) => (dispatch) => {
|
||||
dispatch(removeFromListRequest(listId, accountId));
|
||||
|
||||
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
api().delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
|
||||
};
|
||||
|
|
@ -338,10 +338,10 @@ export const setupListAdder = accountId => (dispatch, getState) => {
|
|||
dispatch(fetchAccountLists(accountId));
|
||||
};
|
||||
|
||||
export const fetchAccountLists = accountId => (dispatch, getState) => {
|
||||
export const fetchAccountLists = accountId => (dispatch) => {
|
||||
dispatch(fetchAccountListsRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/lists`)
|
||||
api().get(`/api/v1/accounts/${accountId}/lists`)
|
||||
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
|
||||
};
|
||||
|
|
@ -370,4 +370,3 @@ export const addToListAdder = listId => (dispatch, getState) => {
|
|||
export const removeFromListAdder = listId => (dispatch, getState) => {
|
||||
dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,152 +0,0 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import api from '../api';
|
||||
import { compareId } from '../compare_id';
|
||||
|
||||
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
|
||||
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
|
||||
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
|
||||
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
|
||||
|
||||
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
||||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// The Fetch API allows us to perform requests that will be carried out
|
||||
// after the page closes. But that only works if the `keepalive` attribute
|
||||
// is supported.
|
||||
if (window.fetch && 'keepalive' in new Request('')) {
|
||||
fetch('/api/v1/markers', {
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (navigator && navigator.sendBeacon) {
|
||||
// Failing that, we can use sendBeacon, but we have to encode the data as
|
||||
// FormData for DoorKeeper to recognize the token.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('bearer_token', accessToken);
|
||||
|
||||
for (const [id, value] of Object.entries(params)) {
|
||||
formData.append(`${id}[last_read_id]`, value.last_read_id);
|
||||
}
|
||||
|
||||
if (navigator.sendBeacon('/api/v1/markers', formData)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
|
||||
// request.
|
||||
try {
|
||||
const client = new XMLHttpRequest();
|
||||
|
||||
client.open('POST', '/api/v1/markers', false);
|
||||
client.setRequestHeader('Content-Type', 'application/json');
|
||||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
client.send(JSON.stringify(params));
|
||||
} catch (e) {
|
||||
// Do not make the BeforeUnload handler error out
|
||||
}
|
||||
};
|
||||
|
||||
const _buildParams = (state) => {
|
||||
const params = {};
|
||||
|
||||
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
|
||||
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
|
||||
|
||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||
params.home = {
|
||||
last_read_id: lastHomeId,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
|
||||
params.notifications = {
|
||||
last_read_id: lastNotificationId,
|
||||
};
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
|
||||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
api(getState).post('/api/v1/markers', params).then(() => {
|
||||
dispatch(submitMarkersSuccess(params));
|
||||
}).catch(() => {});
|
||||
}, 300000, { leading: true, trailing: true });
|
||||
|
||||
export function submitMarkersSuccess({ home, notifications }) {
|
||||
return {
|
||||
type: MARKERS_SUBMIT_SUCCESS,
|
||||
home: (home || {}).last_read_id,
|
||||
notifications: (notifications || {}).last_read_id,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitMarkers(params = {}) {
|
||||
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
|
||||
|
||||
if (params.immediate === true) {
|
||||
debouncedSubmitMarkers.flush();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const fetchMarkers = () => (dispatch, getState) => {
|
||||
const params = { timeline: ['notifications'] };
|
||||
|
||||
dispatch(fetchMarkersRequest());
|
||||
|
||||
api(getState).get('/api/v1/markers', { params }).then(response => {
|
||||
dispatch(fetchMarkersSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchMarkersFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchMarkersRequest() {
|
||||
return {
|
||||
type: MARKERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMarkersSuccess(markers) {
|
||||
return {
|
||||
type: MARKERS_FETCH_SUCCESS,
|
||||
markers,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMarkersFail(error) {
|
||||
return {
|
||||
type: MARKERS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
};
|
||||
}
|
||||
146
app/javascript/mastodon/actions/markers.ts
Normal file
146
app/javascript/mastodon/actions/markers.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { debounce } from 'lodash';
|
||||
|
||||
import type { MarkerJSON } from 'mastodon/api_types/markers';
|
||||
import { getAccessToken } from 'mastodon/initial_state';
|
||||
import type { AppDispatch, RootState } from 'mastodon/store';
|
||||
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import api from '../api';
|
||||
import { compareId } from '../compare_id';
|
||||
|
||||
export const synchronouslySubmitMarkers = createAppAsyncThunk(
|
||||
'markers/submit',
|
||||
async (_args, { getState }) => {
|
||||
const accessToken = getAccessToken();
|
||||
const params = buildPostMarkersParams(getState());
|
||||
|
||||
if (
|
||||
Object.keys(params).length === 0 ||
|
||||
!accessToken ||
|
||||
accessToken === ''
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The Fetch API allows us to perform requests that will be carried out
|
||||
// after the page closes. But that only works if the `keepalive` attribute
|
||||
// is supported.
|
||||
if ('fetch' in window && 'keepalive' in new Request('')) {
|
||||
await fetch('/api/v1/markers', {
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if ('navigator' && 'sendBeacon' in navigator) {
|
||||
// Failing that, we can use sendBeacon, but we have to encode the data as
|
||||
// FormData for DoorKeeper to recognize the token.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('bearer_token', accessToken);
|
||||
|
||||
for (const [id, value] of Object.entries(params)) {
|
||||
if (value.last_read_id)
|
||||
formData.append(`${id}[last_read_id]`, value.last_read_id);
|
||||
}
|
||||
|
||||
if (navigator.sendBeacon('/api/v1/markers', formData)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
|
||||
// request.
|
||||
try {
|
||||
const client = new XMLHttpRequest();
|
||||
|
||||
client.open('POST', '/api/v1/markers', false);
|
||||
client.setRequestHeader('Content-Type', 'application/json');
|
||||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
client.send(JSON.stringify(params));
|
||||
} catch {
|
||||
// Do not make the BeforeUnload handler error out
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
interface MarkerParam {
|
||||
last_read_id?: string;
|
||||
}
|
||||
|
||||
function getLastNotificationId(state: RootState): string | undefined {
|
||||
return state.notificationGroups.lastReadId;
|
||||
}
|
||||
|
||||
const buildPostMarkersParams = (state: RootState) => {
|
||||
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
|
||||
|
||||
const lastNotificationId = getLastNotificationId(state);
|
||||
|
||||
if (
|
||||
lastNotificationId &&
|
||||
compareId(lastNotificationId, state.markers.notifications) > 0
|
||||
) {
|
||||
params.notifications = {
|
||||
last_read_id: lastNotificationId,
|
||||
};
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
export const submitMarkersAction = createAppAsyncThunk<{
|
||||
home: string | undefined;
|
||||
notifications: string | undefined;
|
||||
}>('markers/submitAction', async (_args, { getState }) => {
|
||||
const accessToken = getAccessToken();
|
||||
const params = buildPostMarkersParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || !accessToken || accessToken === '') {
|
||||
return { home: undefined, notifications: undefined };
|
||||
}
|
||||
|
||||
await api().post<MarkerJSON>('/api/v1/markers', params);
|
||||
|
||||
return {
|
||||
home: params.home?.last_read_id,
|
||||
notifications: params.notifications?.last_read_id,
|
||||
};
|
||||
});
|
||||
|
||||
const debouncedSubmitMarkers = debounce(
|
||||
(dispatch: AppDispatch) => {
|
||||
void dispatch(submitMarkersAction());
|
||||
},
|
||||
300000,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
export const submitMarkers = createAppAsyncThunk(
|
||||
'markers/submit',
|
||||
(params: { immediate?: boolean }, { dispatch }) => {
|
||||
debouncedSubmitMarkers(dispatch);
|
||||
|
||||
if (params.immediate) {
|
||||
debouncedSubmitMarkers.flush();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchMarkers = createAppAsyncThunk('markers/fetch', async () => {
|
||||
const response = await api().get<Record<string, MarkerJSON>>(
|
||||
`/api/v1/markers`,
|
||||
{ params: { timeline: ['notifications'] } },
|
||||
);
|
||||
|
||||
return { markers: response.data };
|
||||
});
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { ModalProps } from 'mastodon/reducers/modal';
|
||||
|
||||
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
||||
|
||||
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
||||
|
||||
interface OpenModalPayload {
|
||||
modalType: ModalType;
|
||||
modalProps: unknown;
|
||||
modalProps: ModalProps;
|
||||
}
|
||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,11 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
|
|||
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
||||
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||
|
||||
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
||||
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
||||
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
|
||||
|
||||
export function fetchMutes() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchMutesRequest());
|
||||
|
||||
api(getState).get('/api/v1/mutes').then(response => {
|
||||
api().get('/api/v1/mutes').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -60,7 +56,7 @@ export function expandMutes() {
|
|||
|
||||
dispatch(expandMutesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -92,26 +88,12 @@ export function expandMutesFail(error) {
|
|||
|
||||
export function initMuteModal(account) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: MUTES_INIT_MODAL,
|
||||
account,
|
||||
});
|
||||
|
||||
dispatch(openModal({ modalType: 'MUTE' }));
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleHideNotifications() {
|
||||
return dispatch => {
|
||||
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
|
||||
};
|
||||
}
|
||||
|
||||
export function changeMuteDuration(duration) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: MUTES_CHANGE_DURATION,
|
||||
duration,
|
||||
});
|
||||
dispatch(openModal({
|
||||
modalType: 'MUTE',
|
||||
modalProps: {
|
||||
accountId: account.get('id'),
|
||||
acct: account.get('acct'),
|
||||
},
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
239
app/javascript/mastodon/actions/notification_groups.ts
Normal file
239
app/javascript/mastodon/actions/notification_groups.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
apiClearNotifications,
|
||||
apiFetchNotificationGroups,
|
||||
} from 'mastodon/api/notifications';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type {
|
||||
ApiNotificationGroupJSON,
|
||||
ApiNotificationJSON,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
import { allNotificationTypes } from 'mastodon/api_types/notifications';
|
||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||
import { usePendingItems } from 'mastodon/initial_state';
|
||||
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
|
||||
import {
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsShows,
|
||||
} from 'mastodon/selectors/settings';
|
||||
import type { AppDispatch, RootState } from 'mastodon/store';
|
||||
import {
|
||||
createAppAsyncThunk,
|
||||
createDataLoadingThunk,
|
||||
} from 'mastodon/store/typed_functions';
|
||||
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
import { NOTIFICATIONS_FILTER_SET } from './notifications';
|
||||
import { saveSettings } from './settings';
|
||||
|
||||
function excludeAllTypesExcept(filter: string) {
|
||||
return allNotificationTypes.filter((item) => item !== filter);
|
||||
}
|
||||
|
||||
function getExcludedTypes(state: RootState) {
|
||||
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
|
||||
|
||||
return activeFilter === 'all'
|
||||
? selectSettingsNotificationsExcludedTypes(state)
|
||||
: excludeAllTypesExcept(activeFilter);
|
||||
}
|
||||
|
||||
function dispatchAssociatedRecords(
|
||||
dispatch: AppDispatch,
|
||||
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
|
||||
) {
|
||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
if (notification.type === 'admin.report') {
|
||||
fetchedAccounts.push(notification.report.target_account);
|
||||
}
|
||||
|
||||
if (notification.type === 'moderation_warning') {
|
||||
fetchedAccounts.push(notification.moderation_warning.target_account);
|
||||
}
|
||||
|
||||
if ('status' in notification && notification.status) {
|
||||
fetchedStatuses.push(notification.status);
|
||||
}
|
||||
});
|
||||
|
||||
if (fetchedAccounts.length > 0)
|
||||
dispatch(importFetchedAccounts(fetchedAccounts));
|
||||
|
||||
if (fetchedStatuses.length > 0)
|
||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||
}
|
||||
|
||||
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
||||
|
||||
export const fetchNotifications = createDataLoadingThunk(
|
||||
'notificationGroups/fetch',
|
||||
async (_params, { getState }) =>
|
||||
apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
}),
|
||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(importFetchedStatuses(statuses));
|
||||
dispatchAssociatedRecords(dispatch, notifications);
|
||||
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
|
||||
notifications;
|
||||
|
||||
// TODO: might be worth not using gaps for that…
|
||||
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
|
||||
if (notifications.length > 1)
|
||||
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });
|
||||
|
||||
return payload;
|
||||
// dispatch(submitMarkers());
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchNotificationsGap = createDataLoadingThunk(
|
||||
'notificationGroups/fetchGap',
|
||||
async (params: { gap: NotificationGap }, { getState }) =>
|
||||
apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
max_id: params.gap.maxId,
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
}),
|
||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(importFetchedStatuses(statuses));
|
||||
dispatchAssociatedRecords(dispatch, notifications);
|
||||
|
||||
return { notifications };
|
||||
},
|
||||
);
|
||||
|
||||
export const pollRecentNotifications = createDataLoadingThunk(
|
||||
'notificationGroups/pollRecentNotifications',
|
||||
async (_params, { getState }) => {
|
||||
return apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
max_id: undefined,
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
||||
since_id: usePendingItems
|
||||
? getState().notificationGroups.groups.find(
|
||||
(group) => group.type !== 'gap',
|
||||
)?.page_max_id
|
||||
: undefined,
|
||||
});
|
||||
},
|
||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(importFetchedStatuses(statuses));
|
||||
dispatchAssociatedRecords(dispatch, notifications);
|
||||
|
||||
return { notifications };
|
||||
},
|
||||
);
|
||||
|
||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||
'notificationGroups/processNew',
|
||||
(notification: ApiNotificationJSON, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
|
||||
const notificationShows = selectSettingsNotificationsShows(state);
|
||||
|
||||
const showInColumn =
|
||||
activeFilter === 'all'
|
||||
? notificationShows[notification.type]
|
||||
: activeFilter === notification.type;
|
||||
|
||||
if (!showInColumn) return;
|
||||
|
||||
if (
|
||||
(notification.type === 'mention' || notification.type === 'update') &&
|
||||
notification.status?.filtered
|
||||
) {
|
||||
const filters = notification.status.filtered.filter((result) =>
|
||||
result.filter.context.includes('notifications'),
|
||||
);
|
||||
|
||||
if (filters.some((result) => result.filter.filter_action === 'hide')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatchAssociatedRecords(dispatch, [notification]);
|
||||
|
||||
return notification;
|
||||
},
|
||||
);
|
||||
|
||||
export const loadPending = createAction('notificationGroups/loadPending');
|
||||
|
||||
export const updateScrollPosition = createAppAsyncThunk(
|
||||
'notificationGroups/updateScrollPosition',
|
||||
({ top }: { top: boolean }, { dispatch, getState }) => {
|
||||
if (
|
||||
top &&
|
||||
getState().notificationGroups.mergedNotifications === 'needs-reload'
|
||||
) {
|
||||
void dispatch(fetchNotifications());
|
||||
}
|
||||
|
||||
return { top };
|
||||
},
|
||||
);
|
||||
|
||||
export const setNotificationsFilter = createAppAsyncThunk(
|
||||
'notifications/filter/set',
|
||||
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_FILTER_SET,
|
||||
path: ['notifications', 'quickFilter', 'active'],
|
||||
value: filterType,
|
||||
});
|
||||
void dispatch(fetchNotifications());
|
||||
dispatch(saveSettings());
|
||||
},
|
||||
);
|
||||
|
||||
export const clearNotifications = createDataLoadingThunk(
|
||||
'notifications/clear',
|
||||
() => apiClearNotifications(),
|
||||
);
|
||||
|
||||
export const markNotificationsAsRead = createAction(
|
||||
'notificationGroups/markAsRead',
|
||||
);
|
||||
|
||||
export const mountNotifications = createAppAsyncThunk(
|
||||
'notificationGroups/mount',
|
||||
(_, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
if (
|
||||
state.notificationGroups.mounted === 0 &&
|
||||
state.notificationGroups.mergedNotifications === 'needs-reload'
|
||||
) {
|
||||
void dispatch(fetchNotifications());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const unmountNotifications = createAction('notificationGroups/unmount');
|
||||
|
||||
export const refreshStaleNotificationGroups = createAppAsyncThunk<{
|
||||
deferredRefresh: boolean;
|
||||
}>('notificationGroups/refreshStale', (_, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
if (
|
||||
state.notificationGroups.scrolledToTop ||
|
||||
!state.notificationGroups.mounted
|
||||
) {
|
||||
void dispatch(fetchNotifications());
|
||||
return { deferredRefresh: false };
|
||||
}
|
||||
|
||||
return { deferredRefresh: true };
|
||||
});
|
||||
22
app/javascript/mastodon/actions/notification_policies.ts
Normal file
22
app/javascript/mastodon/actions/notification_policies.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
apiGetNotificationPolicy,
|
||||
apiUpdateNotificationsPolicy,
|
||||
} from 'mastodon/api/notification_policies';
|
||||
import type { NotificationPolicy } from 'mastodon/models/notification_policy';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
export const fetchNotificationPolicy = createDataLoadingThunk(
|
||||
'notificationPolicy/fetch',
|
||||
() => apiGetNotificationPolicy(),
|
||||
);
|
||||
|
||||
export const updateNotificationsPolicy = createDataLoadingThunk(
|
||||
'notificationPolicy/update',
|
||||
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
|
||||
);
|
||||
|
||||
export const decreasePendingRequestsCount = createAction<number>(
|
||||
'notificationPolicy/decreasePendingRequestsCount',
|
||||
);
|
||||
214
app/javascript/mastodon/actions/notification_requests.ts
Normal file
214
app/javascript/mastodon/actions/notification_requests.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import {
|
||||
apiFetchNotificationRequest,
|
||||
apiFetchNotificationRequests,
|
||||
apiFetchNotifications,
|
||||
apiAcceptNotificationRequest,
|
||||
apiDismissNotificationRequest,
|
||||
apiAcceptNotificationRequests,
|
||||
apiDismissNotificationRequests,
|
||||
} from 'mastodon/api/notifications';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type {
|
||||
ApiNotificationGroupJSON,
|
||||
ApiNotificationJSON,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||
import type { AppDispatch } from 'mastodon/store';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
import { decreasePendingRequestsCount } from './notification_policies';
|
||||
|
||||
// TODO: refactor with notification_groups
|
||||
function dispatchAssociatedRecords(
|
||||
dispatch: AppDispatch,
|
||||
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
|
||||
) {
|
||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
if (notification.type === 'admin.report') {
|
||||
fetchedAccounts.push(notification.report.target_account);
|
||||
}
|
||||
|
||||
if (notification.type === 'moderation_warning') {
|
||||
fetchedAccounts.push(notification.moderation_warning.target_account);
|
||||
}
|
||||
|
||||
if ('status' in notification && notification.status) {
|
||||
fetchedStatuses.push(notification.status);
|
||||
}
|
||||
});
|
||||
|
||||
if (fetchedAccounts.length > 0)
|
||||
dispatch(importFetchedAccounts(fetchedAccounts));
|
||||
|
||||
if (fetchedStatuses.length > 0)
|
||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||
}
|
||||
|
||||
export const fetchNotificationRequests = createDataLoadingThunk(
|
||||
'notificationRequests/fetch',
|
||||
async (_params, { getState }) => {
|
||||
let sinceId = undefined;
|
||||
|
||||
if (getState().notificationRequests.items.length > 0) {
|
||||
sinceId = getState().notificationRequests.items[0]?.id;
|
||||
}
|
||||
|
||||
return apiFetchNotificationRequests({
|
||||
since_id: sinceId,
|
||||
});
|
||||
},
|
||||
({ requests, links }, { dispatch }) => {
|
||||
const next = links.refs.find((link) => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
|
||||
|
||||
return { requests, next: next?.uri };
|
||||
},
|
||||
{
|
||||
condition: (_params, { getState }) =>
|
||||
!getState().notificationRequests.isLoading,
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchNotificationRequest = createDataLoadingThunk(
|
||||
'notificationRequest/fetch',
|
||||
async ({ id }: { id: string }) => apiFetchNotificationRequest(id),
|
||||
{
|
||||
condition: ({ id }, { getState }) =>
|
||||
!(
|
||||
getState().notificationRequests.current.item?.id === id ||
|
||||
getState().notificationRequests.current.isLoading
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
export const expandNotificationRequests = createDataLoadingThunk(
|
||||
'notificationRequests/expand',
|
||||
async (_, { getState }) => {
|
||||
const nextUrl = getState().notificationRequests.next;
|
||||
if (!nextUrl) throw new Error('missing URL');
|
||||
|
||||
return apiFetchNotificationRequests(undefined, nextUrl);
|
||||
},
|
||||
({ requests, links }, { dispatch }) => {
|
||||
const next = links.refs.find((link) => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
|
||||
|
||||
return { requests, next: next?.uri };
|
||||
},
|
||||
{
|
||||
condition: (_, { getState }) =>
|
||||
!!getState().notificationRequests.next &&
|
||||
!getState().notificationRequests.isLoading,
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchNotificationsForRequest = createDataLoadingThunk(
|
||||
'notificationRequest/fetchNotifications',
|
||||
async ({ accountId }: { accountId: string }, { getState }) => {
|
||||
const sinceId =
|
||||
// @ts-expect-error current.notifications.items is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
getState().notificationRequests.current.notifications.items[0]?.get(
|
||||
'id',
|
||||
) as string | undefined;
|
||||
|
||||
return apiFetchNotifications({
|
||||
since_id: sinceId,
|
||||
account_id: accountId,
|
||||
});
|
||||
},
|
||||
({ notifications, links }, { dispatch }) => {
|
||||
const next = links.refs.find((link) => link.rel === 'next');
|
||||
|
||||
dispatchAssociatedRecords(dispatch, notifications);
|
||||
|
||||
return { notifications, next: next?.uri };
|
||||
},
|
||||
{
|
||||
condition: ({ accountId }, { getState }) => {
|
||||
const current = getState().notificationRequests.current;
|
||||
return !(
|
||||
current.item?.account_id === accountId &&
|
||||
current.notifications.isLoading
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const expandNotificationsForRequest = createDataLoadingThunk(
|
||||
'notificationRequest/expandNotifications',
|
||||
async (_, { getState }) => {
|
||||
const nextUrl = getState().notificationRequests.current.notifications.next;
|
||||
if (!nextUrl) throw new Error('missing URL');
|
||||
|
||||
return apiFetchNotifications(undefined, nextUrl);
|
||||
},
|
||||
({ notifications, links }, { dispatch }) => {
|
||||
const next = links.refs.find((link) => link.rel === 'next');
|
||||
|
||||
dispatchAssociatedRecords(dispatch, notifications);
|
||||
|
||||
return { notifications, next: next?.uri };
|
||||
},
|
||||
{
|
||||
condition: ({ accountId }: { accountId: string }, { getState }) => {
|
||||
const url = getState().notificationRequests.current.notifications.next;
|
||||
|
||||
return (
|
||||
!!url &&
|
||||
!getState().notificationRequests.current.notifications.isLoading &&
|
||||
getState().notificationRequests.current.item?.account_id === accountId
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const acceptNotificationRequest = createDataLoadingThunk(
|
||||
'notificationRequest/accept',
|
||||
({ id }: { id: string }) => apiAcceptNotificationRequest(id),
|
||||
(_data, { dispatch, discardLoadData }) => {
|
||||
dispatch(decreasePendingRequestsCount(1));
|
||||
|
||||
// The payload is not used in any functions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
||||
|
||||
export const dismissNotificationRequest = createDataLoadingThunk(
|
||||
'notificationRequest/dismiss',
|
||||
({ id }: { id: string }) => apiDismissNotificationRequest(id),
|
||||
(_data, { dispatch, discardLoadData }) => {
|
||||
dispatch(decreasePendingRequestsCount(1));
|
||||
|
||||
// The payload is not used in any functions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
||||
|
||||
export const acceptNotificationRequests = createDataLoadingThunk(
|
||||
'notificationRequests/acceptBulk',
|
||||
({ ids }: { ids: string[] }) => apiAcceptNotificationRequests(ids),
|
||||
(_data, { dispatch, discardLoadData, actionArg: { ids } }) => {
|
||||
dispatch(decreasePendingRequestsCount(ids.length));
|
||||
|
||||
// The payload is not used in any functions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
||||
|
||||
export const dismissNotificationRequests = createDataLoadingThunk(
|
||||
'notificationRequests/dismissBulk',
|
||||
({ ids }: { ids: string[] }) => apiDismissNotificationRequests(ids),
|
||||
(_data, { dispatch, discardLoadData, actionArg: { ids } }) => {
|
||||
dispatch(decreasePendingRequestsCount(ids.length));
|
||||
|
||||
// The payload is not used in any functions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
||||
|
|
@ -10,7 +10,7 @@ import api, { getLinks } from '../api';
|
|||
import { unescapeHTML } from '../utils/html';
|
||||
import { requestNotificationPermission } from '../utils/notifications';
|
||||
|
||||
import { fetchFollowRequests, fetchRelationships } from './accounts';
|
||||
import { fetchFollowRequests } from './accounts';
|
||||
import {
|
||||
importFetchedAccount,
|
||||
importFetchedAccounts,
|
||||
|
|
@ -18,10 +18,12 @@ import {
|
|||
importFetchedStatuses,
|
||||
} from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import { notificationsUpdate } from "./notifications_typed";
|
||||
import { register as registerPushNotifications } from './push_notifications';
|
||||
import { saveSettings } from './settings';
|
||||
|
||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
export * from "./notifications_typed";
|
||||
|
||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||
|
||||
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
||||
|
|
@ -30,7 +32,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
|||
|
||||
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||
|
||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
||||
|
||||
|
|
@ -42,19 +43,19 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
|||
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
|
||||
|
||||
export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
|
||||
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
|
||||
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST';
|
||||
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
|
||||
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
|
||||
|
||||
defineMessages({
|
||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||
});
|
||||
|
||||
const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||
const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
|
||||
|
||||
if (accountIds.length > 0) {
|
||||
dispatch(fetchRelationships(accountIds));
|
||||
}
|
||||
};
|
||||
|
||||
export const loadPending = () => ({
|
||||
type: NOTIFICATIONS_LOAD_PENDING,
|
||||
});
|
||||
|
|
@ -95,14 +96,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||
dispatch(importFetchedAccount(notification.report.target_account));
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_UPDATE,
|
||||
notification,
|
||||
usePendingItems: preferPendingItems,
|
||||
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
|
||||
});
|
||||
|
||||
fetchRelatedRelationships(dispatch, [notification]);
|
||||
dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered}));
|
||||
} else if (playSound && !filtered) {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_UPDATE_NOOP,
|
||||
|
|
@ -148,8 +143,8 @@ const noOp = () => {};
|
|||
|
||||
let expandNotificationsController = new AbortController();
|
||||
|
||||
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
export function expandNotifications({ maxId = undefined, forceLoad = false }) {
|
||||
return async (dispatch, getState) => {
|
||||
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||
const notifications = getState().get('notifications');
|
||||
const isLoadingMore = !!maxId;
|
||||
|
|
@ -159,7 +154,6 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
|||
expandNotificationsController.abort();
|
||||
expandNotificationsController = new AbortController();
|
||||
} else {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +180,8 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
|||
|
||||
dispatch(expandNotificationsRequest(isLoadingMore));
|
||||
|
||||
api(getState).get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
|
||||
try {
|
||||
const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal });
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||
|
|
@ -194,13 +189,10 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
|||
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
|
||||
fetchRelatedRelationships(dispatch, response.data);
|
||||
dispatch(submitMarkers());
|
||||
}).catch(error => {
|
||||
} catch(error) {
|
||||
dispatch(expandNotificationsFail(error, isLoadingMore));
|
||||
}).finally(() => {
|
||||
done();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -231,16 +223,6 @@ export function expandNotificationsFail(error, isLoadingMore) {
|
|||
};
|
||||
}
|
||||
|
||||
export function clearNotifications() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_CLEAR,
|
||||
});
|
||||
|
||||
api(getState).post('/api/v1/notifications/clear');
|
||||
};
|
||||
}
|
||||
|
||||
export function scrollTopNotifications(top) {
|
||||
return {
|
||||
type: NOTIFICATIONS_SCROLL_TOP,
|
||||
|
|
|
|||
10
app/javascript/mastodon/actions/notifications_migration.tsx
Normal file
10
app/javascript/mastodon/actions/notifications_migration.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { createAppAsyncThunk } from 'mastodon/store';
|
||||
|
||||
import { fetchNotifications } from './notification_groups';
|
||||
|
||||
export const initializeNotifications = createAppAsyncThunk(
|
||||
'notifications/initialize',
|
||||
(_, { dispatch }) => {
|
||||
void dispatch(fetchNotifications());
|
||||
},
|
||||
);
|
||||
18
app/javascript/mastodon/actions/notifications_typed.ts
Normal file
18
app/javascript/mastodon/actions/notifications_typed.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications';
|
||||
|
||||
export const notificationsUpdate = createAction(
|
||||
'notifications/update',
|
||||
({
|
||||
playSound,
|
||||
...args
|
||||
}: {
|
||||
notification: ApiNotificationJSON;
|
||||
usePendingItems: boolean;
|
||||
playSound: boolean;
|
||||
}) => ({
|
||||
payload: args,
|
||||
meta: { sound: playSound ? 'boop' : undefined },
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
|
||||
export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
|
||||
|
||||
/**
|
||||
* @typedef MediaProps
|
||||
* @property {string} src
|
||||
* @property {boolean} muted
|
||||
* @property {number} volume
|
||||
* @property {number} currentTime
|
||||
* @property {string} poster
|
||||
* @property {string} backgroundColor
|
||||
* @property {string} foregroundColor
|
||||
* @property {string} accentColor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} statusId
|
||||
* @param {string} accountId
|
||||
* @param {string} playerType
|
||||
* @param {MediaProps} props
|
||||
* @returns {object}
|
||||
*/
|
||||
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
|
||||
// @ts-expect-error
|
||||
return (dispatch, getState) => {
|
||||
// Do not open a player for a toot that does not exist
|
||||
if (getState().hasIn(['statuses', statusId])) {
|
||||
dispatch({
|
||||
type: PICTURE_IN_PICTURE_DEPLOY,
|
||||
statusId,
|
||||
accountId,
|
||||
playerType,
|
||||
props,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* @return {object}
|
||||
*/
|
||||
export const removePictureInPicture = () => ({
|
||||
type: PICTURE_IN_PICTURE_REMOVE,
|
||||
});
|
||||
31
app/javascript/mastodon/actions/picture_in_picture.ts
Normal file
31
app/javascript/mastodon/actions/picture_in_picture.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture';
|
||||
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
interface DeployParams {
|
||||
statusId: string;
|
||||
accountId: string;
|
||||
playerType: 'audio' | 'video';
|
||||
props: PIPMediaProps;
|
||||
}
|
||||
|
||||
export const removePictureInPicture = createAction('pip/remove');
|
||||
|
||||
export const deployPictureInPictureAction =
|
||||
createAction<DeployParams>('pip/deploy');
|
||||
|
||||
export const deployPictureInPicture = createAppAsyncThunk(
|
||||
'pip/deploy',
|
||||
(args: DeployParams, { dispatch, getState }) => {
|
||||
const { statusId } = args;
|
||||
|
||||
// Do not open a player for a toot that does not exist
|
||||
|
||||
// @ts-expect-error state.statuses is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (getState().hasIn(['statuses', statusId])) {
|
||||
dispatch(deployPictureInPictureAction(args));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -8,10 +8,10 @@ export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
|
|||
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
|
||||
|
||||
export function fetchPinnedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchPinnedStatusesRequest());
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
|
||||
api().get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchPinnedStatusesSuccess(response.data, null));
|
||||
}).catch(error => {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
|
|||
export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
|
||||
export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';
|
||||
|
||||
export const vote = (pollId, choices) => (dispatch, getState) => {
|
||||
export const vote = (pollId, choices) => (dispatch) => {
|
||||
dispatch(voteRequest());
|
||||
|
||||
api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
|
||||
api().post(`/api/v1/polls/${pollId}/votes`, { choices })
|
||||
.then(({ data }) => {
|
||||
dispatch(importFetchedPoll(data));
|
||||
dispatch(voteSuccess(data));
|
||||
|
|
@ -21,10 +21,10 @@ export const vote = (pollId, choices) => (dispatch, getState) => {
|
|||
.catch(err => dispatch(voteFail(err)));
|
||||
};
|
||||
|
||||
export const fetchPoll = pollId => (dispatch, getState) => {
|
||||
export const fetchPoll = pollId => (dispatch) => {
|
||||
dispatch(fetchPollRequest());
|
||||
|
||||
api(getState).get(`/api/v1/polls/${pollId}`)
|
||||
api().get(`/api/v1/polls/${pollId}`)
|
||||
.then(({ data }) => {
|
||||
dispatch(importFetchedPoll(data));
|
||||
dispatch(fetchPollSuccess(data));
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ export const initReport = (account, status) => dispatch =>
|
|||
},
|
||||
}));
|
||||
|
||||
export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||
export const submitReport = (params, onSuccess, onFail) => (dispatch) => {
|
||||
dispatch(submitReportRequest());
|
||||
|
||||
api(getState).post('/api/v1/reports', params).then(response => {
|
||||
api().post('/api/v1/reports', params).then(response => {
|
||||
dispatch(submitReportSuccess(response.data));
|
||||
if (onSuccess) onSuccess();
|
||||
}).catch(error => {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export function submitSearch(type) {
|
|||
|
||||
dispatch(fetchSearchRequest(type));
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
api().get('/api/v2/search', {
|
||||
params: {
|
||||
q: value,
|
||||
resolve: signedIn,
|
||||
|
|
@ -99,7 +99,7 @@ export const expandSearch = type => (dispatch, getState) => {
|
|||
|
||||
dispatch(expandSearchRequest(type));
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
api().get('/api/v2/search', {
|
||||
params: {
|
||||
q: value,
|
||||
type,
|
||||
|
|
@ -156,7 +156,7 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchSearchRequest());
|
||||
|
||||
api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
|
||||
api().get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
|
||||
if (response.data.accounts?.length > 0) {
|
||||
dispatch(importFetchedAccounts(response.data.accounts));
|
||||
history.push(`/@${response.data.accounts[0].acct}`);
|
||||
|
|
@ -179,6 +179,11 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
|||
|
||||
export const clickSearchResult = (q, type) => (dispatch, getState) => {
|
||||
const previous = getState().getIn(['search', 'recent']);
|
||||
|
||||
if (previous.some(x => x.get('q') === q && x.get('type') === type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const current = previous.add(fromJS({ type, q })).takeLast(4);
|
||||
|
||||
|
|
@ -207,4 +212,4 @@ export const hydrateSearch = () => (dispatch, getState) => {
|
|||
if (history !== null) {
|
||||
dispatch(updateSearchHistory(history));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const fetchServer = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchServerRequest());
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.get('/api/v2/instance').then(({ data }) => {
|
||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||
dispatch(fetchServerSuccess(data));
|
||||
|
|
@ -46,10 +46,10 @@ const fetchServerFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchServerTranslationLanguages = () => (dispatch, getState) => {
|
||||
export const fetchServerTranslationLanguages = () => (dispatch) => {
|
||||
dispatch(fetchServerTranslationLanguagesRequest());
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.get('/api/v1/instance/translation_languages').then(({ data }) => {
|
||||
dispatch(fetchServerTranslationLanguagesSuccess(data));
|
||||
}).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
|
||||
|
|
@ -76,7 +76,7 @@ export const fetchExtendedDescription = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchExtendedDescriptionRequest());
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.get('/api/v1/instance/extended_description')
|
||||
.then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
|
||||
.catch(err => dispatch(fetchExtendedDescriptionFail(err)));
|
||||
|
|
@ -103,7 +103,7 @@ export const fetchDomainBlocks = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchDomainBlocksRequest());
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.get('/api/v1/instance/domain_blocks')
|
||||
.then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
|
||||
.catch(err => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { browserHistory } from 'mastodon/components/router';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||
|
|
@ -47,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchStatus(id, forceFetch = false) {
|
||||
export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
||||
return (dispatch, getState) => {
|
||||
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
||||
|
||||
dispatch(fetchContext(id));
|
||||
if (alsoFetchContext) {
|
||||
dispatch(fetchContext(id));
|
||||
}
|
||||
|
||||
if (skipLoading) {
|
||||
return;
|
||||
|
|
@ -59,7 +63,7 @@ export function fetchStatus(id, forceFetch = false) {
|
|||
|
||||
dispatch(fetchStatusRequest(id, skipLoading));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
||||
api().get(`/api/v1/statuses/${id}`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(fetchStatusSuccess(skipLoading));
|
||||
}).catch(error => {
|
||||
|
|
@ -93,7 +97,7 @@ export function redraft(status, raw_text) {
|
|||
};
|
||||
}
|
||||
|
||||
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
||||
export const editStatus = (id) => (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
|
||||
if (status.get('poll')) {
|
||||
|
|
@ -102,9 +106,9 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchStatusSourceRequest());
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
api().get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
dispatch(fetchStatusSourceSuccess());
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusSourceFail(error));
|
||||
|
|
@ -124,7 +128,7 @@ export const fetchStatusSourceFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||
export function deleteStatus(id, withRedraft = false) {
|
||||
return (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
|
||||
|
|
@ -134,14 +138,14 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||
|
||||
dispatch(deleteStatusRequest(id));
|
||||
|
||||
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
||||
api().delete(`/api/v1/statuses/${id}`).then(response => {
|
||||
dispatch(deleteStatusSuccess(id));
|
||||
dispatch(deleteFromTimelines(id));
|
||||
dispatch(importFetchedAccount(response.data.account));
|
||||
|
||||
if (withRedraft) {
|
||||
dispatch(redraft(status, response.data.text));
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch(deleteStatusFail(id, error));
|
||||
|
|
@ -175,10 +179,10 @@ export const updateStatus = status => dispatch =>
|
|||
dispatch(importFetchedStatus(status));
|
||||
|
||||
export function fetchContext(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchContextRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
|
||||
api().get(`/api/v1/statuses/${id}/context`).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
|
||||
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
|
||||
|
||||
|
|
@ -219,10 +223,10 @@ export function fetchContextFail(id, error) {
|
|||
}
|
||||
|
||||
export function muteStatus(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(muteStatusRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
|
||||
api().post(`/api/v1/statuses/${id}/mute`).then(() => {
|
||||
dispatch(muteStatusSuccess(id));
|
||||
}).catch(error => {
|
||||
dispatch(muteStatusFail(id, error));
|
||||
|
|
@ -253,10 +257,10 @@ export function muteStatusFail(id, error) {
|
|||
}
|
||||
|
||||
export function unmuteStatus(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unmuteStatusRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
|
||||
api().post(`/api/v1/statuses/${id}/unmute`).then(() => {
|
||||
dispatch(unmuteStatusSuccess(id));
|
||||
}).catch(error => {
|
||||
dispatch(unmuteStatusFail(id, error));
|
||||
|
|
@ -308,6 +312,21 @@ export function revealStatus(ids) {
|
|||
};
|
||||
}
|
||||
|
||||
export function toggleStatusSpoilers(statusId) {
|
||||
return (dispatch, getState) => {
|
||||
const status = getState().statuses.get(statusId);
|
||||
|
||||
if (!status)
|
||||
return;
|
||||
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(statusId));
|
||||
} else {
|
||||
dispatch(hideStatus(statusId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleStatusCollapse(id, isCollapsed) {
|
||||
return {
|
||||
type: STATUS_COLLAPSE,
|
||||
|
|
@ -316,10 +335,10 @@ export function toggleStatusCollapse(id, isCollapsed) {
|
|||
};
|
||||
}
|
||||
|
||||
export const translateStatus = id => (dispatch, getState) => {
|
||||
export const translateStatus = id => (dispatch) => {
|
||||
dispatch(translateStatusRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => {
|
||||
api().post(`/api/v1/statuses/${id}/translate`).then(response => {
|
||||
dispatch(translateStatusSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(translateStatusFail(id, error));
|
||||
|
|
@ -348,3 +367,15 @@ export const undoStatusTranslation = (id, pollId) => ({
|
|||
id,
|
||||
pollId,
|
||||
});
|
||||
|
||||
export const navigateToStatus = (statusId) => {
|
||||
return (_dispatch, getState) => {
|
||||
const state = getState();
|
||||
const accountId = state.statuses.getIn([statusId, 'account']);
|
||||
const acct = state.accounts.getIn([accountId, 'acct']);
|
||||
|
||||
if (acct) {
|
||||
browserHistory.push(`/@${acct}/${statusId}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const convertState = rawState =>
|
|||
fromJS(rawState, (k, v) =>
|
||||
Iterable.isIndexed(v) ? v.toList() : v.toMap());
|
||||
|
||||
|
||||
export function hydrateStore(rawState) {
|
||||
return dispatch => {
|
||||
const state = convertState(rawState);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
deleteAnnouncement,
|
||||
} from './announcements';
|
||||
import { updateConversations } from './conversations';
|
||||
import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
|
||||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { updateStatus } from './statuses';
|
||||
import {
|
||||
|
|
@ -36,7 +37,7 @@ const randomUpTo = max =>
|
|||
* @param {string} channelName
|
||||
* @param {Object.<string, string>} params
|
||||
* @param {Object} options
|
||||
* @param {function(Function, Function): void} [options.fallback]
|
||||
* @param {function(Function, Function): Promise<void>} [options.fallback]
|
||||
* @param {function(): void} [options.fillGaps]
|
||||
* @param {function(object): boolean} [options.accept]
|
||||
* @returns {function(): void}
|
||||
|
|
@ -51,14 +52,13 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
let pollingId;
|
||||
|
||||
/**
|
||||
* @param {function(Function, Function): void} fallback
|
||||
* @param {function(Function, Function): Promise<void>} fallback
|
||||
*/
|
||||
|
||||
const useFallback = fallback => {
|
||||
fallback(dispatch, () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
|
||||
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
|
||||
});
|
||||
const useFallback = async fallback => {
|
||||
await fallback(dispatch, getState);
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
|
||||
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -77,7 +77,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
},
|
||||
|
||||
onDisconnect() {
|
||||
dispatch(disconnectTimeline(timelineId));
|
||||
dispatch(disconnectTimeline({ timeline: timelineId }));
|
||||
|
||||
if (options.fallback) {
|
||||
// @ts-expect-error
|
||||
|
|
@ -98,9 +98,19 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
case 'delete':
|
||||
dispatch(deleteFromTimelines(data.payload));
|
||||
break;
|
||||
case 'notification':
|
||||
case 'notification': {
|
||||
// @ts-expect-error
|
||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||
const notificationJSON = JSON.parse(data.payload);
|
||||
dispatch(updateNotifications(notificationJSON, messages, locale));
|
||||
// TODO: remove this once the groups feature replaces the previous one
|
||||
dispatch(processNewNotificationForGroups(notificationJSON));
|
||||
break;
|
||||
}
|
||||
case 'notifications_merged':
|
||||
const state = getState();
|
||||
if (state.notifications.top || !state.notifications.mounted)
|
||||
dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
|
||||
dispatch(refreshStaleNotificationGroups());
|
||||
break;
|
||||
case 'conversation':
|
||||
// @ts-expect-error
|
||||
|
|
@ -125,21 +135,24 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
|
||||
/**
|
||||
* @param {Function} dispatch
|
||||
* @param {function(): void} done
|
||||
*/
|
||||
const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
||||
// @ts-expect-error
|
||||
dispatch(expandHomeTimeline({}, () =>
|
||||
// @ts-expect-error
|
||||
dispatch(expandNotifications({}, () =>
|
||||
dispatch(fetchAnnouncements(done))))));
|
||||
};
|
||||
async function refreshHomeTimelineAndNotification(dispatch) {
|
||||
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
||||
|
||||
// TODO: polling for merged notifications
|
||||
try {
|
||||
await dispatch(pollRecentGroupNotifications());
|
||||
} catch {
|
||||
// TODO
|
||||
}
|
||||
|
||||
await dispatch(fetchAnnouncements());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {function(): void}
|
||||
*/
|
||||
export const connectUserStream = () =>
|
||||
// @ts-expect-error
|
||||
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
|
|||
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
|
||||
|
||||
export function fetchSuggestions(withRelationships = false) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchSuggestionsRequest());
|
||||
|
||||
api(getState).get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => {
|
||||
api().get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(fetchSuggestionsSuccess(response.data));
|
||||
|
||||
|
|
@ -48,18 +48,11 @@ export function fetchSuggestionsFail(error) {
|
|||
};
|
||||
}
|
||||
|
||||
export const dismissSuggestion = accountId => (dispatch, getState) => {
|
||||
export const dismissSuggestion = accountId => (dispatch) => {
|
||||
dispatch({
|
||||
type: SUGGESTIONS_DISMISS,
|
||||
id: accountId,
|
||||
});
|
||||
|
||||
api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => {
|
||||
dispatch(fetchSuggestionsRequest());
|
||||
|
||||
api(getState).get('/api/v2/suggestions').then(response => {
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(fetchSuggestionsSuccess(response.data));
|
||||
}).catch(error => dispatch(fetchSuggestionsFail(error)));
|
||||
}).catch(() => {});
|
||||
api().delete(`/api/v1/suggestions/${accountId}`).catch(() => {});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST';
|
|||
export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS';
|
||||
export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL';
|
||||
|
||||
export const fetchHashtag = name => (dispatch, getState) => {
|
||||
export const fetchHashtag = name => (dispatch) => {
|
||||
dispatch(fetchHashtagRequest());
|
||||
|
||||
api(getState).get(`/api/v1/tags/${name}`).then(({ data }) => {
|
||||
api().get(`/api/v1/tags/${name}`).then(({ data }) => {
|
||||
dispatch(fetchHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchHashtagFail(err));
|
||||
|
|
@ -45,10 +45,10 @@ export const fetchHashtagFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchFollowedHashtags = () => (dispatch, getState) => {
|
||||
export const fetchFollowedHashtags = () => (dispatch) => {
|
||||
dispatch(fetchFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get('/api/v1/followed_tags').then(response => {
|
||||
api().get('/api/v1/followed_tags').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(err => {
|
||||
|
|
@ -87,7 +87,7 @@ export function expandFollowedHashtags() {
|
|||
|
||||
dispatch(expandFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
|
|
@ -117,10 +117,10 @@ export function expandFollowedHashtagsFail(error) {
|
|||
};
|
||||
}
|
||||
|
||||
export const followHashtag = name => (dispatch, getState) => {
|
||||
export const followHashtag = name => (dispatch) => {
|
||||
dispatch(followHashtagRequest(name));
|
||||
|
||||
api(getState).post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
|
||||
api().post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
|
||||
dispatch(followHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(followHashtagFail(name, err));
|
||||
|
|
@ -144,10 +144,10 @@ export const followHashtagFail = (name, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const unfollowHashtag = name => (dispatch, getState) => {
|
||||
export const unfollowHashtag = name => (dispatch) => {
|
||||
dispatch(unfollowHashtagRequest(name));
|
||||
|
||||
api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
|
||||
api().post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
|
||||
dispatch(unfollowHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(unfollowHashtagFail(name, err));
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
|||
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import {timelineDelete} from './timelines_typed';
|
||||
|
||||
export { disconnectTimeline } from './timelines_typed';
|
||||
|
||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
|
||||
|
||||
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
|
||||
|
|
@ -17,10 +19,13 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
|
|||
|
||||
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
||||
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
|
||||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
|
||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||
|
||||
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
||||
export const TIMELINE_GAP = null;
|
||||
|
||||
export const loadPending = timeline => ({
|
||||
type: TIMELINE_LOAD_PENDING,
|
||||
|
|
@ -58,16 +63,10 @@ export function updateTimeline(timeline, status, accept) {
|
|||
export function deleteFromTimelines(id) {
|
||||
return (dispatch, getState) => {
|
||||
const accountId = getState().getIn(['statuses', id, 'account']);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')).valueSeq().toJSON();
|
||||
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
|
||||
|
||||
dispatch({
|
||||
type: TIMELINE_DELETE,
|
||||
id,
|
||||
accountId,
|
||||
references,
|
||||
reblogOf,
|
||||
});
|
||||
dispatch(timelineDelete({ statusId: id, accountId, references, reblogOf }));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -77,21 +76,18 @@ export function clearTimeline(timeline) {
|
|||
};
|
||||
}
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
const parseTags = (tags = {}, mode) => {
|
||||
return (tags[mode] || []).map((tag) => {
|
||||
return tag.value;
|
||||
});
|
||||
};
|
||||
|
||||
export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
export function expandTimeline(timelineId, path, params = {}) {
|
||||
return async (dispatch, getState) => {
|
||||
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||
const isLoadingMore = !!params.max_id;
|
||||
|
||||
if (timeline.get('isLoading')) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -110,59 +106,67 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
|
||||
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
||||
|
||||
api(getState).get(path, { params }).then(response => {
|
||||
try {
|
||||
const response = await api().get(path, { params });
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
|
||||
|
||||
if (timelineId === 'home' && !isLoadingMore && !isLoadingRecent) {
|
||||
const now = new Date();
|
||||
const fittingIndex = response.data.findIndex(status => now - (new Date(status.created_at)) > 4 * 3600 * 1000);
|
||||
|
||||
if (fittingIndex !== -1) {
|
||||
dispatch(insertIntoTimeline(timelineId, TIMELINE_SUGGESTIONS, Math.max(1, fittingIndex)));
|
||||
}
|
||||
}
|
||||
|
||||
if (timelineId === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
}
|
||||
}).catch(error => {
|
||||
} catch(error) {
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
}).finally(() => {
|
||||
done();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
export function fillTimelineGaps(timelineId, path, params = {}) {
|
||||
return async (dispatch, getState) => {
|
||||
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||
const items = timeline.get('items');
|
||||
const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
|
||||
const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
|
||||
|
||||
// Only expand at most two gaps to avoid doing too many requests
|
||||
done = gaps.take(2).reduce((done, maxId) => {
|
||||
return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
|
||||
}, done);
|
||||
|
||||
done();
|
||||
for (const maxId of gaps.take(2)) {
|
||||
await dispatch(expandTimeline(timelineId, path, { ...params, maxId }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia });
|
||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia });
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||
export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
|
||||
export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId });
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => {
|
||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
any: parseTags(tags, 'any'),
|
||||
all: parseTags(tags, 'all'),
|
||||
none: parseTags(tags, 'none'),
|
||||
local: local,
|
||||
}, done);
|
||||
});
|
||||
};
|
||||
|
||||
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
|
||||
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done);
|
||||
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
|
||||
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
|
||||
export const fillHomeTimelineGaps = () => fillTimelineGaps('home', '/api/v1/timelines/home', {});
|
||||
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia });
|
||||
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia });
|
||||
export const fillListTimelineGaps = (id) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {});
|
||||
|
||||
export function expandTimelineRequest(timeline, isLoadingMore) {
|
||||
return {
|
||||
|
|
@ -211,13 +215,14 @@ export function connectTimeline(timeline) {
|
|||
};
|
||||
}
|
||||
|
||||
export const disconnectTimeline = timeline => ({
|
||||
type: TIMELINE_DISCONNECT,
|
||||
timeline,
|
||||
usePendingItems: preferPendingItems,
|
||||
});
|
||||
|
||||
export const markAsPartial = timeline => ({
|
||||
type: TIMELINE_MARK_AS_PARTIAL,
|
||||
timeline,
|
||||
});
|
||||
|
||||
export const insertIntoTimeline = (timeline, key, index) => ({
|
||||
type: TIMELINE_INSERT,
|
||||
timeline,
|
||||
index,
|
||||
key,
|
||||
});
|
||||
|
|
|
|||
20
app/javascript/mastodon/actions/timelines_typed.ts
Normal file
20
app/javascript/mastodon/actions/timelines_typed.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||
|
||||
export const disconnectTimeline = createAction(
|
||||
'timeline/disconnect',
|
||||
({ timeline }: { timeline: string }) => ({
|
||||
payload: {
|
||||
timeline,
|
||||
usePendingItems: preferPendingItems,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const timelineDelete = createAction<{
|
||||
statusId: string;
|
||||
accountId: string;
|
||||
references: string[];
|
||||
reblogOf: string | null;
|
||||
}>('timelines/delete');
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import api, { getLinks } from '../api';
|
||||
|
||||
import { importFetchedStatuses } from './importer';
|
||||
import { importFetchedStatuses, importFetchedAccounts } from './importer';
|
||||
|
||||
export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
|
||||
export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
|
||||
|
|
@ -18,10 +18,10 @@ export const TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST';
|
|||
export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS';
|
||||
export const TRENDS_STATUSES_EXPAND_FAIL = 'TRENDS_STATUSES_EXPAND_FAIL';
|
||||
|
||||
export const fetchTrendingHashtags = () => (dispatch, getState) => {
|
||||
export const fetchTrendingHashtags = () => (dispatch) => {
|
||||
dispatch(fetchTrendingHashtagsRequest());
|
||||
|
||||
api(getState)
|
||||
api()
|
||||
.get('/api/v1/trends/tags')
|
||||
.then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
|
||||
.catch(err => dispatch(fetchTrendingHashtagsFail(err)));
|
||||
|
|
@ -45,12 +45,15 @@ export const fetchTrendingHashtagsFail = error => ({
|
|||
skipAlert: true,
|
||||
});
|
||||
|
||||
export const fetchTrendingLinks = () => (dispatch, getState) => {
|
||||
export const fetchTrendingLinks = () => (dispatch) => {
|
||||
dispatch(fetchTrendingLinksRequest());
|
||||
|
||||
api(getState)
|
||||
.get('/api/v1/trends/links')
|
||||
.then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
|
||||
api()
|
||||
.get('/api/v1/trends/links', { params: { limit: 20 } })
|
||||
.then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data.flatMap(link => link.authors.map(author => author.account)).filter(account => !!account)));
|
||||
dispatch(fetchTrendingLinksSuccess(data));
|
||||
})
|
||||
.catch(err => dispatch(fetchTrendingLinksFail(err)));
|
||||
};
|
||||
|
||||
|
|
@ -79,7 +82,7 @@ export const fetchTrendingStatuses = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchTrendingStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/trends/statuses').then(response => {
|
||||
api().get('/api/v1/trends/statuses').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -115,7 +118,7 @@ export const expandTrendingStatuses = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(expandTrendingStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api().get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
|
||||
import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import { getAccessToken } from './initial_state';
|
||||
import ready from './ready';
|
||||
import type { GetState } from './store';
|
||||
|
||||
export const getLinks = (response: AxiosResponse) => {
|
||||
const value = response.headers.link as string | undefined;
|
||||
|
|
@ -29,25 +29,25 @@ const setCSRFHeader = () => {
|
|||
|
||||
void ready(setCSRFHeader);
|
||||
|
||||
const authorizationHeaderFromState = (getState?: GetState) => {
|
||||
const accessToken =
|
||||
getState && (getState().meta.get('access_token', '') as string);
|
||||
const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
|
||||
const accessToken = getAccessToken();
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
if (!accessToken) return {};
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
} as RawAxiosRequestHeaders;
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function api(getState: GetState) {
|
||||
export default function api(withAuthorization = true) {
|
||||
return axios.create({
|
||||
transitional: {
|
||||
clarifyTimeoutError: true,
|
||||
},
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
...(withAuthorization ? authorizationTokenFromInitialState() : {}),
|
||||
},
|
||||
|
||||
transformResponse: [
|
||||
|
|
@ -61,3 +61,51 @@ export default function api(getState: GetState) {
|
|||
],
|
||||
});
|
||||
}
|
||||
|
||||
type RequestParamsOrData = Record<string, unknown>;
|
||||
|
||||
export async function apiRequest<ApiResponse = unknown>(
|
||||
method: Method,
|
||||
url: string,
|
||||
args: {
|
||||
params?: RequestParamsOrData;
|
||||
data?: RequestParamsOrData;
|
||||
timeout?: number;
|
||||
} = {},
|
||||
) {
|
||||
const { data } = await api().request<ApiResponse>({
|
||||
method,
|
||||
url: '/api/' + url,
|
||||
...args,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function apiRequestGet<ApiResponse = unknown>(
|
||||
url: string,
|
||||
params?: RequestParamsOrData,
|
||||
) {
|
||||
return apiRequest<ApiResponse>('GET', url, { params });
|
||||
}
|
||||
|
||||
export async function apiRequestPost<ApiResponse = unknown>(
|
||||
url: string,
|
||||
data?: RequestParamsOrData,
|
||||
) {
|
||||
return apiRequest<ApiResponse>('POST', url, { data });
|
||||
}
|
||||
|
||||
export async function apiRequestPut<ApiResponse = unknown>(
|
||||
url: string,
|
||||
data?: RequestParamsOrData,
|
||||
) {
|
||||
return apiRequest<ApiResponse>('PUT', url, { data });
|
||||
}
|
||||
|
||||
export async function apiRequestDelete<ApiResponse = unknown>(
|
||||
url: string,
|
||||
params?: RequestParamsOrData,
|
||||
) {
|
||||
return apiRequest<ApiResponse>('DELETE', url, { params });
|
||||
}
|
||||
|
|
|
|||
7
app/javascript/mastodon/api/accounts.ts
Normal file
7
app/javascript/mastodon/api/accounts.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { apiRequestPost } from 'mastodon/api';
|
||||
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||
|
||||
export const apiSubmitAccountNote = (id: string, value: string) =>
|
||||
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
});
|
||||
15
app/javascript/mastodon/api/directory.ts
Normal file
15
app/javascript/mastodon/api/directory.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { apiRequestGet } from 'mastodon/api';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
|
||||
export const apiGetDirectory = (
|
||||
params: {
|
||||
order: string;
|
||||
local: boolean;
|
||||
offset?: number;
|
||||
},
|
||||
limit = 20,
|
||||
) =>
|
||||
apiRequestGet<ApiAccountJSON[]>('v1/directory', {
|
||||
...params,
|
||||
limit,
|
||||
});
|
||||
10
app/javascript/mastodon/api/interactions.ts
Normal file
10
app/javascript/mastodon/api/interactions.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { apiRequestPost } from 'mastodon/api';
|
||||
import type { Status, StatusVisibility } from 'mastodon/models/status';
|
||||
|
||||
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
||||
apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
|
||||
visibility,
|
||||
});
|
||||
|
||||
export const apiUnreblog = (statusId: string) =>
|
||||
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
|
||||
9
app/javascript/mastodon/api/notification_policies.ts
Normal file
9
app/javascript/mastodon/api/notification_policies.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { apiRequestGet, apiRequestPut } from 'mastodon/api';
|
||||
import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies';
|
||||
|
||||
export const apiGetNotificationPolicy = () =>
|
||||
apiRequestGet<NotificationPolicyJSON>('v2/notifications/policy');
|
||||
|
||||
export const apiUpdateNotificationsPolicy = (
|
||||
policy: Partial<NotificationPolicyJSON>,
|
||||
) => apiRequestPut<NotificationPolicyJSON>('v2/notifications/policy', policy);
|
||||
96
app/javascript/mastodon/api/notifications.ts
Normal file
96
app/javascript/mastodon/api/notifications.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import api, {
|
||||
apiRequest,
|
||||
getLinks,
|
||||
apiRequestGet,
|
||||
apiRequestPost,
|
||||
} from 'mastodon/api';
|
||||
import type {
|
||||
ApiNotificationGroupsResultJSON,
|
||||
ApiNotificationRequestJSON,
|
||||
ApiNotificationJSON,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
|
||||
export const apiFetchNotifications = async (
|
||||
params?: {
|
||||
account_id?: string;
|
||||
since_id?: string;
|
||||
},
|
||||
url?: string,
|
||||
) => {
|
||||
const response = await api().request<ApiNotificationJSON[]>({
|
||||
method: 'GET',
|
||||
url: url ?? '/api/v1/notifications',
|
||||
params,
|
||||
});
|
||||
|
||||
return {
|
||||
notifications: response.data,
|
||||
links: getLinks(response),
|
||||
};
|
||||
};
|
||||
|
||||
export const apiFetchNotificationGroups = async (params?: {
|
||||
url?: string;
|
||||
grouped_types?: string[];
|
||||
exclude_types?: string[];
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
}) => {
|
||||
const response = await api().request<ApiNotificationGroupsResultJSON>({
|
||||
method: 'GET',
|
||||
url: '/api/v2/notifications',
|
||||
params,
|
||||
});
|
||||
|
||||
const { statuses, accounts, notification_groups } = response.data;
|
||||
|
||||
return {
|
||||
statuses,
|
||||
accounts,
|
||||
notifications: notification_groups,
|
||||
links: getLinks(response),
|
||||
};
|
||||
};
|
||||
|
||||
export const apiClearNotifications = () =>
|
||||
apiRequest<undefined>('POST', 'v1/notifications/clear');
|
||||
|
||||
export const apiFetchNotificationRequests = async (
|
||||
params?: {
|
||||
since_id?: string;
|
||||
},
|
||||
url?: string,
|
||||
) => {
|
||||
const response = await api().request<ApiNotificationRequestJSON[]>({
|
||||
method: 'GET',
|
||||
url: url ?? '/api/v1/notifications/requests',
|
||||
params,
|
||||
});
|
||||
|
||||
return {
|
||||
requests: response.data,
|
||||
links: getLinks(response),
|
||||
};
|
||||
};
|
||||
|
||||
export const apiFetchNotificationRequest = async (id: string) => {
|
||||
return apiRequestGet<ApiNotificationRequestJSON>(
|
||||
`v1/notifications/requests/${id}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const apiAcceptNotificationRequest = async (id: string) => {
|
||||
return apiRequestPost(`v1/notifications/requests/${id}/accept`);
|
||||
};
|
||||
|
||||
export const apiDismissNotificationRequest = async (id: string) => {
|
||||
return apiRequestPost(`v1/notifications/requests/${id}/dismiss`);
|
||||
};
|
||||
|
||||
export const apiAcceptNotificationRequests = async (id: string[]) => {
|
||||
return apiRequestPost('v1/notifications/requests/accept', { id });
|
||||
};
|
||||
|
||||
export const apiDismissNotificationRequests = async (id: string[]) => {
|
||||
return apiRequestPost('v1/notifications/requests/dismiss', { id });
|
||||
};
|
||||
47
app/javascript/mastodon/api_types/accounts.ts
Normal file
47
app/javascript/mastodon/api_types/accounts.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||
|
||||
export interface ApiAccountFieldJSON {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
}
|
||||
|
||||
export interface ApiAccountRoleJSON {
|
||||
color: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiAccountJSON {
|
||||
acct: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
bot: boolean;
|
||||
created_at: string;
|
||||
discoverable: boolean;
|
||||
indexable: boolean;
|
||||
display_name: string;
|
||||
emojis: ApiCustomEmojiJSON[];
|
||||
fields: ApiAccountFieldJSON[];
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
group: boolean;
|
||||
header: string;
|
||||
header_static: string;
|
||||
id: string;
|
||||
last_status_at: string;
|
||||
locked: boolean;
|
||||
noindex?: boolean;
|
||||
note: string;
|
||||
roles?: ApiAccountJSON[];
|
||||
statuses_count: number;
|
||||
uri: string;
|
||||
url: string;
|
||||
username: string;
|
||||
moved?: ApiAccountJSON;
|
||||
suspended?: boolean;
|
||||
limited?: boolean;
|
||||
memorial?: boolean;
|
||||
hide_collections: boolean;
|
||||
}
|
||||
8
app/javascript/mastodon/api_types/custom_emoji.ts
Normal file
8
app/javascript/mastodon/api_types/custom_emoji.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiCustomEmojiJSON {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
category?: string;
|
||||
visible_in_picker: boolean;
|
||||
}
|
||||
7
app/javascript/mastodon/api_types/markers.ts
Normal file
7
app/javascript/mastodon/api_types/markers.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// See app/serializers/rest/account_serializer.rb
|
||||
|
||||
export interface MarkerJSON {
|
||||
last_read_id: string;
|
||||
version: string;
|
||||
updated_at: string;
|
||||
}
|
||||
22
app/javascript/mastodon/api_types/media_attachments.ts
Normal file
22
app/javascript/mastodon/api_types/media_attachments.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// See app/serializers/rest/media_attachment_serializer.rb
|
||||
|
||||
export type MediaAttachmentType =
|
||||
| 'image'
|
||||
| 'gifv'
|
||||
| 'video'
|
||||
| 'unknown'
|
||||
| 'audio';
|
||||
|
||||
export interface ApiMediaAttachmentJSON {
|
||||
id: string;
|
||||
type: MediaAttachmentType;
|
||||
url: string;
|
||||
preview_url: string;
|
||||
remoteUrl: string;
|
||||
preview_remote_url: string;
|
||||
text_url: string;
|
||||
// TODO: how to define this?
|
||||
meta: unknown;
|
||||
description?: string;
|
||||
blurhash: string;
|
||||
}
|
||||
15
app/javascript/mastodon/api_types/notification_policies.ts
Normal file
15
app/javascript/mastodon/api_types/notification_policies.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// See app/serializers/rest/notification_policy_serializer.rb
|
||||
|
||||
export type NotificationPolicyValue = 'accept' | 'filter' | 'drop';
|
||||
|
||||
export interface NotificationPolicyJSON {
|
||||
for_not_following: NotificationPolicyValue;
|
||||
for_not_followers: NotificationPolicyValue;
|
||||
for_new_accounts: NotificationPolicyValue;
|
||||
for_private_mentions: NotificationPolicyValue;
|
||||
for_limited_accounts: NotificationPolicyValue;
|
||||
summary: {
|
||||
pending_requests_count: number;
|
||||
pending_notifications_count: number;
|
||||
};
|
||||
}
|
||||
160
app/javascript/mastodon/api_types/notifications.ts
Normal file
160
app/javascript/mastodon/api_types/notifications.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// See app/serializers/rest/notification_group_serializer.rb
|
||||
|
||||
import type { AccountWarningAction } from 'mastodon/models/notification_group';
|
||||
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
import type { ApiReportJSON } from './reports';
|
||||
import type { ApiStatusJSON } from './statuses';
|
||||
|
||||
// See app/model/notification.rb
|
||||
export const allNotificationTypes = [
|
||||
'follow',
|
||||
'follow_request',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll',
|
||||
'status',
|
||||
'update',
|
||||
'admin.sign_up',
|
||||
'admin.report',
|
||||
'moderation_warning',
|
||||
'severed_relationships',
|
||||
];
|
||||
|
||||
export type NotificationWithStatusType =
|
||||
| 'favourite'
|
||||
| 'reblog'
|
||||
| 'status'
|
||||
| 'mention'
|
||||
| 'poll'
|
||||
| 'update';
|
||||
|
||||
export type NotificationType =
|
||||
| NotificationWithStatusType
|
||||
| 'follow'
|
||||
| 'follow_request'
|
||||
| 'moderation_warning'
|
||||
| 'severed_relationships'
|
||||
| 'admin.sign_up'
|
||||
| 'admin.report';
|
||||
|
||||
export interface BaseNotificationJSON {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
created_at: string;
|
||||
group_key: string;
|
||||
account: ApiAccountJSON;
|
||||
}
|
||||
|
||||
export interface BaseNotificationGroupJSON {
|
||||
group_key: string;
|
||||
notifications_count: number;
|
||||
type: NotificationType;
|
||||
sample_account_ids: string[];
|
||||
latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly
|
||||
most_recent_notification_id: string;
|
||||
page_min_id?: string;
|
||||
page_max_id?: string;
|
||||
}
|
||||
|
||||
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
||||
type: NotificationWithStatusType;
|
||||
status_id: string | null;
|
||||
}
|
||||
|
||||
interface NotificationWithStatusJSON extends BaseNotificationJSON {
|
||||
type: NotificationWithStatusType;
|
||||
status: ApiStatusJSON | null;
|
||||
}
|
||||
|
||||
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'admin.report';
|
||||
report: ApiReportJSON;
|
||||
}
|
||||
|
||||
interface ReportNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'admin.report';
|
||||
report: ApiReportJSON;
|
||||
}
|
||||
|
||||
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
|
||||
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: SimpleNotificationTypes;
|
||||
}
|
||||
|
||||
interface SimpleNotificationJSON extends BaseNotificationJSON {
|
||||
type: SimpleNotificationTypes;
|
||||
}
|
||||
|
||||
export interface ApiAccountWarningJSON {
|
||||
id: string;
|
||||
action: AccountWarningAction;
|
||||
text: string;
|
||||
status_ids: string[];
|
||||
created_at: string;
|
||||
target_account: ApiAccountJSON;
|
||||
appeal: unknown;
|
||||
}
|
||||
|
||||
interface ModerationWarningNotificationGroupJSON
|
||||
extends BaseNotificationGroupJSON {
|
||||
type: 'moderation_warning';
|
||||
moderation_warning: ApiAccountWarningJSON;
|
||||
}
|
||||
|
||||
interface ModerationWarningNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'moderation_warning';
|
||||
moderation_warning: ApiAccountWarningJSON;
|
||||
}
|
||||
|
||||
export interface ApiAccountRelationshipSeveranceEventJSON {
|
||||
id: string;
|
||||
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
|
||||
purged: boolean;
|
||||
target_name: string;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface AccountRelationshipSeveranceNotificationGroupJSON
|
||||
extends BaseNotificationGroupJSON {
|
||||
type: 'severed_relationships';
|
||||
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||
}
|
||||
|
||||
interface AccountRelationshipSeveranceNotificationJSON
|
||||
extends BaseNotificationJSON {
|
||||
type: 'severed_relationships';
|
||||
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||
}
|
||||
|
||||
export type ApiNotificationJSON =
|
||||
| SimpleNotificationJSON
|
||||
| ReportNotificationJSON
|
||||
| AccountRelationshipSeveranceNotificationJSON
|
||||
| NotificationWithStatusJSON
|
||||
| ModerationWarningNotificationJSON;
|
||||
|
||||
export type ApiNotificationGroupJSON =
|
||||
| SimpleNotificationGroupJSON
|
||||
| ReportNotificationGroupJSON
|
||||
| AccountRelationshipSeveranceNotificationGroupJSON
|
||||
| NotificationGroupWithStatusJSON
|
||||
| ModerationWarningNotificationGroupJSON;
|
||||
|
||||
export interface ApiNotificationGroupsResultJSON {
|
||||
accounts: ApiAccountJSON[];
|
||||
statuses: ApiStatusJSON[];
|
||||
notification_groups: ApiNotificationGroupJSON[];
|
||||
}
|
||||
|
||||
export interface ApiNotificationRequestJSON {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
notifications_count: string;
|
||||
account: ApiAccountJSON;
|
||||
last_status?: ApiStatusJSON;
|
||||
}
|
||||
23
app/javascript/mastodon/api_types/polls.ts
Normal file
23
app/javascript/mastodon/api_types/polls.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||
|
||||
// See app/serializers/rest/poll_serializer.rb
|
||||
|
||||
export interface ApiPollOptionJSON {
|
||||
title: string;
|
||||
votes_count: number;
|
||||
}
|
||||
|
||||
export interface ApiPollJSON {
|
||||
id: string;
|
||||
expires_at: string;
|
||||
expired: boolean;
|
||||
multiple: boolean;
|
||||
votes_count: number;
|
||||
voters_count: number;
|
||||
|
||||
options: ApiPollOptionJSON[];
|
||||
emojis: ApiCustomEmojiJSON[];
|
||||
|
||||
voted: boolean;
|
||||
own_votes: number[];
|
||||
}
|
||||
18
app/javascript/mastodon/api_types/relationships.ts
Normal file
18
app/javascript/mastodon/api_types/relationships.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// See app/serializers/rest/relationship_serializer.rb
|
||||
export interface ApiRelationshipJSON {
|
||||
blocked_by: boolean;
|
||||
blocking: boolean;
|
||||
domain_blocking: boolean;
|
||||
endorsed: boolean;
|
||||
followed_by: boolean;
|
||||
following: boolean;
|
||||
id: string;
|
||||
languages: string[] | null;
|
||||
muting_notifications: boolean;
|
||||
muting: boolean;
|
||||
note: string;
|
||||
notifying: boolean;
|
||||
requested_by: boolean;
|
||||
requested: boolean;
|
||||
showing_reblogs: boolean;
|
||||
}
|
||||
16
app/javascript/mastodon/api_types/reports.ts
Normal file
16
app/javascript/mastodon/api_types/reports.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { ApiAccountJSON } from './accounts';
|
||||
|
||||
export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation';
|
||||
|
||||
export interface ApiReportJSON {
|
||||
id: string;
|
||||
action_taken: unknown;
|
||||
action_taken_at: unknown;
|
||||
category: ReportCategory;
|
||||
comment: string;
|
||||
forwarded: boolean;
|
||||
created_at: string;
|
||||
status_ids: string[];
|
||||
rule_ids: string[];
|
||||
target_account: ApiAccountJSON;
|
||||
}
|
||||
121
app/javascript/mastodon/api_types/statuses.ts
Normal file
121
app/javascript/mastodon/api_types/statuses.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// See app/serializers/rest/status_serializer.rb
|
||||
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||
import type { ApiMediaAttachmentJSON } from './media_attachments';
|
||||
import type { ApiPollJSON } from './polls';
|
||||
|
||||
// See app/modals/status.rb
|
||||
export type StatusVisibility =
|
||||
| 'public'
|
||||
| 'unlisted'
|
||||
| 'private'
|
||||
// | 'limited' // This is never exposed to the API (they become `private`)
|
||||
| 'direct';
|
||||
|
||||
export interface ApiStatusApplicationJSON {
|
||||
name: string;
|
||||
website: string;
|
||||
}
|
||||
|
||||
export interface ApiTagJSON {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ApiMentionJSON {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
}
|
||||
|
||||
export interface ApiPreviewCardAuthorJSON {
|
||||
name: string;
|
||||
url: string;
|
||||
account?: ApiAccountJSON;
|
||||
}
|
||||
|
||||
export interface ApiPreviewCardJSON {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
language: string;
|
||||
type: string;
|
||||
author_name: string;
|
||||
author_url: string;
|
||||
author_account?: ApiAccountJSON;
|
||||
provider_name: string;
|
||||
provider_url: string;
|
||||
html: string;
|
||||
width: number;
|
||||
height: number;
|
||||
image: string;
|
||||
image_description: string;
|
||||
embed_url: string;
|
||||
blurhash: string;
|
||||
published_at: string;
|
||||
authors: ApiPreviewCardAuthorJSON[];
|
||||
}
|
||||
|
||||
export type FilterContext =
|
||||
| 'home'
|
||||
| 'notifications'
|
||||
| 'public'
|
||||
| 'thread'
|
||||
| 'account';
|
||||
|
||||
export interface ApiFilterJSON {
|
||||
id: string;
|
||||
title: string;
|
||||
context: FilterContext;
|
||||
expires_at: string;
|
||||
filter_action: 'warn' | 'hide';
|
||||
keywords?: unknown[]; // TODO: FilterKeywordSerializer
|
||||
statuses?: unknown[]; // TODO: FilterStatusSerializer
|
||||
}
|
||||
|
||||
export interface ApiFilterResultJSON {
|
||||
filter: ApiFilterJSON;
|
||||
keyword_matches: string[];
|
||||
status_matches: string[];
|
||||
}
|
||||
|
||||
export interface ApiStatusJSON {
|
||||
id: string;
|
||||
created_at: string;
|
||||
in_reply_to_id?: string;
|
||||
in_reply_to_account_id?: string;
|
||||
sensitive: boolean;
|
||||
spoiler_text?: string;
|
||||
visibility: StatusVisibility;
|
||||
language: string;
|
||||
uri: string;
|
||||
url: string;
|
||||
replies_count: number;
|
||||
reblogs_count: number;
|
||||
favorites_count: number;
|
||||
edited_at?: string;
|
||||
|
||||
favorited?: boolean;
|
||||
reblogged?: boolean;
|
||||
muted?: boolean;
|
||||
bookmarked?: boolean;
|
||||
pinned?: boolean;
|
||||
|
||||
filtered?: ApiFilterResultJSON[];
|
||||
content?: string;
|
||||
text?: string;
|
||||
|
||||
reblog?: ApiStatusJSON;
|
||||
application?: ApiStatusApplicationJSON;
|
||||
account: ApiAccountJSON;
|
||||
media_attachments: ApiMediaAttachmentJSON[];
|
||||
mentions: ApiMentionJSON[];
|
||||
|
||||
tags: ApiTagJSON[];
|
||||
emojis: ApiCustomEmojiJSON[];
|
||||
|
||||
card?: ApiPreviewCardJSON;
|
||||
poll?: ApiPollJSON;
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import Rails from '@rails/ujs';
|
||||
import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
export function start() {
|
||||
require.context('../images/', true);
|
||||
require.context('../images/', true, /\.(jpg|png|svg)$/);
|
||||
|
||||
try {
|
||||
Rails.start();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// If called twice
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
|
|||
className="emojione"
|
||||
src="http://example.com/emoji.png"
|
||||
/>
|
||||
:foobar:
|
||||
<div
|
||||
className="autosuggest-emoji__name"
|
||||
>
|
||||
:foobar:
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -22,6 +26,10 @@ exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
|
|||
className="emojione"
|
||||
src="/emoji/1f499.svg"
|
||||
/>
|
||||
:foobar:
|
||||
<div
|
||||
className="autosuggest-emoji__name"
|
||||
>
|
||||
:foobar:
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
|||
}
|
||||
>
|
||||
<img
|
||||
alt="alice"
|
||||
alt=""
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -32,7 +32,7 @@ exports[`<Avatar /> Still renders a still avatar 1`] = `
|
|||
}
|
||||
>
|
||||
<img
|
||||
alt="alice"
|
||||
alt=""
|
||||
src="/static/alice.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import Button from '../button';
|
||||
import { render, fireEvent, screen } from 'mastodon/test_helpers';
|
||||
|
||||
import { Button } from '../button';
|
||||
|
||||
describe('<Button />', () => {
|
||||
it('renders a button element', () => {
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ describe('computeHashtagBarForStatus', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
|
||||
it('does not put the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
|
||||
const status = createStatus(
|
||||
'<p>This is my content! <a href="test">#hashtag</a></p>',
|
||||
['hashtag'],
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
|
||||
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||
import { me } from '../initial_state';
|
||||
|
||||
import { Avatar } from './avatar';
|
||||
import Button from './button';
|
||||
import { Button } from './button';
|
||||
import { FollowersCounter } from './counters';
|
||||
import { DisplayName } from './display_name';
|
||||
import { IconButton } from './icon_button';
|
||||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
@ -31,160 +32,150 @@ const messages = defineMessages({
|
|||
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
|
||||
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
||||
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
static propTypes = {
|
||||
size: PropTypes.number,
|
||||
account: ImmutablePropTypes.map,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onMuteNotifications: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
actionIcon: PropTypes.string,
|
||||
actionTitle: PropTypes.string,
|
||||
defaultAction: PropTypes.string,
|
||||
onActionClick: PropTypes.func,
|
||||
withBio: PropTypes.bool,
|
||||
};
|
||||
const handleFollow = useCallback(() => {
|
||||
onFollow(account);
|
||||
}, [onFollow, account]);
|
||||
|
||||
static defaultProps = {
|
||||
size: 46,
|
||||
};
|
||||
const handleBlock = useCallback(() => {
|
||||
onBlock(account);
|
||||
}, [onBlock, account]);
|
||||
|
||||
handleFollow = () => {
|
||||
this.props.onFollow(this.props.account);
|
||||
};
|
||||
const handleMute = useCallback(() => {
|
||||
onMute(account);
|
||||
}, [onMute, account]);
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
};
|
||||
const handleMuteNotifications = useCallback(() => {
|
||||
onMuteNotifications(account, true);
|
||||
}, [onMuteNotifications, account]);
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
};
|
||||
const handleUnmuteNotifications = useCallback(() => {
|
||||
onMuteNotifications(account, false);
|
||||
}, [onMuteNotifications, account]);
|
||||
|
||||
handleMuteNotifications = () => {
|
||||
this.props.onMuteNotifications(this.props.account, true);
|
||||
};
|
||||
|
||||
handleUnmuteNotifications = () => {
|
||||
this.props.onMuteNotifications(this.props.account, false);
|
||||
};
|
||||
|
||||
handleAction = () => {
|
||||
this.props.onActionClick(this.props.account);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <EmptyAccount size={size} minimal={minimal} />;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<>
|
||||
{account.get('display_name')}
|
||||
{account.get('username')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let buttons;
|
||||
|
||||
if (actionIcon && onActionClick) {
|
||||
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
||||
} else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
|
||||
} else if (blocking) {
|
||||
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
} else if (muting) {
|
||||
let hidingNotificationsButton;
|
||||
|
||||
if (account.getIn(['relationship', 'muting_notifications'])) {
|
||||
hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
|
||||
} else {
|
||||
hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
|
||||
}
|
||||
|
||||
buttons = (
|
||||
<>
|
||||
<Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
|
||||
{hidingNotificationsButton}
|
||||
</>
|
||||
);
|
||||
} else if (defaultAction === 'mute') {
|
||||
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
|
||||
} else if (!account.get('moved') || following) {
|
||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
}
|
||||
}
|
||||
|
||||
let muteTimeRemaining;
|
||||
|
||||
if (account.get('mute_expires_at')) {
|
||||
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
|
||||
}
|
||||
|
||||
let verification;
|
||||
|
||||
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
|
||||
|
||||
if (firstVerifiedField) {
|
||||
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
||||
}
|
||||
if (!account) {
|
||||
return <EmptyAccount size={size} minimal={minimal} />;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||
<div className='account__wrapper'>
|
||||
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar account={account} size={size} />
|
||||
</div>
|
||||
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
{!minimal && (
|
||||
<div className='account__details'>
|
||||
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{!minimal && (
|
||||
<div className='account__relationship'>
|
||||
{buttons}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{withBio && (account.get('note').length > 0 ? (
|
||||
<div
|
||||
className='account__note translate'
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
) : (
|
||||
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
{account.get('display_name')}
|
||||
{account.get('username')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
let buttons;
|
||||
|
||||
export default injectIntl(Account);
|
||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={handleFollow} />;
|
||||
} else if (blocking) {
|
||||
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
|
||||
} else if (muting) {
|
||||
let menu;
|
||||
|
||||
if (account.getIn(['relationship', 'muting_notifications'])) {
|
||||
menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
|
||||
} else {
|
||||
menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
|
||||
}
|
||||
|
||||
buttons = (
|
||||
<>
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
|
||||
<Button text={intl.formatMessage(messages.unmute)} onClick={handleMute} />
|
||||
</>
|
||||
);
|
||||
} else if (defaultAction === 'mute') {
|
||||
buttons = <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />;
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
|
||||
} else if (!account.get('suspended') && !account.get('moved') || following) {
|
||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={handleFollow} />;
|
||||
}
|
||||
}
|
||||
|
||||
let muteTimeRemaining;
|
||||
|
||||
if (account.get('mute_expires_at')) {
|
||||
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
|
||||
}
|
||||
|
||||
let verification;
|
||||
|
||||
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
|
||||
|
||||
if (firstVerifiedField) {
|
||||
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||
<div className='account__wrapper'>
|
||||
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`} data-hover-card-account={account.get('id')}>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar account={account} size={size} />
|
||||
</div>
|
||||
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
{!minimal && (
|
||||
<div className='account__details'>
|
||||
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{!minimal && (
|
||||
<div className='account__relationship'>
|
||||
{buttons}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{withBio && (account.get('note').length > 0 ? (
|
||||
<div
|
||||
className='account__note translate'
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
) : (
|
||||
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Account.propTypes = {
|
||||
size: PropTypes.number,
|
||||
account: ImmutablePropTypes.record,
|
||||
onFollow: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
onMuteNotifications: PropTypes.func,
|
||||
hidden: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
defaultAction: PropTypes.string,
|
||||
withBio: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Account;
|
||||
|
|
|
|||
20
app/javascript/mastodon/components/account_bio.tsx
Normal file
20
app/javascript/mastodon/components/account_bio.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
||||
|
||||
export const AccountBio: React.FC<{
|
||||
note: string;
|
||||
className: string;
|
||||
}> = ({ note, className }) => {
|
||||
const handleClick = useLinks();
|
||||
|
||||
if (note.length === 0 || note === '<p></p>') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${className} translate`}
|
||||
dangerouslySetInnerHTML={{ __html: note }}
|
||||
onClickCapture={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
42
app/javascript/mastodon/components/account_fields.tsx
Normal file
42
app/javascript/mastodon/components/account_fields.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
export const AccountFields: React.FC<{
|
||||
fields: Account['fields'];
|
||||
limit: number;
|
||||
}> = ({ fields, limit = -1 }) => {
|
||||
const handleClick = useLinks();
|
||||
|
||||
if (fields.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account-fields' onClickCapture={handleClick}>
|
||||
{fields.take(limit).map((pair, i) => (
|
||||
<dl
|
||||
key={i}
|
||||
className={classNames({ verified: pair.get('verified_at') })}
|
||||
>
|
||||
<dt
|
||||
dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }}
|
||||
className='translate'
|
||||
/>
|
||||
|
||||
<dd className='translate' title={pair.get('value_plain') ?? ''}>
|
||||
{pair.get('verified_at') && (
|
||||
<Icon id='check' icon={CheckIcon} className='verified__mark' />
|
||||
)}
|
||||
<span
|
||||
dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }}
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -48,7 +48,7 @@ export default class Counter extends PureComponent {
|
|||
componentDidMount () {
|
||||
const { measure, start_at, end_at, params } = this.props;
|
||||
|
||||
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
|
||||
api(false).post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default class Dimension extends PureComponent {
|
|||
componentDidMount () {
|
||||
const { start_at, end_at, dimension, limit, params } = this.props;
|
||||
|
||||
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
|
||||
api(false).post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class ImpactReport extends PureComponent {
|
|||
include_subdomains: true,
|
||||
};
|
||||
|
||||
api().post('/api/v1/admin/measures', {
|
||||
api(false).post('/api/v1/admin/measures', {
|
||||
keys: ['instance_accounts', 'instance_follows', 'instance_followers'],
|
||||
start_at: null,
|
||||
end_at: null,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class ReportReasonSelector extends PureComponent {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
api().get('/api/v1/instance').then(res => {
|
||||
api(false).get('/api/v1/instance').then(res => {
|
||||
this.setState({
|
||||
rules: res.data.rules,
|
||||
});
|
||||
|
|
@ -122,7 +122,7 @@ class ReportReasonSelector extends PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
api().put(`/api/v1/admin/reports/${id}`, {
|
||||
api(false).put(`/api/v1/admin/reports/${id}`, {
|
||||
category,
|
||||
rule_ids: category === 'violation' ? rule_ids : [],
|
||||
}).catch(err => {
|
||||
|
|
@ -153,7 +153,7 @@ class ReportReasonSelector extends PureComponent {
|
|||
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
|
||||
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
|
||||
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
|
||||
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
|
||||
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled || rules.length === 0}>
|
||||
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
|
||||
</Category>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default class Retention extends PureComponent {
|
|||
componentDidMount () {
|
||||
const { start_at, end_at, frequency } = this.props;
|
||||
|
||||
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
|
||||
api(false).post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
|
|
@ -51,7 +51,7 @@ export default class Retention extends PureComponent {
|
|||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
|
||||
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading…' />;
|
||||
} else {
|
||||
content = (
|
||||
<table className='retention__table'>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import api from 'mastodon/api';
|
||||
import Hashtag from 'mastodon/components/hashtag';
|
||||
import { Hashtag } from 'mastodon/components/hashtag';
|
||||
|
||||
export default class Trends extends PureComponent {
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ export default class Trends extends PureComponent {
|
|||
componentDidMount () {
|
||||
const { limit } = this.props;
|
||||
|
||||
api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
|
||||
api(false).get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
|
|
|
|||
67
app/javascript/mastodon/components/alt_text_badge.tsx
Normal file
67
app/javascript/mastodon/components/alt_text_badge.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useState, useCallback, useRef } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import type {
|
||||
OffsetValue,
|
||||
UsePopperOptions,
|
||||
} from 'react-overlays/esm/usePopper';
|
||||
|
||||
const offset = [0, 4] as OffsetValue;
|
||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||
|
||||
export const AltTextBadge: React.FC<{
|
||||
description: string;
|
||||
}> = ({ description }) => {
|
||||
const anchorRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen((v) => !v);
|
||||
}, [setOpen]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={anchorRef}
|
||||
className='media-gallery__alt__label'
|
||||
onClick={handleClick}
|
||||
>
|
||||
ALT
|
||||
</button>
|
||||
|
||||
<Overlay
|
||||
rootClose
|
||||
onHide={handleClose}
|
||||
show={open}
|
||||
target={anchorRef.current}
|
||||
placement='top-end'
|
||||
flip
|
||||
offset={offset}
|
||||
popperConfig={popperConfig}
|
||||
>
|
||||
{({ props }) => (
|
||||
<div {...props} className='hover-card-controller'>
|
||||
<div
|
||||
className='media-gallery__alt__popover dropdown-animation'
|
||||
role='tooltip'
|
||||
>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id='alt_text_badge.title'
|
||||
defaultMessage='Alt text'
|
||||
/>
|
||||
</h4>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -48,8 +48,9 @@ export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
|||
<span
|
||||
key={key}
|
||||
style={{
|
||||
position: direction * style.y > 0 ? 'absolute' : 'static',
|
||||
transform: `translateY(${style.y * 100}%)`,
|
||||
position:
|
||||
direction * (style.y ?? 0) > 0 ? 'absolute' : 'static',
|
||||
transform: `translateY(${(style.y ?? 0) * 100}%)`,
|
||||
}}
|
||||
>
|
||||
<ShortNumber value={data as number} />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
|
||||
|
|
@ -25,7 +26,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
<div className={classNames('attachment-list', { compact })}>
|
||||
{!compact && (
|
||||
<div className='attachment-list__icon'>
|
||||
<Icon id='link' />
|
||||
<Icon id='link' icon={LinkIcon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>
|
||||
{compact && <Icon id='link' />}
|
||||
{compact && <Icon id='link' icon={LinkIcon} />}
|
||||
{compact && ' ' }
|
||||
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { PureComponent } from 'react';
|
|||
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
|
||||
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
||||
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
|
||||
|
||||
export default class AutosuggestEmoji extends PureComponent {
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ export default class AutosuggestEmoji extends PureComponent {
|
|||
alt={emoji.native || emoji.colons}
|
||||
/>
|
||||
|
||||
{emoji.colons}
|
||||
<div className='autosuggest-emoji__name'>{emoji.colons}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -16,27 +14,18 @@ interface Props {
|
|||
};
|
||||
}
|
||||
|
||||
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
|
||||
const weeklyUses = tag.history && (
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<FormattedMessage
|
||||
id='autosuggest_hashtag.per_week'
|
||||
defaultMessage='{count} per week'
|
||||
values={{ count: weeklyUses }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
|
|
@ -195,34 +197,37 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='autosuggest-input'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
<input
|
||||
type='text'
|
||||
ref={this.setInput}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
aria-label={placeholder}
|
||||
id={id}
|
||||
className={className}
|
||||
maxLength={maxLength}
|
||||
lang={lang}
|
||||
spellCheck={spellCheck}
|
||||
/>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
ref={this.setInput}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
id={id}
|
||||
className={className}
|
||||
maxLength={maxLength}
|
||||
lang={lang}
|
||||
spellCheck={spellCheck}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback, useRef, useState, useEffect, forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
|
@ -37,54 +38,45 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
|||
}
|
||||
};
|
||||
|
||||
export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||
const AutosuggestTextarea = forwardRef(({
|
||||
value,
|
||||
suggestions,
|
||||
disabled,
|
||||
placeholder,
|
||||
onSuggestionSelected,
|
||||
onSuggestionsClearRequested,
|
||||
onSuggestionsFetchRequested,
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onPaste,
|
||||
onFocus,
|
||||
autoFocus = true,
|
||||
lang,
|
||||
}, textareaRef) => {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
||||
const lastTokenRef = useRef(null);
|
||||
const tokenStartRef = useRef(0);
|
||||
|
||||
static defaultProps = {
|
||||
autoFocus: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
suggestionsHidden: true,
|
||||
focused: false,
|
||||
selectedSuggestion: 0,
|
||||
lastToken: null,
|
||||
tokenStart: 0,
|
||||
};
|
||||
|
||||
onChange = (e) => {
|
||||
const handleChange = useCallback((e) => {
|
||||
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||
|
||||
if (token !== null && this.state.lastToken !== token) {
|
||||
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
|
||||
this.props.onSuggestionsFetchRequested(token);
|
||||
if (token !== null && lastTokenRef.current !== token) {
|
||||
tokenStartRef.current = tokenStart;
|
||||
lastTokenRef.current = token;
|
||||
setSelectedSuggestion(0);
|
||||
onSuggestionsFetchRequested(token);
|
||||
} else if (token === null) {
|
||||
this.setState({ lastToken: null });
|
||||
this.props.onSuggestionsClearRequested();
|
||||
lastTokenRef.current = null;
|
||||
onSuggestionsClearRequested();
|
||||
}
|
||||
|
||||
this.props.onChange(e);
|
||||
};
|
||||
|
||||
onKeyDown = (e) => {
|
||||
const { suggestions, disabled } = this.props;
|
||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
||||
onChange(e);
|
||||
}, [onSuggestionsFetchRequested, onSuggestionsClearRequested, onChange, setSelectedSuggestion]);
|
||||
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
|
|
@ -102,80 +94,75 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
document.querySelector('.ui').parentElement.focus();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
this.setState({ suggestionsHidden: true });
|
||||
setSuggestionsHidden(true);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
|
||||
setSelectedSuggestion(Math.min(selectedSuggestion + 1, suggestions.size - 1));
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
|
||||
setSelectedSuggestion(Math.max(selectedSuggestion - 1, 0));
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
// Select suggestion
|
||||
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||
if (lastTokenRef.current !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
||||
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestions.get(selectedSuggestion));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (e.defaultPrevented || !this.props.onKeyDown) {
|
||||
if (e.defaultPrevented || !onKeyDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onKeyDown(e);
|
||||
};
|
||||
onKeyDown(e);
|
||||
}, [disabled, suggestions, suggestionsHidden, selectedSuggestion, setSelectedSuggestion, setSuggestionsHidden, onSuggestionSelected, onKeyDown]);
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ suggestionsHidden: true, focused: false });
|
||||
};
|
||||
const handleBlur = useCallback(() => {
|
||||
setSuggestionsHidden(true);
|
||||
}, [setSuggestionsHidden]);
|
||||
|
||||
onFocus = (e) => {
|
||||
this.setState({ focused: true });
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(e);
|
||||
const handleFocus = useCallback((e) => {
|
||||
if (onFocus) {
|
||||
onFocus(e);
|
||||
}
|
||||
};
|
||||
}, [onFocus]);
|
||||
|
||||
onSuggestionClick = (e) => {
|
||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||
const handleSuggestionClick = useCallback((e) => {
|
||||
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.textarea.focus();
|
||||
};
|
||||
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestion);
|
||||
textareaRef.current?.focus();
|
||||
}, [suggestions, onSuggestionSelected, textareaRef]);
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
}
|
||||
|
||||
setTextarea = (c) => {
|
||||
this.textarea = c;
|
||||
};
|
||||
|
||||
onPaste = (e) => {
|
||||
const handlePaste = useCallback((e) => {
|
||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||
this.props.onPaste(e.clipboardData.files);
|
||||
onPaste(e.clipboardData.files);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
}, [onPaste]);
|
||||
|
||||
renderSuggestion = (suggestion, i) => {
|
||||
const { selectedSuggestion } = this.state;
|
||||
// Show the suggestions again whenever they change and the textarea is focused
|
||||
useEffect(() => {
|
||||
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
|
||||
setSuggestionsHidden(false);
|
||||
}
|
||||
}, [suggestions, textareaRef, setSuggestionsHidden]);
|
||||
|
||||
const renderSuggestion = (suggestion, i) => {
|
||||
let inner, key;
|
||||
|
||||
if (suggestion.type === 'emoji') {
|
||||
|
|
@ -190,50 +177,61 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
|
||||
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={handleSuggestionClick}>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
return (
|
||||
<div className='autosuggest-textarea'>
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
aria-label={placeholder}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
return [
|
||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||
<div className='autosuggest-textarea'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
|
||||
{suggestions.map(renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
<Textarea
|
||||
ref={this.setTextarea}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{children}
|
||||
</div>,
|
||||
AutosuggestTextarea.propTypes = {
|
||||
value: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onFocus:PropTypes.func,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
export default AutosuggestTextarea;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
import { useHovering } from '../../hooks/useHovering';
|
||||
import type { Account } from '../../types/resources';
|
||||
import { autoPlayGif } from '../initial_state';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -10,6 +11,8 @@ interface Props {
|
|||
style?: React.CSSProperties;
|
||||
inline?: boolean;
|
||||
animate?: boolean;
|
||||
counter?: number | string;
|
||||
counterBorderColor?: string;
|
||||
}
|
||||
|
||||
export const Avatar: React.FC<Props> = ({
|
||||
|
|
@ -18,6 +21,8 @@ export const Avatar: React.FC<Props> = ({
|
|||
size = 20,
|
||||
inline = false,
|
||||
style: styleFromParent,
|
||||
counter,
|
||||
counterBorderColor,
|
||||
}) => {
|
||||
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
||||
|
||||
|
|
@ -41,7 +46,15 @@ export const Avatar: React.FC<Props> = ({
|
|||
onMouseLeave={handleMouseLeave}
|
||||
style={style}
|
||||
>
|
||||
{src && <img src={src} alt={account?.get('acct')} />}
|
||||
{src && <img src={src} alt='' />}
|
||||
{counter && (
|
||||
<div
|
||||
className='account__avatar__counter'
|
||||
style={{ borderColor: counterBorderColor }}
|
||||
>
|
||||
{counter}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
import { useHovering } from '../../hooks/useHovering';
|
||||
import type { Account } from '../../types/resources';
|
||||
import { autoPlayGif } from '../initial_state';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { ReactComponent as GroupsIcon } from '@material-design-icons/svg/outlined/group.svg';
|
||||
import { ReactComponent as PersonIcon } from '@material-design-icons/svg/outlined/person.svg';
|
||||
import { ReactComponent as SmartToyIcon } from '@material-design-icons/svg/outlined/smart_toy.svg';
|
||||
import GroupsIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
|
||||
import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react';
|
||||
|
||||
|
||||
export const Badge = ({ icon, label, domain }) => (
|
||||
<div className='account-role'>
|
||||
export const Badge = ({ icon = <PersonIcon />, label, domain, roleId }) => (
|
||||
<div className='account-role' data-account-role-id={roleId}>
|
||||
{icon}
|
||||
{label}
|
||||
{domain && <span className='account-role__domain'>{domain}</span>}
|
||||
|
|
@ -19,10 +19,7 @@ Badge.propTypes = {
|
|||
icon: PropTypes.node,
|
||||
label: PropTypes.node,
|
||||
domain: PropTypes.node,
|
||||
};
|
||||
|
||||
Badge.defaultProps = {
|
||||
icon: <PersonIcon />,
|
||||
roleId: PropTypes.string
|
||||
};
|
||||
|
||||
export const GroupBadge = () => (
|
||||
|
|
@ -31,4 +28,4 @@ export const GroupBadge = () => (
|
|||
|
||||
export const AutomatedBadge = () => (
|
||||
<Badge icon={<SmartToyIcon />} label={<FormattedMessage id='account.badges.bot' defaultMessage='Automated' />} />
|
||||
);
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class Button extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.node,
|
||||
type: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
block: PropTypes.bool,
|
||||
secondary: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
type: 'button',
|
||||
};
|
||||
|
||||
handleClick = (e) => {
|
||||
if (!this.props.disabled && this.props.onClick) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
focus() {
|
||||
this.node.focus();
|
||||
}
|
||||
|
||||
render () {
|
||||
const className = classNames('button', this.props.className, {
|
||||
'button-secondary': this.props.secondary,
|
||||
'button--block': this.props.block,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.handleClick}
|
||||
ref={this.setRef}
|
||||
title={this.props.title}
|
||||
type={this.props.type}
|
||||
>
|
||||
{this.props.text || this.props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
62
app/javascript/mastodon/components/button.tsx
Normal file
62
app/javascript/mastodon/components/button.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface BaseProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||
block?: boolean;
|
||||
secondary?: boolean;
|
||||
dangerous?: boolean;
|
||||
}
|
||||
|
||||
interface PropsChildren extends PropsWithChildren<BaseProps> {
|
||||
text?: undefined;
|
||||
}
|
||||
|
||||
interface PropsWithText extends BaseProps {
|
||||
text: JSX.Element | string;
|
||||
children?: undefined;
|
||||
}
|
||||
|
||||
type Props = PropsWithText | PropsChildren;
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
type = 'button',
|
||||
onClick,
|
||||
disabled,
|
||||
block,
|
||||
secondary,
|
||||
dangerous,
|
||||
className,
|
||||
title,
|
||||
text,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const handleClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
|
||||
(e) => {
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
},
|
||||
[disabled, onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames('button', className, {
|
||||
'button-secondary': secondary,
|
||||
'button--block': block,
|
||||
'button--dangerous': dangerous,
|
||||
})}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
type={type}
|
||||
{...props}
|
||||
>
|
||||
{text ?? children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export const Check: React.FC = () => (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 20 20'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
48
app/javascript/mastodon/components/check_box.tsx
Normal file
48
app/javascript/mastodon/components/check_box.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
import CheckIndeterminateSmallIcon from '@/material-icons/400-24px/check_indeterminate_small.svg?react';
|
||||
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||
|
||||
import { Icon } from './icon';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
name: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CheckBox: React.FC<Props> = ({
|
||||
name,
|
||||
value,
|
||||
checked,
|
||||
indeterminate,
|
||||
onChange,
|
||||
label,
|
||||
}) => {
|
||||
return (
|
||||
<label className='check-box'>
|
||||
<input
|
||||
name={name}
|
||||
type='checkbox'
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={classNames('check-box__input', { checked, indeterminate })}
|
||||
>
|
||||
{indeterminate ? (
|
||||
<Icon id='indeterminate' icon={CheckIndeterminateSmallIcon} />
|
||||
) : (
|
||||
checked && <Icon id='check' icon={DoneIcon} />
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
|
@ -22,6 +22,12 @@ export default class Column extends PureComponent {
|
|||
scrollable = document.scrollingElement;
|
||||
} else {
|
||||
scrollable = this.node.querySelector('.scrollable');
|
||||
|
||||
// Some columns have nested `.scrollable` containers, with the outer one
|
||||
// being a wrapper while the actual scrollable content is deeper.
|
||||
if (scrollable.classList.contains('scrollable--flex')) {
|
||||
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
|
||||
}
|
||||
}
|
||||
|
||||
if (!scrollable) {
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
export default class ColumnBackButton extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { router } = this.context;
|
||||
const { onClick } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else if (router.history.location?.state?.fromMastodon) {
|
||||
router.history.goBack();
|
||||
} else {
|
||||
router.history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { multiColumn } = this.props;
|
||||
|
||||
const component = (
|
||||
<button onClick={this.handleClick} className='column-back-button'>
|
||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>
|
||||
);
|
||||
|
||||
if (multiColumn) {
|
||||
return component;
|
||||
} else {
|
||||
// The portal container and the component may be rendered to the DOM in
|
||||
// the same React render pass, so the container might not be available at
|
||||
// the time `render()` is called.
|
||||
const container = document.getElementById('tabs-bar__portal');
|
||||
if (container === null) {
|
||||
// The container wasn't available, force a re-render so that the
|
||||
// component can eventually be inserted in the container and not scroll
|
||||
// with the rest of the area.
|
||||
this.forceUpdate();
|
||||
return component;
|
||||
} else {
|
||||
return createPortal(component, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
app/javascript/mastodon/components/column_back_button.tsx
Normal file
44
app/javascript/mastodon/components/column_back_button.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
|
||||
import { useAppHistory } from './router';
|
||||
|
||||
type OnClickCallback = () => void;
|
||||
|
||||
function useHandleClick(onClick?: OnClickCallback) {
|
||||
const history = useAppHistory();
|
||||
|
||||
return useCallback(() => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else if (history.location.state?.fromMastodon) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push('/');
|
||||
}
|
||||
}, [history, onClick]);
|
||||
}
|
||||
|
||||
export const ColumnBackButton: React.FC<{ onClick: OnClickCallback }> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
const handleClick = useHandleClick(onClick);
|
||||
|
||||
const component = (
|
||||
<button onClick={handleClick} className='column-back-button'>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
className='column-back-button__icon'
|
||||
/>
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>
|
||||
);
|
||||
|
||||
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import ColumnBackButton from './column_back_button';
|
||||
|
||||
export default class ColumnBackButtonSlim extends ColumnBackButton {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='column-back-button--slim'>
|
||||
<div role='button' tabIndex={0} onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
|
||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue