mirror of
https://iceshrimp.dev/blueb/Chuckya-fe-standalone.git
synced 2026-01-11 13:33:21 -08:00
Merge remote-tracking branch 'upstream/main' into standalone
This commit is contained in:
commit
29f89db19d
1243 changed files with 26053 additions and 17758 deletions
|
|
@ -1,18 +1,10 @@
|
|||
import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships';
|
||||
import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions';
|
||||
import { apiSubmitAccountNote } from 'flavours/glitch/api/accounts';
|
||||
import { createDataLoadingThunk } from 'flavours/glitch/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 }) => {
|
||||
const response = await api(getState).post<ApiRelationshipJSON>(
|
||||
`/api/v1/accounts/${args.id}/note`,
|
||||
{
|
||||
comment: args.value,
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship: response.data };
|
||||
},
|
||||
({ accountId, note }: { accountId: string; note: string }) =>
|
||||
apiSubmitAccountNote(accountId, note),
|
||||
(relationship) => ({ relationship }),
|
||||
{ skipLoading: true },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -89,11 +89,11 @@ 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 => {
|
||||
|
|
@ -102,10 +102,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());
|
||||
|
|
@ -159,7 +159,7 @@ export function followAccount(id, options = { reblogs: true }) {
|
|||
|
||||
dispatch(followAccountRequest({ id, locked }));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||
api().post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||
dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing}));
|
||||
}).catch(error => {
|
||||
dispatch(followAccountFail({ id, error, locked }));
|
||||
|
|
@ -171,7 +171,7 @@ export function unfollowAccount(id) {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(unfollowAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
|
||||
api().post(`/api/v1/accounts/${id}/unfollow`).then(response => {
|
||||
dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')}));
|
||||
}).catch(error => {
|
||||
dispatch(unfollowAccountFail({ id, error }));
|
||||
|
|
@ -183,7 +183,7 @@ 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({ relationship: response.data, statuses: getState().get('statuses') }));
|
||||
}).catch(error => {
|
||||
|
|
@ -193,10 +193,10 @@ export function blockAccount(id) {
|
|||
}
|
||||
|
||||
export function unblockAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unblockAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
|
||||
api().post(`/api/v1/accounts/${id}/unblock`).then(response => {
|
||||
dispatch(unblockAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(unblockAccountFail({ id, error }));
|
||||
|
|
@ -236,7 +236,7 @@ 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({ relationship: response.data, statuses: getState().get('statuses') }));
|
||||
}).catch(error => {
|
||||
|
|
@ -246,10 +246,10 @@ export function muteAccount(id, notifications, duration=0) {
|
|||
}
|
||||
|
||||
export function unmuteAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(unmuteAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
|
||||
api().post(`/api/v1/accounts/${id}/unmute`).then(response => {
|
||||
dispatch(unmuteAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(unmuteAccountFail({ id, error }));
|
||||
|
|
@ -287,10 +287,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));
|
||||
|
|
@ -337,7 +337,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));
|
||||
|
|
@ -374,10 +374,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));
|
||||
|
|
@ -424,7 +424,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));
|
||||
|
|
@ -473,7 +473,7 @@ export function fetchRelationships(accountIds) {
|
|||
|
||||
dispatch(fetchRelationshipsRequest(newAccountIds));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||
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));
|
||||
|
|
@ -499,10 +499,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));
|
||||
|
|
@ -541,7 +541,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));
|
||||
|
|
@ -571,10 +571,10 @@ 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 })))
|
||||
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
|
||||
|
|
@ -598,10 +598,10 @@ 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 })))
|
||||
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
|
||||
|
|
@ -624,10 +624,10 @@ 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 => {
|
||||
api().post(`/api/v1/accounts/${id}/pin`).then(response => {
|
||||
dispatch(pinAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(pinAccountFail(error));
|
||||
|
|
@ -636,10 +636,10 @@ 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 => {
|
||||
api().post(`/api/v1/accounts/${id}/unpin`).then(response => {
|
||||
dispatch(unpinAccountSuccess({ relationship: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(unpinAccountFail(error));
|
||||
|
|
@ -676,10 +676,10 @@ export function unpinAccountFail(error) {
|
|||
}
|
||||
|
||||
export function fetchPinnedAccounts() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchPinnedAccountsRequest());
|
||||
|
||||
api(getState).get('/api/v1/endorsements', { params: { limit: 0 } }).then(response => {
|
||||
api().get('/api/v1/endorsements', { params: { limit: 0 } }).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchPinnedAccountsSuccess(response.data));
|
||||
}).catch(err => dispatch(fetchPinnedAccountsFail(err)));
|
||||
|
|
@ -707,7 +707,7 @@ export function fetchPinnedAccountsFail(error) {
|
|||
};
|
||||
}
|
||||
|
||||
export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch, getState) => {
|
||||
export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch) => {
|
||||
const data = new FormData();
|
||||
|
||||
data.append('display_name', displayName);
|
||||
|
|
@ -717,13 +717,13 @@ export const updateAccount = ({ displayName, note, avatar, header, discoverable,
|
|||
data.append('discoverable', discoverable);
|
||||
data.append('indexable', indexable);
|
||||
|
||||
return api(getState).patch('/api/v1/accounts/update_credentials', data).then(response => {
|
||||
return api().patch('/api/v1/accounts/update_credentials', data).then(response => {
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchPinnedAccountsSuggestions(q) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchPinnedAccountsSuggestionsRequest());
|
||||
|
||||
const params = {
|
||||
|
|
@ -733,7 +733,7 @@ export function fetchPinnedAccountsSuggestions(q) {
|
|||
following: true,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params }).then(response => {
|
||||
api().get('/api/v1/accounts/search', { params }).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchPinnedAccountsSuggestionsSuccess(q, response.data));
|
||||
}).catch(err => dispatch(fetchPinnedAccountsSuggestionsFail(err)));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
|||
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
||||
|
||||
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));
|
||||
|
|
@ -56,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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
|||
});
|
||||
}
|
||||
|
||||
api(getState).request({
|
||||
api().request({
|
||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||
method: statusId === null ? 'post' : 'put',
|
||||
data: {
|
||||
|
|
@ -317,7 +317,7 @@ export function tenorSet(options) {
|
|||
|
||||
export function uploadCompose(files, alt = '') {
|
||||
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);
|
||||
|
|
@ -332,7 +332,7 @@ export function uploadCompose(files, alt = '') {
|
|||
dispatch(uploadComposeRequest());
|
||||
|
||||
for (const [i, f] of Array.from(files).entries()) {
|
||||
if (media.size + i > 3) break;
|
||||
if (media.size + i > (uploadLimit - 1)) break;
|
||||
|
||||
resizeImage(f).then(file => {
|
||||
const data = new FormData();
|
||||
|
|
@ -341,7 +341,7 @@ export function uploadCompose(files, alt = '') {
|
|||
// Account for disparity in size of original image and resized data
|
||||
total += file.size - f.size;
|
||||
|
||||
return api(getState).post('/api/v2/media', data, {
|
||||
return api().post('/api/v2/media', data, {
|
||||
onUploadProgress: function({ loaded }){
|
||||
progress[i] = loaded;
|
||||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||
|
|
@ -358,7 +358,7 @@ export function uploadCompose(files, alt = '') {
|
|||
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, f));
|
||||
} else if (response.status === 206) {
|
||||
|
|
@ -381,7 +381,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;
|
||||
|
|
@ -389,7 +389,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));
|
||||
},
|
||||
|
|
@ -472,7 +472,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));
|
||||
|
|
@ -560,7 +560,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: {
|
||||
|
|
@ -594,7 +594,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: {
|
||||
|
|
|
|||
|
|
@ -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/flavours/glitch/actions/directory.ts
Normal file
37
app/javascript/flavours/glitch/actions/directory.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { apiGetDirectory } from 'flavours/glitch/api/directory';
|
||||
import { createDataLoadingThunk } from 'flavours/glitch/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 };
|
||||
},
|
||||
);
|
||||
|
|
@ -24,7 +24,7 @@ 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'));
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ 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 }));
|
||||
|
|
@ -80,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 => {
|
||||
|
|
@ -123,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 => {
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -68,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);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@ 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';
|
||||
|
||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||
|
|
@ -15,10 +11,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';
|
||||
|
|
@ -61,89 +53,13 @@ export const REACTION_REMOVE_REQUEST = 'REACTION_REMOVE_REQUEST';
|
|||
export const REACTION_REMOVE_SUCCESS = 'REACTION_REMOVE_SUCCESS';
|
||||
export const REACTION_REMOVE_FAIL = 'REACTION_REMOVE_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) {
|
||||
|
|
@ -153,10 +69,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 => {
|
||||
|
|
@ -216,10 +132,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) {
|
||||
|
|
@ -229,10 +145,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 => {
|
||||
|
|
@ -288,10 +204,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));
|
||||
|
|
@ -335,7 +251,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));
|
||||
|
|
@ -370,10 +286,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));
|
||||
|
|
@ -417,7 +333,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));
|
||||
|
|
@ -452,10 +368,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 => {
|
||||
|
|
@ -490,10 +406,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 => {
|
||||
|
|
|
|||
35
app/javascript/flavours/glitch/actions/interactions_typed.ts
Normal file
35
app/javascript/flavours/glitch/actions/interactions_typed.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { apiReblog, apiUnreblog } from 'flavours/glitch/api/interactions';
|
||||
import type { StatusVisibility } from 'flavours/glitch/models/status';
|
||||
import { createDataLoadingThunk } from 'flavours/glitch/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;
|
||||
},
|
||||
);
|
||||
|
|
@ -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,21 +1,24 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import type { MarkerJSON } from 'flavours/glitch/api_types/markers';
|
||||
import type { RootState } from 'flavours/glitch/store';
|
||||
import { getAccessToken } from 'flavours/glitch/initial_state';
|
||||
import type { AppDispatch, RootState } from 'flavours/glitch/store';
|
||||
import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions';
|
||||
|
||||
import api, { authorizationTokenFromState } from '../api';
|
||||
import api from '../api';
|
||||
import { compareId } from '../compare_id';
|
||||
|
||||
export const synchronouslySubmitMarkers = createAppAsyncThunk(
|
||||
'markers/submit',
|
||||
async (_args, { getState }) => {
|
||||
const accessToken = authorizationTokenFromState(getState);
|
||||
const accessToken = getAccessToken();
|
||||
const params = buildPostMarkersParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || !accessToken) {
|
||||
if (
|
||||
Object.keys(params).length === 0 ||
|
||||
!accessToken ||
|
||||
accessToken === ''
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -71,34 +74,17 @@ interface MarkerParam {
|
|||
last_read_id?: string;
|
||||
}
|
||||
|
||||
function getLastHomeId(state: RootState): string | undefined {
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||
return (
|
||||
state
|
||||
// @ts-expect-error state.timelines is not yet typed
|
||||
.getIn(['timelines', 'home', 'items'], ImmutableList())
|
||||
// @ts-expect-error state.timelines is not yet typed
|
||||
.find((item) => item !== null)
|
||||
);
|
||||
}
|
||||
|
||||
function getLastNotificationId(state: RootState): string | undefined {
|
||||
// @ts-expect-error state.notifications is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
||||
return state.getIn(['notifications', 'lastReadId']);
|
||||
}
|
||||
|
||||
const buildPostMarkersParams = (state: RootState) => {
|
||||
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
|
||||
|
||||
const lastHomeId = getLastHomeId(state);
|
||||
const lastNotificationId = getLastNotificationId(state);
|
||||
|
||||
if (lastHomeId && compareId(lastHomeId, state.markers.home) > 0) {
|
||||
params.home = {
|
||||
last_read_id: lastHomeId,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
lastNotificationId &&
|
||||
lastNotificationId !== '0' &&
|
||||
|
|
@ -116,14 +102,14 @@ export const submitMarkersAction = createAppAsyncThunk<{
|
|||
home: string | undefined;
|
||||
notifications: string | undefined;
|
||||
}>('markers/submitAction', async (_args, { getState }) => {
|
||||
const accessToken = authorizationTokenFromState(getState);
|
||||
const accessToken = getAccessToken();
|
||||
const params = buildPostMarkersParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
if (Object.keys(params).length === 0 || !accessToken || accessToken === '') {
|
||||
return { home: undefined, notifications: undefined };
|
||||
}
|
||||
|
||||
await api(getState).post<MarkerJSON>('/api/v1/markers', params);
|
||||
await api().post<MarkerJSON>('/api/v1/markers', params);
|
||||
|
||||
return {
|
||||
home: params.home?.last_read_id,
|
||||
|
|
@ -132,8 +118,8 @@ export const submitMarkersAction = createAppAsyncThunk<{
|
|||
});
|
||||
|
||||
const debouncedSubmitMarkers = debounce(
|
||||
(dispatch) => {
|
||||
dispatch(submitMarkersAction());
|
||||
(dispatch: AppDispatch) => {
|
||||
void dispatch(submitMarkersAction());
|
||||
},
|
||||
300000,
|
||||
{
|
||||
|
|
@ -153,14 +139,11 @@ export const submitMarkers = createAppAsyncThunk(
|
|||
},
|
||||
);
|
||||
|
||||
export const fetchMarkers = createAppAsyncThunk(
|
||||
'markers/fetch',
|
||||
async (_args, { getState }) => {
|
||||
const response = await api(getState).get<Record<string, MarkerJSON>>(
|
||||
`/api/v1/markers`,
|
||||
{ params: { timeline: ['notifications'] } },
|
||||
);
|
||||
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 };
|
||||
},
|
||||
);
|
||||
return { markers: response.data };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
|||
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||
|
||||
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));
|
||||
|
|
@ -56,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));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import {
|
||||
apiGetNotificationPolicy,
|
||||
apiUpdateNotificationsPolicy,
|
||||
} from 'flavours/glitch/api/notification_policies';
|
||||
import type { NotificationPolicy } from 'flavours/glitch/models/notification_policy';
|
||||
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
|
||||
|
||||
export const fetchNotificationPolicy = createDataLoadingThunk(
|
||||
'notificationPolicy/fetch',
|
||||
() => apiGetNotificationPolicy(),
|
||||
);
|
||||
|
||||
export const updateNotificationsPolicy = createDataLoadingThunk(
|
||||
'notificationPolicy/update',
|
||||
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
|
||||
);
|
||||
|
|
@ -57,10 +57,6 @@ 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_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
|
||||
export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
|
||||
export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
|
||||
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
|
||||
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
|
||||
|
|
@ -229,7 +225,7 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
|||
|
||||
dispatch(expandNotificationsRequest(isLoadingMore));
|
||||
|
||||
api(getState).get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
|
||||
api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||
|
|
@ -275,12 +271,12 @@ export function expandNotificationsFail(error, isLoadingMore) {
|
|||
}
|
||||
|
||||
export function clearNotifications() {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_CLEAR,
|
||||
});
|
||||
|
||||
api(getState).post('/api/v1/notifications/clear');
|
||||
api().post('/api/v1/notifications/clear');
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +302,7 @@ export function deleteMarkedNotifications() {
|
|||
return;
|
||||
}
|
||||
|
||||
api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
|
||||
api().delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
|
||||
dispatch(deleteMarkedNotificationsSuccess());
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
|
|
@ -435,40 +431,6 @@ export function setBrowserPermission (value) {
|
|||
};
|
||||
}
|
||||
|
||||
export const fetchNotificationPolicy = () => (dispatch, getState) => {
|
||||
dispatch(fetchNotificationPolicyRequest());
|
||||
|
||||
api(getState).get('/api/v1/notifications/policy').then(({ data }) => {
|
||||
dispatch(fetchNotificationPolicySuccess(data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationPolicyFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationPolicyRequest = () => ({
|
||||
type: NOTIFICATION_POLICY_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchNotificationPolicySuccess = policy => ({
|
||||
type: NOTIFICATION_POLICY_FETCH_SUCCESS,
|
||||
policy,
|
||||
});
|
||||
|
||||
export const fetchNotificationPolicyFail = error => ({
|
||||
type: NOTIFICATION_POLICY_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateNotificationsPolicy = params => (dispatch, getState) => {
|
||||
dispatch(fetchNotificationPolicyRequest());
|
||||
|
||||
api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => {
|
||||
dispatch(fetchNotificationPolicySuccess(data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationPolicyFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationRequests = () => (dispatch, getState) => {
|
||||
const params = {};
|
||||
|
||||
|
|
@ -482,7 +444,7 @@ export const fetchNotificationRequests = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchNotificationRequestsRequest());
|
||||
|
||||
api(getState).get('/api/v1/notifications/requests', { params }).then(response => {
|
||||
api().get('/api/v1/notifications/requests', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
|
||||
|
|
@ -515,7 +477,7 @@ export const expandNotificationRequests = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(expandNotificationRequestsRequest());
|
||||
|
||||
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.map(x => x.account)));
|
||||
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
|
||||
|
|
@ -548,7 +510,7 @@ export const fetchNotificationRequest = id => (dispatch, getState) => {
|
|||
|
||||
dispatch(fetchNotificationRequestRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
|
||||
api().get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
|
||||
dispatch(fetchNotificationRequestSuccess(data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationRequestFail(id, err));
|
||||
|
|
@ -571,10 +533,10 @@ export const fetchNotificationRequestFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequest = id => (dispatch, getState) => {
|
||||
export const acceptNotificationRequest = id => (dispatch) => {
|
||||
dispatch(acceptNotificationRequestRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
||||
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
||||
dispatch(acceptNotificationRequestSuccess(id));
|
||||
}).catch(err => {
|
||||
dispatch(acceptNotificationRequestFail(id, err));
|
||||
|
|
@ -597,10 +559,10 @@ export const acceptNotificationRequestFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequest = id => (dispatch, getState) => {
|
||||
export const dismissNotificationRequest = id => (dispatch) => {
|
||||
dispatch(dismissNotificationRequestRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
||||
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
||||
dispatch(dismissNotificationRequestSuccess(id));
|
||||
}).catch(err => {
|
||||
dispatch(dismissNotificationRequestFail(id, err));
|
||||
|
|
@ -639,7 +601,7 @@ export const fetchNotificationsForRequest = accountId => (dispatch, getState) =>
|
|||
|
||||
dispatch(fetchNotificationsForRequestRequest());
|
||||
|
||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
api().get('/api/v1/notifications', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||
|
|
@ -675,7 +637,7 @@ export const expandNotificationsForRequest = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(expandNotificationsForRequestRequest());
|
||||
|
||||
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.map(item => item.account)));
|
||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function changeSetting(path, value) {
|
|||
}
|
||||
|
||||
const debouncedSave = debounce((dispatch, getState) => {
|
||||
if (getState().getIn(['settings', 'saved'])) {
|
||||
if (getState().getIn(['settings', 'saved']) || !getState().getIn(['meta', 'me'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,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 => {
|
||||
|
|
@ -103,7 +103,7 @@ 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);
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
|
||||
|
|
@ -135,7 +135,7 @@ 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));
|
||||
|
|
@ -176,10 +176,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));
|
||||
|
||||
|
|
@ -220,10 +220,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));
|
||||
|
|
@ -254,10 +254,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));
|
||||
|
|
@ -317,10 +317,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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,11 +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}`).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));
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
|
|||
|
||||
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';
|
||||
|
|
@ -18,7 +20,6 @@ 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';
|
||||
|
|
@ -73,16 +74,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 }));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +120,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
|
||||
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
||||
|
||||
api(getState).get(path, { params }).then(response => {
|
||||
api().get(path, { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
|
|
@ -175,6 +170,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t
|
|||
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/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 expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
|
|
@ -237,12 +233,6 @@ export function connectTimeline(timeline) {
|
|||
};
|
||||
}
|
||||
|
||||
export const disconnectTimeline = timeline => ({
|
||||
type: TIMELINE_DISCONNECT,
|
||||
timeline,
|
||||
usePendingItems: preferPendingItems,
|
||||
});
|
||||
|
||||
export const markAsPartial = timeline => ({
|
||||
type: TIMELINE_MARK_AS_PARTIAL,
|
||||
timeline,
|
||||
|
|
|
|||
20
app/javascript/flavours/glitch/actions/timelines_typed.ts
Normal file
20
app/javascript/flavours/glitch/actions/timelines_typed.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/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,22 +29,14 @@ const setCSRFHeader = () => {
|
|||
|
||||
void ready(setCSRFHeader);
|
||||
|
||||
export const authorizationTokenFromState = (getState?: GetState) => {
|
||||
return (
|
||||
getState && (getState().meta.get('access_token', '') as string | false)
|
||||
);
|
||||
};
|
||||
const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
|
||||
const accessToken = getAccessToken();
|
||||
|
||||
const authorizationHeaderFromState = (getState?: GetState) => {
|
||||
const accessToken = authorizationTokenFromState(getState);
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
if (!accessToken) return {};
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
} as RawAxiosRequestHeaders;
|
||||
};
|
||||
};
|
||||
|
||||
const baseUrlFromState = (getState?: GetState) => {
|
||||
|
|
@ -53,11 +45,11 @@ const baseUrlFromState = (getState?: GetState) => {
|
|||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function api(getState: GetState) {
|
||||
export default function api(withAuthorization = true) {
|
||||
return axios.create({
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
...(withAuthorization ? authorizationTokenFromInitialState() : {}),
|
||||
},
|
||||
|
||||
baseURL: baseUrlFromState(getState),
|
||||
|
|
@ -73,3 +65,50 @@ 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;
|
||||
} = {},
|
||||
) {
|
||||
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/flavours/glitch/api/accounts.ts
Normal file
7
app/javascript/flavours/glitch/api/accounts.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { apiRequestPost } from 'flavours/glitch/api';
|
||||
import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships';
|
||||
|
||||
export const apiSubmitAccountNote = (id: string, value: string) =>
|
||||
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
});
|
||||
15
app/javascript/flavours/glitch/api/directory.ts
Normal file
15
app/javascript/flavours/glitch/api/directory.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { apiRequestGet } from 'flavours/glitch/api';
|
||||
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
|
||||
|
||||
export const apiGetDirectory = (
|
||||
params: {
|
||||
order: string;
|
||||
local: boolean;
|
||||
offset?: number;
|
||||
},
|
||||
limit = 20,
|
||||
) =>
|
||||
apiRequestGet<ApiAccountJSON[]>('v1/directory', {
|
||||
...params,
|
||||
limit,
|
||||
});
|
||||
10
app/javascript/flavours/glitch/api/interactions.ts
Normal file
10
app/javascript/flavours/glitch/api/interactions.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { apiRequestPost } from 'flavours/glitch/api';
|
||||
import type { Status, StatusVisibility } from 'flavours/glitch/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`);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { apiRequestGet, apiRequestPut } from 'flavours/glitch/api';
|
||||
import type { NotificationPolicyJSON } from 'flavours/glitch/api_types/notification_policies';
|
||||
|
||||
export const apiGetNotificationPolicy = () =>
|
||||
apiRequestGet<NotificationPolicyJSON>('/v1/notifications/policy');
|
||||
|
||||
export const apiUpdateNotificationsPolicy = (
|
||||
policy: Partial<NotificationPolicyJSON>,
|
||||
) => apiRequestPut<NotificationPolicyJSON>('/v1/notifications/policy', policy);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// See app/serializers/rest/notification_policy_serializer.rb
|
||||
|
||||
export interface NotificationPolicyJSON {
|
||||
filter_not_following: boolean;
|
||||
filter_not_followers: boolean;
|
||||
filter_new_accounts: boolean;
|
||||
filter_private_mentions: boolean;
|
||||
summary: {
|
||||
pending_requests_count: number;
|
||||
pending_notifications_count: number;
|
||||
};
|
||||
}
|
||||
|
|
@ -30,6 +30,12 @@ export interface ApiMentionJSON {
|
|||
acct: string;
|
||||
}
|
||||
|
||||
export interface ApiPreviewCardAuthorJSON {
|
||||
name: string;
|
||||
url: string;
|
||||
account?: ApiAccountJSON;
|
||||
}
|
||||
|
||||
export interface ApiPreviewCardJSON {
|
||||
url: string;
|
||||
title: string;
|
||||
|
|
@ -38,6 +44,7 @@ export interface ApiPreviewCardJSON {
|
|||
type: string;
|
||||
author_name: string;
|
||||
author_url: string;
|
||||
author_account?: ApiAccountJSON;
|
||||
provider_name: string;
|
||||
provider_url: string;
|
||||
html: string;
|
||||
|
|
@ -48,6 +55,7 @@ export interface ApiPreviewCardJSON {
|
|||
embed_url: string;
|
||||
blurhash: string;
|
||||
published_at: string;
|
||||
authors: ApiPreviewCardAuthorJSON[];
|
||||
}
|
||||
|
||||
export interface ApiStatusJSON {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
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 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 'flavours/glitch/components/empty_account';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||
|
||||
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||
import { me } from '../initial_state';
|
||||
|
||||
import { Avatar } from './avatar';
|
||||
|
|
@ -30,153 +32,152 @@ 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.record,
|
||||
onFollow: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
onMuteNotifications: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
defaultAction: PropTypes.string,
|
||||
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);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, hidden, withBio, 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 (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('suspended') && !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'>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} 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'>
|
||||
{account.get('followers_count') !== -1 && (
|
||||
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} />
|
||||
)} {verification} {muteTimeRemaining}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Permalink>
|
||||
|
||||
{!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 title={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'>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} 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'>
|
||||
{account.get('followers_count') !== -1 && (
|
||||
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} />
|
||||
)} {verification} {muteTimeRemaining}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Permalink>
|
||||
|
||||
{!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/flavours/glitch/components/account_bio.tsx
Normal file
20
app/javascript/flavours/glitch/components/account_bio.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useLinks } from 'flavours/glitch/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/flavours/glitch/components/account_fields.tsx
Normal file
42
app/javascript/flavours/glitch/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 { Icon } from 'flavours/glitch/components/icon';
|
||||
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
||||
import type { Account } from 'flavours/glitch/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 => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
|
|||
<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}%)`,
|
||||
}}
|
||||
>
|
||||
{obfuscate ? (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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, roleId }) => (
|
||||
export const Badge = ({ icon = <PersonIcon />, label, domain, roleId }) => (
|
||||
<div className='account-role' data-account-role-id={roleId}>
|
||||
{icon}
|
||||
{label}
|
||||
|
|
@ -22,10 +22,6 @@ Badge.propTypes = {
|
|||
roleId: PropTypes.string
|
||||
};
|
||||
|
||||
Badge.defaultProps = {
|
||||
icon: <PersonIcon />,
|
||||
};
|
||||
|
||||
export const GroupBadge = () => (
|
||||
<Badge icon={<GroupsIcon />} label={<FormattedMessage id='account.badges.group' defaultMessage='Group' />} />
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,235 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import { useAppHistory } from './router';
|
||||
|
||||
const messages = defineMessages({
|
||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
});
|
||||
|
||||
const BackButton = ({ onlyIcon }) => {
|
||||
const history = useAppHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
if (history.location?.state?.fromMastodon) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push('/');
|
||||
}
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
|
||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
||||
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
BackButton.propTypes = {
|
||||
onlyIcon: PropTypes.bool,
|
||||
};
|
||||
|
||||
class ColumnHeader extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
title: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
extraButton: PropTypes.node,
|
||||
showBackButton: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
pinned: PropTypes.bool,
|
||||
placeholder: PropTypes.bool,
|
||||
onPin: PropTypes.func,
|
||||
onMove: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
appendContent: PropTypes.node,
|
||||
collapseIssues: PropTypes.bool,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
animating: false,
|
||||
};
|
||||
|
||||
handleToggleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.setState({ collapsed: !this.state.collapsed, animating: true });
|
||||
};
|
||||
|
||||
handleTitleClick = () => {
|
||||
this.props.onClick?.();
|
||||
};
|
||||
|
||||
handleMoveLeft = () => {
|
||||
this.props.onMove(-1);
|
||||
};
|
||||
|
||||
handleMoveRight = () => {
|
||||
this.props.onMove(1);
|
||||
};
|
||||
|
||||
handleTransitionEnd = () => {
|
||||
this.setState({ animating: false });
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
if (!this.props.pinned) {
|
||||
this.props.history.replace('/');
|
||||
}
|
||||
|
||||
this.props.onPin();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
'active': active,
|
||||
});
|
||||
|
||||
const buttonClassName = classNames('column-header', {
|
||||
'active': active,
|
||||
});
|
||||
|
||||
const collapsibleClassName = classNames('column-header__collapsible', {
|
||||
'collapsed': collapsed,
|
||||
'animating': animating,
|
||||
});
|
||||
|
||||
const collapsibleButtonClassName = classNames('column-header__button', {
|
||||
'active': !collapsed,
|
||||
});
|
||||
|
||||
let extraContent, pinButton, moveButtons, backButton, collapseButton;
|
||||
|
||||
if (children) {
|
||||
extraContent = (
|
||||
<div key='extra-content' className='column-header__collapsible__extra'>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (multiColumn && pinned) {
|
||||
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
|
||||
moveButtons = (
|
||||
<div className='column-header__setting-arrows'>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn && this.props.onPin) {
|
||||
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
}
|
||||
|
||||
if (history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
|
||||
backButton = <BackButton onlyIcon={!!title} />;
|
||||
}
|
||||
|
||||
const collapsedContent = [
|
||||
extraContent,
|
||||
];
|
||||
|
||||
if (multiColumn) {
|
||||
collapsedContent.push(
|
||||
<div key='buttons' className='column-header__advanced-buttons'>
|
||||
{pinButton}
|
||||
{moveButtons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||
collapseButton = (
|
||||
<button
|
||||
className={collapsibleButtonClassName}
|
||||
title={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||
onClick={this.handleToggleClick}
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id='sliders' icon={SettingsIcon} />
|
||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||
</i>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const hasTitle = (icon || iconComponent) && title;
|
||||
|
||||
const component = (
|
||||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<>
|
||||
{backButton}
|
||||
|
||||
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||
{!backButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
||||
{title}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasTitle && backButton}
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{extraButton}
|
||||
{collapseButton}
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
|
||||
<div className='column-header__collapsible-inner'>
|
||||
{(!collapsed || animating) && collapsedContent}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{appendContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (placeholder) {
|
||||
return component;
|
||||
} else {
|
||||
return (<ButtonInTabsBar>
|
||||
{component}
|
||||
</ButtonInTabsBar>);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(withRouter(ColumnHeader));
|
||||
301
app/javascript/flavours/glitch/components/column_header.tsx
Normal file
301
app/javascript/flavours/glitch/components/column_header.tsx
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||
import type { IconProp } from 'flavours/glitch/components/icon';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context';
|
||||
import { useIdentity } from 'flavours/glitch/identity_context';
|
||||
|
||||
import { useAppHistory } from './router';
|
||||
|
||||
const messages = defineMessages({
|
||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||
moveLeft: {
|
||||
id: 'column_header.moveLeft_settings',
|
||||
defaultMessage: 'Move column to the left',
|
||||
},
|
||||
moveRight: {
|
||||
id: 'column_header.moveRight_settings',
|
||||
defaultMessage: 'Move column to the right',
|
||||
},
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
});
|
||||
|
||||
const BackButton: React.FC<{
|
||||
onlyIcon: boolean;
|
||||
}> = ({ onlyIcon }) => {
|
||||
const history = useAppHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
if (history.location.state?.fromMastodon) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push('/');
|
||||
}
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleBackClick}
|
||||
className={classNames('column-header__back-button', {
|
||||
compact: onlyIcon,
|
||||
})}
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
className='column-back-button__icon'
|
||||
/>
|
||||
{!onlyIcon && (
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
iconComponent?: IconProp;
|
||||
active?: boolean;
|
||||
children?: React.ReactNode;
|
||||
pinned?: boolean;
|
||||
multiColumn?: boolean;
|
||||
extraButton?: React.ReactNode;
|
||||
showBackButton?: boolean;
|
||||
placeholder?: boolean;
|
||||
appendContent?: React.ReactNode;
|
||||
collapseIssues?: boolean;
|
||||
onClick?: () => void;
|
||||
onMove?: (arg0: number) => void;
|
||||
onPin?: () => void;
|
||||
}
|
||||
|
||||
export const ColumnHeader: React.FC<Props> = ({
|
||||
title,
|
||||
icon,
|
||||
iconComponent,
|
||||
active,
|
||||
children,
|
||||
pinned,
|
||||
multiColumn,
|
||||
extraButton,
|
||||
showBackButton,
|
||||
placeholder,
|
||||
appendContent,
|
||||
collapseIssues,
|
||||
onClick,
|
||||
onMove,
|
||||
onPin,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { signedIn } = useIdentity();
|
||||
const history = useAppHistory();
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [animating, setAnimating] = useState(false);
|
||||
|
||||
const handleToggleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setCollapsed((value) => !value);
|
||||
setAnimating(true);
|
||||
},
|
||||
[setCollapsed, setAnimating],
|
||||
);
|
||||
|
||||
const handleTitleClick = useCallback(() => {
|
||||
onClick?.();
|
||||
}, [onClick]);
|
||||
|
||||
const handleMoveLeft = useCallback(() => {
|
||||
onMove?.(-1);
|
||||
}, [onMove]);
|
||||
|
||||
const handleMoveRight = useCallback(() => {
|
||||
onMove?.(1);
|
||||
}, [onMove]);
|
||||
|
||||
const handleTransitionEnd = useCallback(() => {
|
||||
setAnimating(false);
|
||||
}, [setAnimating]);
|
||||
|
||||
const handlePin = useCallback(() => {
|
||||
if (!pinned) {
|
||||
history.replace('/');
|
||||
}
|
||||
|
||||
onPin?.();
|
||||
}, [history, pinned, onPin]);
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
active,
|
||||
});
|
||||
|
||||
const buttonClassName = classNames('column-header', {
|
||||
active,
|
||||
});
|
||||
|
||||
const collapsibleClassName = classNames('column-header__collapsible', {
|
||||
collapsed,
|
||||
animating,
|
||||
});
|
||||
|
||||
const collapsibleButtonClassName = classNames('column-header__button', {
|
||||
active: !collapsed,
|
||||
});
|
||||
|
||||
let extraContent, pinButton, moveButtons, backButton, collapseButton;
|
||||
|
||||
if (children) {
|
||||
extraContent = (
|
||||
<div key='extra-content' className='column-header__collapsible__extra'>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (multiColumn && pinned) {
|
||||
pinButton = (
|
||||
<button
|
||||
className='text-btn column-header__setting-btn'
|
||||
onClick={handlePin}
|
||||
>
|
||||
<Icon id='times' icon={CloseIcon} />{' '}
|
||||
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
|
||||
</button>
|
||||
);
|
||||
|
||||
moveButtons = (
|
||||
<div className='column-header__setting-arrows'>
|
||||
<button
|
||||
title={intl.formatMessage(messages.moveLeft)}
|
||||
aria-label={intl.formatMessage(messages.moveLeft)}
|
||||
className='icon-button column-header__setting-btn'
|
||||
onClick={handleMoveLeft}
|
||||
>
|
||||
<Icon id='chevron-left' icon={ChevronLeftIcon} />
|
||||
</button>
|
||||
<button
|
||||
title={intl.formatMessage(messages.moveRight)}
|
||||
aria-label={intl.formatMessage(messages.moveRight)}
|
||||
className='icon-button column-header__setting-btn'
|
||||
onClick={handleMoveRight}
|
||||
>
|
||||
<Icon id='chevron-right' icon={ChevronRightIcon} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn && onPin) {
|
||||
pinButton = (
|
||||
<button
|
||||
className='text-btn column-header__setting-btn'
|
||||
onClick={handlePin}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />{' '}
|
||||
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!pinned &&
|
||||
((multiColumn && history.location.state?.fromMastodon) || showBackButton)
|
||||
) {
|
||||
backButton = <BackButton onlyIcon={!!title} />;
|
||||
}
|
||||
|
||||
const collapsedContent = [extraContent];
|
||||
|
||||
if (multiColumn) {
|
||||
collapsedContent.push(
|
||||
<div key='buttons' className='column-header__advanced-buttons'>
|
||||
{pinButton}
|
||||
{moveButtons}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
if (signedIn && (children || (multiColumn && onPin))) {
|
||||
collapseButton = (
|
||||
<button
|
||||
className={collapsibleButtonClassName}
|
||||
title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
|
||||
aria-label={intl.formatMessage(
|
||||
collapsed ? messages.show : messages.hide,
|
||||
)}
|
||||
onClick={handleToggleClick}
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id='sliders' icon={SettingsIcon} />
|
||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||
</i>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const hasIcon = icon && iconComponent;
|
||||
const hasTitle = hasIcon && title;
|
||||
|
||||
const component = (
|
||||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<>
|
||||
{backButton}
|
||||
|
||||
<button onClick={handleTitleClick} className='column-header__title'>
|
||||
{!backButton && (
|
||||
<Icon
|
||||
id={icon}
|
||||
icon={iconComponent}
|
||||
className='column-header__icon'
|
||||
/>
|
||||
)}
|
||||
{title}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasTitle && backButton}
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{extraButton}
|
||||
{collapseButton}
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className={collapsibleClassName}
|
||||
tabIndex={collapsed ? -1 : undefined}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
>
|
||||
<div className='column-header__collapsible-inner'>
|
||||
{(!collapsed || animating) && collapsedContent}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{appendContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (placeholder) {
|
||||
return component;
|
||||
} else {
|
||||
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ColumnHeader;
|
||||
125
app/javascript/flavours/glitch/components/follow_button.tsx
Normal file
125
app/javascript/flavours/glitch/components/follow_button.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useIdentity } from '@/flavours/glitch/identity_context';
|
||||
import {
|
||||
fetchRelationships,
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
} from 'flavours/glitch/actions/accounts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
});
|
||||
|
||||
export const FollowButton: React.FC<{
|
||||
accountId?: string;
|
||||
}> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { signedIn } = useIdentity();
|
||||
const account = useAppSelector((state) =>
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
);
|
||||
const relationship = useAppSelector((state) =>
|
||||
accountId ? state.relationships.get(accountId) : undefined,
|
||||
);
|
||||
const following = relationship?.following || relationship?.requested;
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId && signedIn) {
|
||||
dispatch(fetchRelationships([accountId]));
|
||||
}
|
||||
}, [dispatch, accountId, signedIn]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!signedIn) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
type: 'follow',
|
||||
accountId: accountId,
|
||||
url: account?.url,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!relationship) return;
|
||||
|
||||
if (accountId === me) {
|
||||
return;
|
||||
} else if (relationship.following || relationship.requested) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id='confirmations.unfollow.message'
|
||||
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
),
|
||||
confirm: intl.formatMessage(messages.unfollow),
|
||||
onConfirm: () => {
|
||||
dispatch(unfollowAccount(accountId));
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(followAccount(accountId));
|
||||
}
|
||||
}, [dispatch, intl, accountId, relationship, account, signedIn]);
|
||||
|
||||
let label;
|
||||
|
||||
if (!signedIn) {
|
||||
label = intl.formatMessage(messages.follow);
|
||||
} else if (accountId === me) {
|
||||
label = intl.formatMessage(messages.edit_profile);
|
||||
} else if (!relationship) {
|
||||
label = <LoadingIndicator />;
|
||||
} else if (!relationship.following && relationship.followed_by) {
|
||||
label = intl.formatMessage(messages.followBack);
|
||||
} else if (relationship.following || relationship.requested) {
|
||||
label = intl.formatMessage(messages.unfollow);
|
||||
} else {
|
||||
label = intl.formatMessage(messages.follow);
|
||||
}
|
||||
|
||||
if (accountId === me) {
|
||||
return (
|
||||
<a
|
||||
href='/settings/profile'
|
||||
target='_blank'
|
||||
rel='noreferrer noopener'
|
||||
className='button button-secondary'
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
disabled={relationship?.blocked_by || relationship?.blocking}
|
||||
secondary={following}
|
||||
className={following ? 'button--destructive' : undefined}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
@ -52,7 +52,10 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
|||
);
|
||||
|
||||
return Object.values(groups).map((tags) => {
|
||||
if (tags.length === 1) return tags[0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know that the array has at least one element
|
||||
const firstTag = tags[0]!;
|
||||
|
||||
if (tags.length === 1) return firstTag;
|
||||
|
||||
// The best match is the one where we have the less difference between upper and lower case letter count
|
||||
const best = minBy(tags, (tag) => {
|
||||
|
|
@ -66,7 +69,7 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
|||
return Math.abs(lowerCase - upperCase);
|
||||
});
|
||||
|
||||
return best ?? tags[0];
|
||||
return best ?? firstTag;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import { useEffect, forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { AccountBio } from 'flavours/glitch/components/account_bio';
|
||||
import { AccountFields } from 'flavours/glitch/components/account_fields';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { FollowersCounter } from 'flavours/glitch/components/counters';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { FollowButton } from 'flavours/glitch/components/follow_button';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { domain } from 'flavours/glitch/initial_state';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
export const HoverCardAccount = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ accountId?: string }
|
||||
>(({ accountId }, ref) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const account = useAppSelector((state) =>
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId && !account) {
|
||||
dispatch(fetchAccount(accountId));
|
||||
}
|
||||
}, [dispatch, accountId, account]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id='hover-card'
|
||||
role='tooltip'
|
||||
className={classNames('hover-card dropdown-animation', {
|
||||
'hover-card--loading': !account,
|
||||
})}
|
||||
>
|
||||
{account ? (
|
||||
<>
|
||||
<Permalink
|
||||
to={`/@${account.acct}`}
|
||||
href={account.get('url')}
|
||||
className='hover-card__name'
|
||||
>
|
||||
<Avatar account={account} size={46} />
|
||||
<DisplayName account={account} localDomain={domain} />
|
||||
</Permalink>
|
||||
|
||||
<div className='hover-card__text-row'>
|
||||
<AccountBio
|
||||
note={account.note_emojified}
|
||||
className='hover-card__bio'
|
||||
/>
|
||||
<AccountFields fields={account.fields} limit={2} />
|
||||
</div>
|
||||
|
||||
<div className='hover-card__number'>
|
||||
<ShortNumber
|
||||
value={account.followers_count}
|
||||
renderer={FollowersCounter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FollowButton accountId={accountId} />
|
||||
</>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
HoverCardAccount.displayName = 'HoverCardAccount';
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import type {
|
||||
OffsetValue,
|
||||
UsePopperOptions,
|
||||
} from 'react-overlays/esm/usePopper';
|
||||
|
||||
import { HoverCardAccount } from 'flavours/glitch/components/hover_card_account';
|
||||
import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
|
||||
|
||||
const offset = [-12, 4] as OffsetValue;
|
||||
const enterDelay = 750;
|
||||
const leaveDelay = 150;
|
||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||
|
||||
const isHoverCardAnchor = (element: HTMLElement) =>
|
||||
element.matches('[data-hover-card-account]');
|
||||
|
||||
export const HoverCardController: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [accountId, setAccountId] = useState<string | undefined>();
|
||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||
const [setScrollTimeout] = useTimeout();
|
||||
const location = useLocation();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
cancelEnterTimeout();
|
||||
cancelLeaveTimeout();
|
||||
setOpen(false);
|
||||
setAnchor(null);
|
||||
}, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]);
|
||||
|
||||
useEffect(() => {
|
||||
handleClose();
|
||||
}, [handleClose, location]);
|
||||
|
||||
useEffect(() => {
|
||||
let isScrolling = false;
|
||||
let currentAnchor: HTMLElement | null = null;
|
||||
let currentTitle: string | null = null;
|
||||
|
||||
const open = (target: HTMLElement) => {
|
||||
target.setAttribute('aria-describedby', 'hover-card');
|
||||
setOpen(true);
|
||||
setAnchor(target);
|
||||
setAccountId(target.getAttribute('data-hover-card-account') ?? undefined);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
currentAnchor?.removeAttribute('aria-describedby');
|
||||
currentAnchor = null;
|
||||
setOpen(false);
|
||||
setAnchor(null);
|
||||
setAccountId(undefined);
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: MouseEvent) => {
|
||||
const { target } = e;
|
||||
|
||||
// We've exited the window
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
// We've entered an anchor
|
||||
if (!isScrolling && isHoverCardAnchor(target)) {
|
||||
cancelLeaveTimeout();
|
||||
|
||||
currentAnchor?.removeAttribute('aria-describedby');
|
||||
currentAnchor = target;
|
||||
|
||||
currentTitle = target.getAttribute('title');
|
||||
target.removeAttribute('title');
|
||||
|
||||
setEnterTimeout(() => {
|
||||
open(target);
|
||||
}, enterDelay);
|
||||
}
|
||||
|
||||
// We've entered the hover card
|
||||
if (
|
||||
!isScrolling &&
|
||||
(target === currentAnchor || target === cardRef.current)
|
||||
) {
|
||||
cancelLeaveTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e: MouseEvent) => {
|
||||
const { target } = e;
|
||||
|
||||
if (!currentAnchor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentTitle &&
|
||||
target instanceof HTMLElement &&
|
||||
target === currentAnchor
|
||||
)
|
||||
target.setAttribute('title', currentTitle);
|
||||
|
||||
if (target === currentAnchor || target === cardRef.current) {
|
||||
cancelEnterTimeout();
|
||||
|
||||
setLeaveTimeout(() => {
|
||||
close();
|
||||
}, leaveDelay);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollEnd = () => {
|
||||
isScrolling = false;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
isScrolling = true;
|
||||
cancelEnterTimeout();
|
||||
setScrollTimeout(handleScrollEnd, 100);
|
||||
};
|
||||
|
||||
const handleMouseMove = () => {
|
||||
delayEnterTimeout(enterDelay);
|
||||
};
|
||||
|
||||
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
document.body.addEventListener('mousemove', handleMouseMove, {
|
||||
passive: true,
|
||||
capture: false,
|
||||
});
|
||||
|
||||
document.body.addEventListener('mouseleave', handleMouseLeave, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
document.addEventListener('scroll', handleScroll, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
||||
document.body.removeEventListener('mousemove', handleMouseMove);
|
||||
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||
document.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [
|
||||
setEnterTimeout,
|
||||
setLeaveTimeout,
|
||||
setScrollTimeout,
|
||||
cancelEnterTimeout,
|
||||
cancelLeaveTimeout,
|
||||
delayEnterTimeout,
|
||||
setOpen,
|
||||
setAccountId,
|
||||
setAnchor,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
rootClose
|
||||
onHide={handleClose}
|
||||
show={open}
|
||||
target={anchor}
|
||||
placement='bottom-start'
|
||||
flip
|
||||
offset={offset}
|
||||
popperConfig={popperConfig}
|
||||
>
|
||||
{({ props }) => (
|
||||
<div {...props} className='hover-card-controller'>
|
||||
<HoverCardAccount accountId={accountId} ref={cardRef} />
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
|
@ -311,7 +311,7 @@ class MediaGallery extends PureComponent {
|
|||
render () {
|
||||
const { media, lang, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props;
|
||||
const { visible } = this.state;
|
||||
const size = media.take(4).size;
|
||||
const size = media.size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
|
@ -331,7 +331,7 @@ class MediaGallery extends PureComponent {
|
|||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />);
|
||||
children = media.map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />);
|
||||
}
|
||||
|
||||
if (uncached) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
|
||||
|
||||
export const MoreFromAuthor = ({ accountId }) => (
|
||||
<div className='more-from-author'>
|
||||
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
|
||||
<use xlinkHref='#logo-symbol-icon' />
|
||||
</svg>
|
||||
|
||||
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
MoreFromAuthor.propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import emojify from 'flavours/glitch/features/emoji/emoji';
|
||||
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
|
||||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
|
||||
|
|
@ -38,12 +39,8 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
|||
}, {});
|
||||
|
||||
class Poll extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
poll: ImmutablePropTypes.map,
|
||||
lang: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
|
@ -235,7 +232,7 @@ class Poll extends ImmutablePureComponent {
|
|||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
{!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||
{!showResults && <button className='button button-secondary' disabled={disabled || !this.props.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||
{!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
|
||||
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
|
||||
{votesCount}
|
||||
|
|
@ -247,4 +244,4 @@ class Poll extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default injectIntl(Poll);
|
||||
export default injectIntl(withIdentity(Poll));
|
||||
|
|
|
|||
|
|
@ -42,10 +42,12 @@ class ServerBanner extends PureComponent {
|
|||
return (
|
||||
<div className='server-banner'>
|
||||
<div className='server-banner__introduction'>
|
||||
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
|
||||
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
|
||||
</div>
|
||||
|
||||
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
|
||||
<Link to='/about'>
|
||||
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
|
||||
</Link>
|
||||
|
||||
<div className='server-banner__description'>
|
||||
{isLoading ? (
|
||||
|
|
@ -84,10 +86,6 @@ class ServerBanner extends PureComponent {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className='spacer' />
|
||||
|
||||
<Link className='button button--block button-secondary' to='/about'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const ShortNumberCounter: React.FC<ShortNumberCounterProps> = ({ value }) => {
|
|||
|
||||
const count = (
|
||||
<FormattedNumber
|
||||
value={rawNumber}
|
||||
value={rawNumber ?? 0}
|
||||
maximumFractionDigits={maxFractionDigits}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { HotKeys } from 'react-hotkeys';
|
|||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
||||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
|
||||
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
|
|
@ -20,7 +21,6 @@ import Card from '../features/status/components/card';
|
|||
// to use the progress bar to show download progress
|
||||
import Bundle from '../features/ui/components/bundle';
|
||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||
import { IdentityConsumer } from '../features/ui/util/identity_consumer';
|
||||
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
|
||||
import { displayMedia, visibleReactions } from '../initial_state';
|
||||
|
||||
|
|
@ -78,6 +78,7 @@ class Status extends ImmutablePureComponent {
|
|||
static contextType = SensitiveMediaContext;
|
||||
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
containerId: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
status: ImmutablePropTypes.map,
|
||||
|
|
@ -545,6 +546,7 @@ class Status extends ImmutablePureComponent {
|
|||
nextInReplyToId,
|
||||
rootId,
|
||||
history,
|
||||
identity,
|
||||
...other
|
||||
} = this.props;
|
||||
const { isCollapsed } = this.state;
|
||||
|
|
@ -807,7 +809,7 @@ class Status extends ImmutablePureComponent {
|
|||
{prepend}
|
||||
|
||||
<div
|
||||
className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'has-background': isCollapsed && background, collapsed: isCollapsed })}
|
||||
className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'has-background': isCollapsed && background })}
|
||||
data-id={status.get('id')}
|
||||
style={isCollapsed && background ? { backgroundImage: `url(${background})` } : null}
|
||||
>
|
||||
|
|
@ -846,18 +848,14 @@ class Status extends ImmutablePureComponent {
|
|||
{...statusContentProps}
|
||||
/>
|
||||
|
||||
<IdentityConsumer>
|
||||
{identity => (
|
||||
<StatusReactions
|
||||
statusId={status.get('id')}
|
||||
reactions={status.get('reactions')}
|
||||
numVisible={visibleReactions}
|
||||
addReaction={this.props.onReactionAdd}
|
||||
removeReaction={this.props.onReactionRemove}
|
||||
canReact={identity.signedIn}
|
||||
/>
|
||||
)}
|
||||
</IdentityConsumer>
|
||||
<StatusReactions
|
||||
statusId={status.get('id')}
|
||||
reactions={status.get('reactions')}
|
||||
numVisible={visibleReactions}
|
||||
addReaction={this.props.onReactionAdd}
|
||||
removeReaction={this.props.onReactionRemove}
|
||||
canReact={this.props.identity.signedIn}
|
||||
/>
|
||||
|
||||
{(!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar']))) && (
|
||||
<StatusActionBar
|
||||
|
|
@ -881,4 +879,4 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withOptionalRouter(injectIntl(Status));
|
||||
export default withOptionalRouter(injectIntl((withIdentity(Status))));
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
|
|||
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg';
|
||||
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg';
|
||||
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
||||
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
|
@ -70,12 +71,8 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
|
|
@ -112,7 +109,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
];
|
||||
|
||||
handleReplyClick = () => {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { signedIn } = this.props.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onReply(this.props.status, this.props.history);
|
||||
|
|
@ -128,7 +125,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleFavouriteClick = (e) => {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { signedIn } = this.props.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onFavourite(this.props.status, e);
|
||||
|
|
@ -142,7 +139,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleReblogClick = e => {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { signedIn } = this.props.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
|
|
@ -218,7 +215,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
render () {
|
||||
const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
|
||||
const { permissions, signedIn } = this.context.identity;
|
||||
const { permissions, signedIn } = this.props.identity;
|
||||
|
||||
const mutingConversation = status.get('muted');
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
|
|
@ -370,4 +367,4 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withRouter(injectIntl(StatusActionBar));
|
||||
export default withRouter(withIdentity(injectIntl(StatusActionBar)));
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import LinkIcon from '@/material-icons/400-24px/link.svg?react';
|
|||
import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
|
||||
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
||||
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
|
||||
|
||||
|
|
@ -126,12 +127,8 @@ const mapStateToProps = state => ({
|
|||
});
|
||||
|
||||
class StatusContent extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
statusContent: PropTypes.string,
|
||||
expanded: PropTypes.bool,
|
||||
|
|
@ -185,6 +182,7 @@ class StatusContent extends PureComponent {
|
|||
if (mention) {
|
||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||
link.setAttribute('title', `@${mention.get('acct')}`);
|
||||
link.setAttribute('data-hover-card-account', mention.get('id'));
|
||||
if (rewriteMentions !== 'no') {
|
||||
while (link.firstChild) link.removeChild(link.firstChild);
|
||||
link.appendChild(document.createTextNode('@'));
|
||||
|
|
@ -349,7 +347,7 @@ class StatusContent extends PureComponent {
|
|||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
||||
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
||||
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
||||
|
||||
const content = { __html: statusContent ?? getStatusContent(status) };
|
||||
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
||||
|
|
@ -503,4 +501,4 @@ class StatusContent extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent)));
|
||||
export default withRouter(withIdentity(connect(mapStateToProps)(injectIntl(StatusContent))));
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export default class StatusHeader extends PureComponent {
|
|||
target='_blank'
|
||||
onClick={this.handleAccountClick}
|
||||
rel='noopener noreferrer'
|
||||
title={status.getIn(['account', 'acct'])}
|
||||
data-hover-card-account={status.getIn(['account', 'id'])}
|
||||
>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
withCounters: PropTypes.bool,
|
||||
timelineId: PropTypes.string.isRequired,
|
||||
lastId: PropTypes.string,
|
||||
bindToDocument: PropTypes.bool,
|
||||
regex: PropTypes.string,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export default class StatusPrepend extends PureComponent {
|
|||
onClick={this.handleClick}
|
||||
href={account.get('url')}
|
||||
className='status__display-name'
|
||||
data-hover-card-account={account.get('id')}
|
||||
>
|
||||
<bdi>
|
||||
<strong
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
|
@ -15,6 +14,7 @@ import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
|||
import ErrorBoundary from 'flavours/glitch/components/error_boundary';
|
||||
import { Router } from 'flavours/glitch/components/router';
|
||||
import UI from 'flavours/glitch/features/ui';
|
||||
import { IdentityContext, createIdentityContext } from 'flavours/glitch/identity_context';
|
||||
import initialState, { title as siteTitle } from 'flavours/glitch/initial_state';
|
||||
import { IntlProvider } from 'flavours/glitch/locales';
|
||||
import { store } from 'flavours/glitch/store';
|
||||
|
|
@ -33,33 +33,9 @@ if (initialState.meta.me) {
|
|||
store.dispatch(fetchCustomEmojis());
|
||||
}
|
||||
|
||||
const createIdentityContext = state => ({
|
||||
signedIn: !!state.meta.me,
|
||||
accountId: state.meta.me,
|
||||
disabledAccountId: state.meta.disabled_account_id,
|
||||
accessToken: state.meta.access_token,
|
||||
permissions: state.role ? state.role.permissions : 0,
|
||||
});
|
||||
|
||||
export default class Mastodon extends PureComponent {
|
||||
|
||||
static childContextTypes = {
|
||||
identity: PropTypes.shape({
|
||||
signedIn: PropTypes.bool.isRequired,
|
||||
accountId: PropTypes.string,
|
||||
disabledAccountId: PropTypes.string,
|
||||
accessToken: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
identity = createIdentityContext(initialState);
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
identity: this.identity,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.identity.signedIn) {
|
||||
this.disconnect = store.dispatch(connectUserStream());
|
||||
|
|
@ -79,19 +55,21 @@ export default class Mastodon extends PureComponent {
|
|||
|
||||
render () {
|
||||
return (
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<Route path='/' component={UI} />
|
||||
</ScrollContext>
|
||||
</Router>
|
||||
<IdentityContext.Provider value={this.identity}>
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<Route path='/' component={UI} />
|
||||
</ScrollContext>
|
||||
</Router>
|
||||
|
||||
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
||||
</ErrorBoundary>
|
||||
</ReduxProvider>
|
||||
</IntlProvider>
|
||||
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
||||
</ErrorBoundary>
|
||||
</ReduxProvider>
|
||||
</IntlProvider>
|
||||
</IdentityContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,9 +117,9 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
|
||||
onModalReblog (status, privacy) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else {
|
||||
dispatch(reblog(status, privacy));
|
||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'packs/public-path';
|
||||
import '@/entrypoints/public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import Rails from '@rails/ujs';
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import 'packs/public-path';
|
||||
import '@/entrypoints/public-path';
|
||||
|
||||
import { start } from 'flavours/glitch/common';
|
||||
import { loadLocale } from 'flavours/glitch/locales';
|
||||
import main from "flavours/glitch/main";
|
||||
import main from 'flavours/glitch/main';
|
||||
import { loadPolyfills } from 'flavours/glitch/polyfills';
|
||||
|
||||
start();
|
||||
|
|
@ -10,6 +10,6 @@ start();
|
|||
loadPolyfills()
|
||||
.then(loadLocale)
|
||||
.then(main)
|
||||
.catch(e => {
|
||||
.catch((e: unknown) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/* This file is a hack to have something more reliable than the upstream `common` tag
|
||||
that is implicitly generated as the common chunk through webpack's `splitChunks` config */
|
||||
|
||||
import 'packs/public-path';
|
||||
import '@/entrypoints/public-path';
|
||||
import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
// This is a hack to ensures that webpack compiles our images.
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import 'packs/public-path';
|
||||
import '@/entrypoints/public-path';
|
||||
import ready from 'flavours/glitch/ready';
|
||||
|
||||
ready(() => {
|
||||
const image = document.querySelector('img');
|
||||
const image = document.querySelector<HTMLImageElement>('img');
|
||||
|
||||
if (!image) return;
|
||||
|
||||
image.addEventListener('mouseenter', () => {
|
||||
image.src = '/oops.gif';
|
||||
|
|
@ -11,4 +13,6 @@ ready(() => {
|
|||
image.addEventListener('mouseleave', () => {
|
||||
image.src = '/oops.png';
|
||||
});
|
||||
}).catch((e: unknown) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import 'packs/public-path';
|
||||
import '@/entrypoints/public-path';
|
||||
|
||||
import { IntlMessageFormat } from 'intl-messageformat';
|
||||
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
||||
|
|
@ -65,7 +65,7 @@ window.addEventListener('message', (e) => {
|
|||
{
|
||||
type: 'setHeight',
|
||||
id: data.id,
|
||||
height: document.getElementsByTagName('html')[0].scrollHeight,
|
||||
height: document.getElementsByTagName('html')[0]?.scrollHeight,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
|
|
@ -135,7 +135,7 @@ function loaded() {
|
|||
);
|
||||
};
|
||||
const todayFormat = new IntlMessageFormat(
|
||||
localeData['relative_format.today'] || 'Today at {time}',
|
||||
localeData['relative_format.today'] ?? 'Today at {time}',
|
||||
locale,
|
||||
);
|
||||
|
||||
|
|
@ -288,13 +288,13 @@ function loaded() {
|
|||
if (statusEl.dataset.spoiler === 'expanded') {
|
||||
statusEl.dataset.spoiler = 'folded';
|
||||
this.textContent = new IntlMessageFormat(
|
||||
localeData['status.show_more'] || 'Show more',
|
||||
localeData['status.show_more'] ?? 'Show more',
|
||||
locale,
|
||||
).format() as string;
|
||||
} else {
|
||||
statusEl.dataset.spoiler = 'expanded';
|
||||
this.textContent = new IntlMessageFormat(
|
||||
localeData['status.show_less'] || 'Show less',
|
||||
localeData['status.show_less'] ?? 'Show less',
|
||||
locale,
|
||||
).format() as string;
|
||||
}
|
||||
|
|
@ -316,8 +316,8 @@ function loaded() {
|
|||
|
||||
const message =
|
||||
statusEl.dataset.spoiler === 'expanded'
|
||||
? localeData['status.show_less'] || 'Show less'
|
||||
: localeData['status.show_more'] || 'Show more';
|
||||
? localeData['status.show_less'] ?? 'Show less'
|
||||
: localeData['status.show_more'] ?? 'Show more';
|
||||
spoilerLink.textContent = new IntlMessageFormat(
|
||||
message,
|
||||
locale,
|
||||
|
|
@ -8,7 +8,7 @@ and performs no other task.
|
|||
|
||||
*/
|
||||
|
||||
import 'packs/public-path';
|
||||
import '@/entrypoints/public-path';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
|
|
@ -67,7 +67,9 @@ const fetchInteractionURLFailure = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const isValidDomain = (value: string) => {
|
||||
const isValidDomain = (value: unknown) => {
|
||||
if (typeof value !== 'string') return false;
|
||||
|
||||
const url = new URL('https:///path');
|
||||
url.hostname = value;
|
||||
return url.hostname === value;
|
||||
|
|
@ -124,6 +126,11 @@ const fromAcct = (acct: string) => {
|
|||
const domain = segments[1];
|
||||
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
|
||||
|
||||
if (!domain) {
|
||||
fetchInteractionURLFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
axios
|
||||
.get(`https://${domain}/.well-known/webfinger`, {
|
||||
params: { resource: `acct:${acct}` },
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import 'packs/public-path';
|
||||
import '@/entrypoints/public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { start } from 'flavours/glitch/common';
|
||||
|
|
@ -16,7 +16,7 @@ function loaded() {
|
|||
|
||||
if (!attr) return;
|
||||
|
||||
const props = JSON.parse(attr);
|
||||
const props = JSON.parse(attr) as object;
|
||||
const root = createRoot(mountNode);
|
||||
|
||||
root.render(<ComposeContainer {...props} />);
|
||||
|
|
@ -24,9 +24,13 @@ function loaded() {
|
|||
}
|
||||
|
||||
function main() {
|
||||
ready(loaded);
|
||||
ready(loaded).catch((error: unknown) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
loadPolyfills().then(main).catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
loadPolyfills()
|
||||
.then(main)
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
});
|
||||
48
app/javascript/flavours/glitch/entrypoints/sign_up.ts
Normal file
48
app/javascript/flavours/glitch/entrypoints/sign_up.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import '@/entrypoints/public-path';
|
||||
import axios from 'axios';
|
||||
|
||||
import ready from 'flavours/glitch/ready';
|
||||
|
||||
async function checkConfirmation() {
|
||||
const response = await axios.get('/api/v1/emails/check_confirmation');
|
||||
|
||||
if (response.data) {
|
||||
window.location.href = '/start';
|
||||
}
|
||||
}
|
||||
|
||||
ready(() => {
|
||||
setInterval(() => {
|
||||
void checkConfirmation();
|
||||
}, 5000);
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLButtonElement>('button.timer-button')
|
||||
.forEach((button) => {
|
||||
let counter = 30;
|
||||
|
||||
const container = document.createElement('span');
|
||||
|
||||
const updateCounter = () => {
|
||||
container.innerText = ` (${counter})`;
|
||||
};
|
||||
|
||||
updateCounter();
|
||||
|
||||
const countdown = setInterval(() => {
|
||||
counter--;
|
||||
|
||||
if (counter === 0) {
|
||||
button.disabled = false;
|
||||
button.removeChild(container);
|
||||
clearInterval(countdown);
|
||||
} else {
|
||||
updateCounter();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
button.appendChild(container);
|
||||
});
|
||||
}).catch((e: unknown) => {
|
||||
throw e;
|
||||
});
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import * as WebAuthnJSON from '@github/webauthn-json';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
import ready from 'flavours/glitch/ready';
|
||||
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
type PublicKeyCredentialCreationOptionsJSON =
|
||||
WebAuthnJSON.CredentialCreationOptionsJSON['publicKey'];
|
||||
|
||||
function exceptionHasAxiosError(
|
||||
error: unknown,
|
||||
): error is AxiosError<{ error: unknown }> {
|
||||
return (
|
||||
error instanceof AxiosError &&
|
||||
typeof error.response?.data === 'object' &&
|
||||
'error' in error.response.data
|
||||
);
|
||||
}
|
||||
|
||||
function logAxiosResponseError(error: unknown) {
|
||||
if (exceptionHasAxiosError(error)) console.error(error);
|
||||
}
|
||||
|
||||
function getCSRFToken() {
|
||||
return document
|
||||
.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')
|
||||
?.getAttribute('content');
|
||||
}
|
||||
|
||||
function hideFlashMessages() {
|
||||
document.querySelectorAll('.flash-message').forEach((flashMessage) => {
|
||||
flashMessage.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
async function callback(
|
||||
url: string,
|
||||
body:
|
||||
| {
|
||||
credential: WebAuthnJSON.PublicKeyCredentialWithAttestationJSON;
|
||||
nickname: string;
|
||||
}
|
||||
| {
|
||||
user: { credential: WebAuthnJSON.PublicKeyCredentialWithAssertionJSON };
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const response = await axios.post<{ redirect_path: string }>(
|
||||
url,
|
||||
JSON.stringify(body),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': getCSRFToken(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
window.location.replace(response.data.redirect_path);
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response?.status === 422) {
|
||||
const errorMessage = document.getElementById(
|
||||
'security-key-error-message',
|
||||
);
|
||||
errorMessage?.classList.remove('hidden');
|
||||
|
||||
logAxiosResponseError(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWebauthnCredentialRegistration(nickname: string) {
|
||||
try {
|
||||
const response = await axios.get<PublicKeyCredentialCreationOptionsJSON>(
|
||||
'/settings/security_keys/options',
|
||||
);
|
||||
|
||||
const credentialOptions = response.data;
|
||||
|
||||
try {
|
||||
const credential = await WebAuthnJSON.create({
|
||||
publicKey: credentialOptions,
|
||||
});
|
||||
|
||||
const params = {
|
||||
credential: credential,
|
||||
nickname: nickname,
|
||||
};
|
||||
|
||||
await callback('/settings/security_keys', params);
|
||||
} catch (error) {
|
||||
const errorMessage = document.getElementById(
|
||||
'security-key-error-message',
|
||||
);
|
||||
errorMessage?.classList.remove('hidden');
|
||||
console.error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
logAxiosResponseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWebauthnCredentialAuthentication() {
|
||||
try {
|
||||
const response = await axios.get<PublicKeyCredentialCreationOptionsJSON>(
|
||||
'sessions/security_key_options',
|
||||
);
|
||||
|
||||
const credentialOptions = response.data;
|
||||
|
||||
try {
|
||||
const credential = await WebAuthnJSON.get({
|
||||
publicKey: credentialOptions,
|
||||
});
|
||||
|
||||
const params = { user: { credential: credential } };
|
||||
void callback('sign_in', params);
|
||||
} catch (error) {
|
||||
const errorMessage = document.getElementById(
|
||||
'security-key-error-message',
|
||||
);
|
||||
errorMessage?.classList.remove('hidden');
|
||||
console.error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
logAxiosResponseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
ready(() => {
|
||||
if (!WebAuthnJSON.supported()) {
|
||||
const unsupported_browser_message = document.getElementById(
|
||||
'unsupported-browser-message',
|
||||
);
|
||||
if (unsupported_browser_message) {
|
||||
unsupported_browser_message.classList.remove('hidden');
|
||||
const button = document.querySelector<HTMLButtonElement>(
|
||||
'button.btn.js-webauthn',
|
||||
);
|
||||
if (button) button.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
const webAuthnCredentialRegistrationForm =
|
||||
document.querySelector<HTMLFormElement>('form#new_webauthn_credential');
|
||||
if (webAuthnCredentialRegistrationForm) {
|
||||
webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(event.target instanceof HTMLFormElement)) return;
|
||||
|
||||
const nickname = event.target.querySelector<HTMLInputElement>(
|
||||
'input[name="new_webauthn_credential[nickname]"]',
|
||||
);
|
||||
|
||||
if (nickname?.value) {
|
||||
void handleWebauthnCredentialRegistration(nickname.value);
|
||||
} else {
|
||||
nickname?.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const webAuthnCredentialAuthenticationForm =
|
||||
document.getElementById('webauthn-form');
|
||||
if (webAuthnCredentialAuthenticationForm) {
|
||||
webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
void handleWebauthnCredentialAuthentication();
|
||||
});
|
||||
|
||||
const otpAuthenticationForm = document.getElementById(
|
||||
'otp-authentication-form',
|
||||
);
|
||||
|
||||
const linkToOtp = document.getElementById('link-to-otp');
|
||||
|
||||
linkToOtp?.addEventListener('click', () => {
|
||||
webAuthnCredentialAuthenticationForm.classList.add('hidden');
|
||||
otpAuthenticationForm?.classList.remove('hidden');
|
||||
hideFlashMessages();
|
||||
});
|
||||
|
||||
const linkToWebAuthn = document.getElementById('link-to-webauthn');
|
||||
linkToWebAuthn?.addEventListener('click', () => {
|
||||
otpAuthenticationForm?.classList.add('hidden');
|
||||
webAuthnCredentialAuthenticationForm.classList.remove('hidden');
|
||||
hideFlashMessages();
|
||||
});
|
||||
}
|
||||
}).catch((e: unknown) => {
|
||||
throw e;
|
||||
});
|
||||
|
|
@ -23,6 +23,7 @@ import { Icon } from 'flavours/glitch/components/icon';
|
|||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
import { autoPlayGif, me, domain as localDomain } from 'flavours/glitch/initial_state';
|
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
||||
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
|
||||
|
|
@ -94,6 +95,7 @@ const dateFormatOptions = {
|
|||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
account: ImmutablePropTypes.record,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
|
|
@ -117,10 +119,6 @@ class Header extends ImmutablePureComponent {
|
|||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
openEditProfile = () => {
|
||||
window.open(profileLink, '_blank');
|
||||
};
|
||||
|
|
@ -170,7 +168,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
render () {
|
||||
const { account, hidden, intl } = this.props;
|
||||
const { signedIn, permissions } = this.context.identity;
|
||||
const { signedIn, permissions } = this.props.identity;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
|
|
@ -414,4 +412,4 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withRouter(injectIntl(Header));
|
||||
export default withRouter(withIdentity(injectIntl(Header)));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { account }) => ({
|
|||
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||
|
||||
onSave (value) {
|
||||
dispatch(submitAccountNote({ id: account.get('id'), value}));
|
||||
dispatch(submitAccountNote({ accountId: account.get('id'), note: value }));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
|||
import Header from '../components/header';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
||||
});
|
||||
|
|
@ -43,7 +42,7 @@ const makeMapStateToProps = () => {
|
|||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onFollow (account) {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
|
|
@ -52,15 +51,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||
},
|
||||
}));
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
import { domain } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
|
|
@ -40,16 +41,12 @@ const mapStateToProps = (state, { columnId }) => {
|
|||
};
|
||||
|
||||
class CommunityTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onlyMedia: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
|
@ -80,7 +77,7 @@ class CommunityTimeline extends PureComponent {
|
|||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
const { signedIn } = this.props.identity;
|
||||
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
|
||||
|
|
@ -90,7 +87,7 @@ class CommunityTimeline extends PureComponent {
|
|||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { signedIn } = this.props.identity;
|
||||
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
|
|
@ -165,4 +162,4 @@ class CommunityTimeline extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(CommunityTimeline));
|
||||
export default withIdentity(connect(mapStateToProps)(injectIntl(CommunityTimeline)));
|
||||
|
|
|
|||
|
|
@ -110,18 +110,6 @@ class LanguageDropdownMenu extends PureComponent {
|
|||
}).map(result => result.obj);
|
||||
}
|
||||
|
||||
frequentlyUsed () {
|
||||
const { languages, value } = this.props;
|
||||
const current = languages.find(lang => lang[0] === value);
|
||||
const results = [];
|
||||
|
||||
if (current) {
|
||||
results.push(current);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
|||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
import { domain, searchEnabled } from 'flavours/glitch/initial_state';
|
||||
import { HASHTAG_REGEX } from 'flavours/glitch/utils/hashtags';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
|
@ -33,12 +34,8 @@ const labelForRecentSearch = search => {
|
|||
};
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
value: PropTypes.string.isRequired,
|
||||
recent: ImmutablePropTypes.orderedSet,
|
||||
submitted: PropTypes.bool,
|
||||
|
|
@ -276,7 +273,7 @@ class Search extends PureComponent {
|
|||
}
|
||||
|
||||
_calculateOptions (value) {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { signedIn } = this.props.identity;
|
||||
const trimmedValue = value.trim();
|
||||
const options = [];
|
||||
|
||||
|
|
@ -318,7 +315,7 @@ class Search extends PureComponent {
|
|||
render () {
|
||||
const { intl, value, submitted, recent } = this.props;
|
||||
const { expanded, options, selectedOption } = this.state;
|
||||
const { signedIn } = this.context.identity;
|
||||
const { signedIn } = this.props.identity;
|
||||
|
||||
const hasValue = value.length > 0 || submitted;
|
||||
|
||||
|
|
@ -402,4 +399,4 @@ class Search extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withRouter(injectIntl(Search));
|
||||
export default withRouter(withIdentity(injectIntl(Search)));
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||
import { expandSearch } from 'flavours/glitch/actions/search';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { SearchSection } from 'flavours/glitch/features/explore/components/search_section';
|
||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
|
|
@ -26,62 +26,68 @@ const withoutLastResult = list => {
|
|||
}
|
||||
};
|
||||
|
||||
class SearchResults extends ImmutablePureComponent {
|
||||
export const SearchResults = () => {
|
||||
const results = useAppSelector((state) => state.getIn(['search', 'results']));
|
||||
const isLoading = useAppSelector((state) => state.getIn(['search', 'isLoading']));
|
||||
|
||||
static propTypes = {
|
||||
results: ImmutablePropTypes.map.isRequired,
|
||||
expandSearch: PropTypes.func.isRequired,
|
||||
searchTerm: PropTypes.string,
|
||||
};
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
handleLoadMoreAccounts = () => this.props.expandSearch('accounts');
|
||||
const handleLoadMoreAccounts = useCallback(() => {
|
||||
dispatch(expandSearch('accounts'));
|
||||
}, [dispatch]);
|
||||
|
||||
handleLoadMoreStatuses = () => this.props.expandSearch('statuses');
|
||||
const handleLoadMoreStatuses = useCallback(() => {
|
||||
dispatch(expandSearch('statuses'));
|
||||
}, [dispatch]);
|
||||
|
||||
handleLoadMoreHashtags = () => this.props.expandSearch('hashtags');
|
||||
const handleLoadMoreHashtags = useCallback(() => {
|
||||
dispatch(expandSearch('hashtags'));
|
||||
}, [dispatch]);
|
||||
|
||||
render () {
|
||||
const { results } = this.props;
|
||||
let accounts, statuses, hashtags;
|
||||
|
||||
let accounts, statuses, hashtags;
|
||||
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
accounts = (
|
||||
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
|
||||
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
hashtags = (
|
||||
<SearchSection title={<><Icon id='hashtag' icon={TagIcon} /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
|
||||
{withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
{(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
statuses = (
|
||||
<SearchSection title={<><Icon id='quote-right' icon={FindInPageIcon} /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
|
||||
{withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
{(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className='search-results'>
|
||||
{accounts}
|
||||
{hashtags}
|
||||
{statuses}
|
||||
</div>
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
accounts = (
|
||||
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
|
||||
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreAccounts} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
hashtags = (
|
||||
<SearchSection title={<><Icon id='hashtag' icon={TagIcon} /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
|
||||
{withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
{(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreHashtags} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchResults;
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
statuses = (
|
||||
<SearchSection title={<><Icon id='quote-right' icon={FindInPageIcon} /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
|
||||
{withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
{(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreStatuses} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='search-results'>
|
||||
{!accounts && !hashtags && !statuses && (
|
||||
isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{accounts}
|
||||
{hashtags}
|
||||
{statuses}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { expandSearch } from 'flavours/glitch/actions/search';
|
||||
import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions';
|
||||
|
||||
import SearchResults from '../components/search_results';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
results: state.getIn(['search', 'results']),
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
searchTerm: state.getIn(['search', 'searchTerm']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchSuggestions: () => dispatch(fetchSuggestions()),
|
||||
expandSearch: type => dispatch(expandSearch(type)),
|
||||
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
|
||||
|
|
@ -9,7 +9,7 @@ const mapStateToProps = state => {
|
|||
const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0;
|
||||
const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0;
|
||||
const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
|
||||
const isOverLimit = attachmentsSize > 3;
|
||||
const isOverLimit = attachmentsSize > state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments'])-1;
|
||||
const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ import { mascot } from '../../initial_state';
|
|||
import { isMobile } from '../../is_mobile';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import { SearchResults } from './components/search_results';
|
||||
import ComposeFormContainer from './containers/compose_form_container';
|
||||
import SearchContainer from './containers/search_container';
|
||||
import SearchResultsContainer from './containers/search_results_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
|
|
@ -183,7 +183,7 @@ class Compose extends PureComponent {
|
|||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
<SearchResultsContainer />
|
||||
<SearchResults />
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
|||
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
||||
|
||||
const names = accounts.map(a => (
|
||||
<Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}>
|
||||
<Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} data-hover-card-account={a.get('id')}>
|
||||
<bdi>
|
||||
<strong
|
||||
className='display-name__html'
|
||||
|
|
|
|||
|
|
@ -1,247 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
} from 'flavours/glitch/actions/accounts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { autoPlayGif, me } from 'flavours/glitch/initial_state';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
|
||||
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
account: getAccount(state, id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onFollow(account) {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id='confirmations.unfollow.message'
|
||||
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
),
|
||||
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||
} }),
|
||||
);
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onBlock(account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onMute(account) {
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
class AccountCard extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onDismiss: PropTypes.func,
|
||||
};
|
||||
|
||||
handleMouseEnter = ({ currentTarget }) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
||||
|
||||
for (var i = 0; i < emojis.length; i++) {
|
||||
let emoji = emojis[i];
|
||||
emoji.src = emoji.getAttribute('data-original');
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseLeave = ({ currentTarget }) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
||||
|
||||
for (var i = 0; i < emojis.length; i++) {
|
||||
let emoji = emojis[i];
|
||||
emoji.src = emoji.getAttribute('data-static');
|
||||
}
|
||||
};
|
||||
|
||||
handleFollow = () => {
|
||||
this.props.onFollow(this.props.account);
|
||||
};
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
};
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
};
|
||||
|
||||
handleEditProfile = () => {
|
||||
window.open('/settings/profile', '_blank');
|
||||
};
|
||||
|
||||
handleDismiss = (e) => {
|
||||
const { account, onDismiss } = this.props;
|
||||
onDismiss(account.get('id'));
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
let actionBtn;
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
|
||||
} else if (account.getIn(['relationship', 'muting'])) {
|
||||
actionBtn = <Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
}
|
||||
} else {
|
||||
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account-card'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
||||
<div className='account-card__header'>
|
||||
{this.props.onDismiss && <IconButton className='media-modal__close' title={intl.formatMessage(messages.dismissSuggestion)} icon='times' onClick={this.handleDismiss} size={20} />}
|
||||
|
||||
<img
|
||||
src={
|
||||
autoPlayGif ? account.get('header') : account.get('header_static')
|
||||
}
|
||||
alt=''
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='account-card__title'>
|
||||
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</Permalink>
|
||||
|
||||
{account.get('note').length > 0 && (
|
||||
<div
|
||||
className='account-card__bio translate'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='account-card__actions'>
|
||||
<div className='account-card__counters'>
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('statuses_count')} />
|
||||
<small>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
{account.get('followers_count') < 0 ? '-' : <ShortNumber value={account.get('followers_count')} />}{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
id='account.followers'
|
||||
defaultMessage='Followers'
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('following_count')} />{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
id='account.following'
|
||||
defaultMessage='Following'
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account-card__actions__button'>
|
||||
{actionBtn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(AccountCard));
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
import type { MouseEventHandler } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
} from 'flavours/glitch/actions/accounts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { autoPlayGif, me } from 'flavours/glitch/initial_state';
|
||||
import type { Account } from 'flavours/glitch/models/account';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
cancel_follow_request: {
|
||||
id: 'account.cancel_follow_request',
|
||||
defaultMessage: 'Withdraw follow request',
|
||||
},
|
||||
cancelFollowRequestConfirm: {
|
||||
id: 'confirmations.cancel_follow_request.confirm',
|
||||
defaultMessage: 'Withdraw request',
|
||||
},
|
||||
requested: {
|
||||
id: 'account.requested',
|
||||
defaultMessage: 'Awaiting approval. Click to cancel follow request',
|
||||
},
|
||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||
unfollowConfirm: {
|
||||
id: 'confirmations.unfollow.confirm',
|
||||
defaultMessage: 'Unfollow',
|
||||
},
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
});
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const account = useAppSelector((s) => getAccount(s, accountId));
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleMouseEnter = useCallback<MouseEventHandler>(
|
||||
({ currentTarget }) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
const emojis =
|
||||
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
||||
|
||||
emojis.forEach((emoji) => {
|
||||
const original = emoji.getAttribute('data-original');
|
||||
if (original) emoji.src = original;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMouseLeave = useCallback<MouseEventHandler>(
|
||||
({ currentTarget }) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojis =
|
||||
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
||||
|
||||
emojis.forEach((emoji) => {
|
||||
const staticUrl = emoji.getAttribute('data-static');
|
||||
if (staticUrl) emoji.src = staticUrl;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleFollow = useCallback(() => {
|
||||
if (!account) return;
|
||||
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id='confirmations.unfollow.message'
|
||||
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
),
|
||||
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id='confirmations.cancel_follow_request.message'
|
||||
defaultMessage='Are you sure you want to withdraw your request to follow {name}?'
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
),
|
||||
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
}, [account, dispatch, intl]);
|
||||
|
||||
const handleBlock = useCallback(() => {
|
||||
if (account?.relationship?.blocking) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
}
|
||||
}, [account, dispatch]);
|
||||
|
||||
const handleMute = useCallback(() => {
|
||||
if (account?.relationship?.muting) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
}
|
||||
}, [account, dispatch]);
|
||||
|
||||
const handleEditProfile = useCallback(() => {
|
||||
window.open('/settings/profile', '_blank');
|
||||
}, []);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
let actionBtn;
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) {
|
||||
// Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.cancel_follow_request)}
|
||||
title={intl.formatMessage(messages.requested)}
|
||||
onClick={handleFollow}
|
||||
/>
|
||||
);
|
||||
} else if (account.getIn(['relationship', 'muting'])) {
|
||||
actionBtn = (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.unmute)}
|
||||
onClick={handleMute}
|
||||
/>
|
||||
);
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = (
|
||||
<Button
|
||||
disabled={account.relationship?.blocked_by}
|
||||
className={classNames({
|
||||
'button--destructive': account.getIn(['relationship', 'following']),
|
||||
})}
|
||||
text={intl.formatMessage(
|
||||
account.getIn(['relationship', 'following'])
|
||||
? messages.unfollow
|
||||
: messages.follow,
|
||||
)}
|
||||
onClick={handleFollow}
|
||||
/>
|
||||
);
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.unblock)}
|
||||
onClick={handleBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
actionBtn = (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.edit_profile)}
|
||||
onClick={handleEditProfile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account-card'>
|
||||
<Permalink
|
||||
href={account.get('url')}
|
||||
to={`/@${account.get('acct')}`}
|
||||
className='account-card__permalink'
|
||||
>
|
||||
<div className='account-card__header'>
|
||||
<img
|
||||
src={
|
||||
autoPlayGif ? account.get('header') : account.get('header_static')
|
||||
}
|
||||
alt=''
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='account-card__title'>
|
||||
<div className='account-card__title__avatar'>
|
||||
<Avatar account={account as Account} size={56} />
|
||||
</div>
|
||||
<DisplayName account={account as Account} />
|
||||
</div>
|
||||
</Permalink>
|
||||
|
||||
{account.get('note').length > 0 && (
|
||||
<div
|
||||
className='account-card__bio translate'
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='account-card__actions'>
|
||||
<div className='account-card__counters'>
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('statuses_count')} />
|
||||
<small>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('followers_count')} />{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
id='account.followers'
|
||||
defaultMessage='Followers'
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('following_count')} />{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
id='account.following'
|
||||
defaultMessage='Following'
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account-card__actions__button'>{actionBtn}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns';
|
||||
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { RadioButton } from 'flavours/glitch/components/radio_button';
|
||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||
|
||||
import AccountCard from './components/account_card';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||
recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
|
||||
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
|
||||
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
|
||||
federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
});
|
||||
|
||||
class Directory extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
domain: PropTypes.string.isRequired,
|
||||
params: PropTypes.shape({
|
||||
order: PropTypes.string,
|
||||
local: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
state = {
|
||||
order: null,
|
||||
local: null,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
|
||||
}
|
||||
};
|
||||
|
||||
getParams = (props, state) => ({
|
||||
order: state.order === null ? (props.params.order || 'active') : state.order,
|
||||
local: state.local === null ? (props.params.local || false) : state.local,
|
||||
});
|
||||
|
||||
handleMove = dir => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
};
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchDirectory(this.getParams(this.props, this.state)));
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
const { dispatch } = this.props;
|
||||
const paramsOld = this.getParams(prevProps, prevState);
|
||||
const paramsNew = this.getParams(this.props, this.state);
|
||||
|
||||
if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
|
||||
dispatch(fetchDirectory(paramsNew));
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleChangeOrder = e => {
|
||||
const { dispatch, columnId } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, ['order'], e.target.value));
|
||||
} else {
|
||||
this.setState({ order: e.target.value });
|
||||
}
|
||||
};
|
||||
|
||||
handleChangeLocal = e => {
|
||||
const { dispatch, columnId } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
|
||||
} else {
|
||||
this.setState({ local: e.target.value === '1' });
|
||||
}
|
||||
};
|
||||
|
||||
handleLoadMore = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(expandDirectory(this.getParams(this.props, this.state)));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props;
|
||||
const { order, local } = this.getParams(this.props, this.state);
|
||||
const pinned = !!columnId;
|
||||
|
||||
const scrollableArea = (
|
||||
<div className='scrollable'>
|
||||
<div className='filter-form'>
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
|
||||
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
|
||||
</div>
|
||||
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} />
|
||||
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='directory__list'>
|
||||
{isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
|
||||
<AccountCard id={accountId} key={accountId} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='address-book-o'
|
||||
iconComponent={PeopleIcon}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Directory));
|
||||
219
app/javascript/flavours/glitch/features/directory/index.tsx
Normal file
219
app/javascript/flavours/glitch/features/directory/index.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import type { ChangeEventHandler } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import {
|
||||
addColumn,
|
||||
removeColumn,
|
||||
moveColumn,
|
||||
changeColumnParams,
|
||||
} from 'flavours/glitch/actions/columns';
|
||||
import {
|
||||
fetchDirectory,
|
||||
expandDirectory,
|
||||
} from 'flavours/glitch/actions/directory';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { RadioButton } from 'flavours/glitch/components/radio_button';
|
||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
import { AccountCard } from './components/account_card';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||
recentlyActive: {
|
||||
id: 'directory.recently_active',
|
||||
defaultMessage: 'Recently active',
|
||||
},
|
||||
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
|
||||
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
|
||||
federated: {
|
||||
id: 'directory.federated',
|
||||
defaultMessage: 'From known fediverse',
|
||||
},
|
||||
});
|
||||
|
||||
export const Directory: React.FC<{
|
||||
columnId?: string;
|
||||
multiColumn?: boolean;
|
||||
params?: { order: string; local?: boolean };
|
||||
}> = ({ columnId, multiColumn, params }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [state, setState] = useState<{
|
||||
order: string | null;
|
||||
local: boolean | null;
|
||||
}>({
|
||||
order: null,
|
||||
local: null,
|
||||
});
|
||||
|
||||
const column = useRef<Column>(null);
|
||||
|
||||
const order = state.order ?? params?.order ?? 'active';
|
||||
const local = state.local ?? params?.local ?? false;
|
||||
|
||||
const handlePin = useCallback(() => {
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('DIRECTORY', { order, local }));
|
||||
}
|
||||
}, [dispatch, columnId, order, local]);
|
||||
|
||||
const domain = useAppSelector((s) => s.meta.get('domain') as string);
|
||||
const accountIds = useAppSelector(
|
||||
(state) =>
|
||||
state.user_lists.getIn(
|
||||
['directory', 'items'],
|
||||
ImmutableList(),
|
||||
) as ImmutableList<string>,
|
||||
);
|
||||
const isLoading = useAppSelector(
|
||||
(state) =>
|
||||
state.user_lists.getIn(['directory', 'isLoading'], true) as boolean,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void dispatch(fetchDirectory({ order, local }));
|
||||
}, [dispatch, order, local]);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(dir: number) => {
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
},
|
||||
[dispatch, columnId],
|
||||
);
|
||||
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
column.current?.scrollTop();
|
||||
}, []);
|
||||
|
||||
const handleChangeOrder = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
(e) => {
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, ['order'], e.target.value));
|
||||
} else {
|
||||
setState((s) => ({ order: e.target.value, local: s.local }));
|
||||
}
|
||||
},
|
||||
[dispatch, columnId],
|
||||
);
|
||||
|
||||
const handleChangeLocal = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
(e) => {
|
||||
if (columnId) {
|
||||
dispatch(
|
||||
changeColumnParams(columnId, ['local'], e.target.value === '1'),
|
||||
);
|
||||
} else {
|
||||
setState((s) => ({ local: e.target.value === '1', order: s.order }));
|
||||
}
|
||||
},
|
||||
[dispatch, columnId],
|
||||
);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
void dispatch(expandDirectory({ order, local }));
|
||||
}, [dispatch, order, local]);
|
||||
|
||||
const pinned = !!columnId;
|
||||
|
||||
const scrollableArea = (
|
||||
<div className='scrollable'>
|
||||
<div className='filter-form'>
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton
|
||||
name='order'
|
||||
value='active'
|
||||
label={intl.formatMessage(messages.recentlyActive)}
|
||||
checked={order === 'active'}
|
||||
onChange={handleChangeOrder}
|
||||
/>
|
||||
<RadioButton
|
||||
name='order'
|
||||
value='new'
|
||||
label={intl.formatMessage(messages.newArrivals)}
|
||||
checked={order === 'new'}
|
||||
onChange={handleChangeOrder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton
|
||||
name='local'
|
||||
value='1'
|
||||
label={intl.formatMessage(messages.local, { domain })}
|
||||
checked={local}
|
||||
onChange={handleChangeLocal}
|
||||
/>
|
||||
<RadioButton
|
||||
name='local'
|
||||
value='0'
|
||||
label={intl.formatMessage(messages.federated)}
|
||||
checked={!local}
|
||||
onChange={handleChangeLocal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='directory__list'>
|
||||
{isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
accountIds.map((accountId) => (
|
||||
<AccountCard accountId={accountId} key={accountId} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LoadMore onClick={handleLoadMore} visible={!isLoading} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
ref={column}
|
||||
label={intl.formatMessage(messages.title)}
|
||||
>
|
||||
<ColumnHeader
|
||||
icon='address-book-o'
|
||||
iconComponent={PeopleIcon}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={handlePin}
|
||||
onMove={handleMove}
|
||||
onClick={handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
{multiColumn && !pinned ? (
|
||||
// @ts-expect-error ScrollContainer is not properly typed yet
|
||||
<ScrollContainer scrollKey='directory'>
|
||||
{scrollableArea}
|
||||
</ScrollContainer>
|
||||
) : (
|
||||
scrollableArea
|
||||
)}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- Needed because this is called as an async components
|
||||
export default Directory;
|
||||
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