Merge remote-tracking branch 'origin/develop' into chats
This commit is contained in:
commit
acede4b519
93 changed files with 4342 additions and 4288 deletions
|
@ -1,5 +1,5 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
|
@ -359,14 +359,30 @@ const unblockAccountFail = (error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const muteAccount = (id: string, notifications?: boolean) =>
|
||||
const muteAccount = (id: string, notifications?: boolean, duration = 0) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
||||
dispatch(muteAccountRequest(id));
|
||||
|
||||
const params: Record<string, any> = {
|
||||
notifications,
|
||||
};
|
||||
|
||||
if (duration) {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const v = parseVersion(instance.version);
|
||||
|
||||
if (v.software === PLEROMA) {
|
||||
params.expires_in = duration;
|
||||
} else {
|
||||
params.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/mute`, { notifications })
|
||||
.post(`/api/v1/accounts/${id}/mute`, params)
|
||||
.then(response => {
|
||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||
return dispatch(muteAccountSuccess(response.data, getState().statuses));
|
||||
|
|
|
@ -5,7 +5,7 @@ import { httpErrorMessages } from 'soapbox/utils/errors';
|
|||
import type { SnackbarActionSeverity } from './snackbar';
|
||||
import type { AnyAction } from '@reduxjs/toolkit';
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { NotificationObject } from 'soapbox/react-notification';
|
||||
import type { NotificationObject } from 'react-notification';
|
||||
|
||||
const messages = defineMessages({
|
||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { History } from 'history';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
|
||||
const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
|
||||
const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
|
||||
|
||||
const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
|
||||
const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
|
||||
const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
|
||||
|
||||
const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE';
|
||||
const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET';
|
||||
const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP';
|
||||
|
||||
const submit = (routerHistory: History) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const groupId = getState().group_editor.get('groupId') as string;
|
||||
const title = getState().group_editor.get('title') as string;
|
||||
const description = getState().group_editor.get('description') as string;
|
||||
const coverImage = getState().group_editor.get('coverImage') as any;
|
||||
|
||||
if (groupId === null) {
|
||||
dispatch(create(title, description, coverImage, routerHistory));
|
||||
} else {
|
||||
dispatch(update(groupId, title, description, coverImage, routerHistory));
|
||||
}
|
||||
};
|
||||
|
||||
const create = (title: string, description: string, coverImage: File, routerHistory: History) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(createRequest());
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('title', title);
|
||||
formData.append('description', description);
|
||||
|
||||
if (coverImage !== null) {
|
||||
formData.append('cover_image', coverImage);
|
||||
}
|
||||
|
||||
api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
|
||||
dispatch(createSuccess(data));
|
||||
routerHistory.push(`/groups/${data.id}`);
|
||||
}).catch(err => dispatch(createFail(err)));
|
||||
};
|
||||
|
||||
const createRequest = (id?: string) => ({
|
||||
type: GROUP_CREATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const createSuccess = (group: APIEntity) => ({
|
||||
type: GROUP_CREATE_SUCCESS,
|
||||
group,
|
||||
});
|
||||
|
||||
const createFail = (error: AxiosError) => ({
|
||||
type: GROUP_CREATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const update = (groupId: string, title: string, description: string, coverImage: File, routerHistory: History) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(updateRequest(groupId));
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('title', title);
|
||||
formData.append('description', description);
|
||||
|
||||
if (coverImage !== null) {
|
||||
formData.append('cover_image', coverImage);
|
||||
}
|
||||
|
||||
api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
|
||||
dispatch(updateSuccess(data));
|
||||
routerHistory.push(`/groups/${data.id}`);
|
||||
}).catch(err => dispatch(updateFail(err)));
|
||||
};
|
||||
|
||||
const updateRequest = (id: string) => ({
|
||||
type: GROUP_UPDATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const updateSuccess = (group: APIEntity) => ({
|
||||
type: GROUP_UPDATE_SUCCESS,
|
||||
group,
|
||||
});
|
||||
|
||||
const updateFail = (error: AxiosError) => ({
|
||||
type: GROUP_UPDATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const changeValue = (field: string, value: string | File) => ({
|
||||
type: GROUP_EDITOR_VALUE_CHANGE,
|
||||
field,
|
||||
value,
|
||||
});
|
||||
|
||||
const reset = () => ({
|
||||
type: GROUP_EDITOR_RESET,
|
||||
});
|
||||
|
||||
const setUp = (group: string) => ({
|
||||
type: GROUP_EDITOR_SETUP,
|
||||
group,
|
||||
});
|
||||
|
||||
export {
|
||||
GROUP_CREATE_REQUEST,
|
||||
GROUP_CREATE_SUCCESS,
|
||||
GROUP_CREATE_FAIL,
|
||||
GROUP_UPDATE_REQUEST,
|
||||
GROUP_UPDATE_SUCCESS,
|
||||
GROUP_UPDATE_FAIL,
|
||||
GROUP_EDITOR_VALUE_CHANGE,
|
||||
GROUP_EDITOR_RESET,
|
||||
GROUP_EDITOR_SETUP,
|
||||
submit,
|
||||
create,
|
||||
createRequest,
|
||||
createSuccess,
|
||||
createFail,
|
||||
update,
|
||||
updateRequest,
|
||||
updateSuccess,
|
||||
updateFail,
|
||||
changeValue,
|
||||
reset,
|
||||
setUp,
|
||||
};
|
|
@ -1,550 +0,0 @@
|
|||
import { AxiosError } from 'axios';
|
||||
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
|
||||
const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
|
||||
const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
|
||||
|
||||
const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
|
||||
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
|
||||
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
|
||||
|
||||
const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST';
|
||||
const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS';
|
||||
const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL';
|
||||
|
||||
const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST';
|
||||
const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS';
|
||||
const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL';
|
||||
|
||||
const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
|
||||
const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
|
||||
const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
|
||||
|
||||
const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST';
|
||||
const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS';
|
||||
const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL';
|
||||
|
||||
const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST';
|
||||
const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS';
|
||||
const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL';
|
||||
|
||||
const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST';
|
||||
const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS';
|
||||
const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL';
|
||||
|
||||
const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST';
|
||||
const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS';
|
||||
const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL';
|
||||
|
||||
const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST';
|
||||
const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS';
|
||||
const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL';
|
||||
|
||||
const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST';
|
||||
const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS';
|
||||
const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL';
|
||||
|
||||
const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST';
|
||||
const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
|
||||
const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
|
||||
|
||||
const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchGroupRelationships([id]));
|
||||
|
||||
if (getState().groups.get(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchGroupRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/groups/${id}`)
|
||||
.then(({ data }) => dispatch(fetchGroupSuccess(data)))
|
||||
.catch(err => dispatch(fetchGroupFail(id, err)));
|
||||
};
|
||||
|
||||
const fetchGroupRequest = (id: string) => ({
|
||||
type: GROUP_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchGroupSuccess = (group: APIEntity) => ({
|
||||
type: GROUP_FETCH_SUCCESS,
|
||||
group,
|
||||
});
|
||||
|
||||
const fetchGroupFail = (id: string, error: AxiosError) => ({
|
||||
type: GROUP_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchGroupRelationships = (groupIds: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const loadedRelationships = getState().group_relationships;
|
||||
const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null);
|
||||
|
||||
if (newGroupIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchGroupRelationshipsRequest(newGroupIds));
|
||||
|
||||
api(getState).get(`/api/v1/groups/${newGroupIds[0]}/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||
dispatch(fetchGroupRelationshipsSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchGroupRelationshipsFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGroupRelationshipsRequest = (ids: string[]) => ({
|
||||
type: GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
||||
ids,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({
|
||||
type: GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
||||
relationships,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const fetchGroupRelationshipsFail = (error: AxiosError) => ({
|
||||
type: GROUP_RELATIONSHIPS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const fetchGroups = (tab: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchGroupsRequest());
|
||||
|
||||
api(getState).get('/api/v1/groups?tab=' + tab)
|
||||
.then(({ data }) => {
|
||||
dispatch(fetchGroupsSuccess(data, tab));
|
||||
dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id)));
|
||||
})
|
||||
.catch(err => dispatch(fetchGroupsFail(err)));
|
||||
};
|
||||
|
||||
const fetchGroupsRequest = () => ({
|
||||
type: GROUPS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchGroupsSuccess = (groups: APIEntity[], tab: string) => ({
|
||||
type: GROUPS_FETCH_SUCCESS,
|
||||
groups,
|
||||
tab,
|
||||
});
|
||||
|
||||
const fetchGroupsFail = (error: AxiosError) => ({
|
||||
type: GROUPS_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const joinGroup = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(joinGroupRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => {
|
||||
dispatch(joinGroupSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(joinGroupFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const leaveGroup = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(leaveGroupRequest(id));
|
||||
|
||||
api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => {
|
||||
dispatch(leaveGroupSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(leaveGroupFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const joinGroupRequest = (id: string) => ({
|
||||
type: GROUP_JOIN_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const joinGroupSuccess = (relationship: APIEntity) => ({
|
||||
type: GROUP_JOIN_SUCCESS,
|
||||
relationship,
|
||||
});
|
||||
|
||||
const joinGroupFail = (id: string, error: AxiosError) => ({
|
||||
type: GROUP_JOIN_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const leaveGroupRequest = (id: string) => ({
|
||||
type: GROUP_LEAVE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const leaveGroupSuccess = (relationship: APIEntity) => ({
|
||||
type: GROUP_LEAVE_SUCCESS,
|
||||
relationship,
|
||||
});
|
||||
|
||||
const leaveGroupFail = (id: string, error: AxiosError) => ({
|
||||
type: GROUP_LEAVE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchMembers = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchMembersRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchMembersFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchMembersRequest = (id: string) => ({
|
||||
type: GROUP_MEMBERS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: GROUP_MEMBERS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchMembersFail = (id: string, error: AxiosError) => ({
|
||||
type: GROUP_MEMBERS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandMembers = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const url = getState().user_lists.groups.get(id)!.next;
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandMembersRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandMembersFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandMembersRequest = (id: string) => ({
|
||||
type: GROUP_MEMBERS_EXPAND_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const expandMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandMembersFail = (id: string, error: AxiosError) => ({
|
||||
type: GROUP_MEMBERS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchRemovedAccounts = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchRemovedAccountsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchRemovedAccountsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchRemovedAccountsRequest = (id: string) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchRemovedAccountsFail = (id: string, error: AxiosError) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandRemovedAccounts = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const url = getState().user_lists.groups_removed_accounts.get(id)!.next;
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandRemovedAccountsRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandRemovedAccountsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandRemovedAccountsRequest = (id: string) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const expandRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandRemovedAccountsFail = (id: string, error: AxiosError) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const removeRemovedAccount = (groupId: string, id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(removeRemovedAccountRequest(groupId, id));
|
||||
|
||||
api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
|
||||
dispatch(removeRemovedAccountSuccess(groupId, id));
|
||||
}).catch(error => {
|
||||
dispatch(removeRemovedAccountFail(groupId, id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const removeRemovedAccountRequest = (groupId: string, id: string) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
|
||||
groupId,
|
||||
id,
|
||||
});
|
||||
|
||||
const removeRemovedAccountSuccess = (groupId: string, id: string) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
|
||||
groupId,
|
||||
id,
|
||||
});
|
||||
|
||||
const removeRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
|
||||
groupId,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const createRemovedAccount = (groupId: string, id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(createRemovedAccountRequest(groupId, id));
|
||||
|
||||
api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
|
||||
dispatch(createRemovedAccountSuccess(groupId, id));
|
||||
}).catch(error => {
|
||||
dispatch(createRemovedAccountFail(groupId, id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const createRemovedAccountRequest = (groupId: string, id: string) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
|
||||
groupId,
|
||||
id,
|
||||
});
|
||||
|
||||
const createRemovedAccountSuccess = (groupId: string, id: string) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
|
||||
groupId,
|
||||
id,
|
||||
});
|
||||
|
||||
const createRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
|
||||
type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
|
||||
groupId,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const groupRemoveStatus = (groupId: string, id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(groupRemoveStatusRequest(groupId, id));
|
||||
|
||||
api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => {
|
||||
dispatch(groupRemoveStatusSuccess(groupId, id));
|
||||
}).catch(error => {
|
||||
dispatch(groupRemoveStatusFail(groupId, id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const groupRemoveStatusRequest = (groupId: string, id: string) => ({
|
||||
type: GROUP_REMOVE_STATUS_REQUEST,
|
||||
groupId,
|
||||
id,
|
||||
});
|
||||
|
||||
const groupRemoveStatusSuccess = (groupId: string, id: string) => ({
|
||||
type: GROUP_REMOVE_STATUS_SUCCESS,
|
||||
groupId,
|
||||
id,
|
||||
});
|
||||
|
||||
const groupRemoveStatusFail = (groupId: string, id: string, error: AxiosError) => ({
|
||||
type: GROUP_REMOVE_STATUS_FAIL,
|
||||
groupId,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export {
|
||||
GROUP_FETCH_REQUEST,
|
||||
GROUP_FETCH_SUCCESS,
|
||||
GROUP_FETCH_FAIL,
|
||||
GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
||||
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
||||
GROUP_RELATIONSHIPS_FETCH_FAIL,
|
||||
GROUPS_FETCH_REQUEST,
|
||||
GROUPS_FETCH_SUCCESS,
|
||||
GROUPS_FETCH_FAIL,
|
||||
GROUP_JOIN_REQUEST,
|
||||
GROUP_JOIN_SUCCESS,
|
||||
GROUP_JOIN_FAIL,
|
||||
GROUP_LEAVE_REQUEST,
|
||||
GROUP_LEAVE_SUCCESS,
|
||||
GROUP_LEAVE_FAIL,
|
||||
GROUP_MEMBERS_FETCH_REQUEST,
|
||||
GROUP_MEMBERS_FETCH_SUCCESS,
|
||||
GROUP_MEMBERS_FETCH_FAIL,
|
||||
GROUP_MEMBERS_EXPAND_REQUEST,
|
||||
GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||
GROUP_MEMBERS_EXPAND_FAIL,
|
||||
GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
|
||||
GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
|
||||
GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
|
||||
GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
|
||||
GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
|
||||
GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
|
||||
GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
|
||||
GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
|
||||
GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
|
||||
GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
|
||||
GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
|
||||
GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
|
||||
GROUP_REMOVE_STATUS_REQUEST,
|
||||
GROUP_REMOVE_STATUS_SUCCESS,
|
||||
GROUP_REMOVE_STATUS_FAIL,
|
||||
fetchGroup,
|
||||
fetchGroupRequest,
|
||||
fetchGroupSuccess,
|
||||
fetchGroupFail,
|
||||
fetchGroupRelationships,
|
||||
fetchGroupRelationshipsRequest,
|
||||
fetchGroupRelationshipsSuccess,
|
||||
fetchGroupRelationshipsFail,
|
||||
fetchGroups,
|
||||
fetchGroupsRequest,
|
||||
fetchGroupsSuccess,
|
||||
fetchGroupsFail,
|
||||
joinGroup,
|
||||
leaveGroup,
|
||||
joinGroupRequest,
|
||||
joinGroupSuccess,
|
||||
joinGroupFail,
|
||||
leaveGroupRequest,
|
||||
leaveGroupSuccess,
|
||||
leaveGroupFail,
|
||||
fetchMembers,
|
||||
fetchMembersRequest,
|
||||
fetchMembersSuccess,
|
||||
fetchMembersFail,
|
||||
expandMembers,
|
||||
expandMembersRequest,
|
||||
expandMembersSuccess,
|
||||
expandMembersFail,
|
||||
fetchRemovedAccounts,
|
||||
fetchRemovedAccountsRequest,
|
||||
fetchRemovedAccountsSuccess,
|
||||
fetchRemovedAccountsFail,
|
||||
expandRemovedAccounts,
|
||||
expandRemovedAccountsRequest,
|
||||
expandRemovedAccountsSuccess,
|
||||
expandRemovedAccountsFail,
|
||||
removeRemovedAccount,
|
||||
removeRemovedAccountRequest,
|
||||
removeRemovedAccountSuccess,
|
||||
removeRemovedAccountFail,
|
||||
createRemovedAccount,
|
||||
createRemovedAccountRequest,
|
||||
createRemovedAccountSuccess,
|
||||
createRemovedAccountFail,
|
||||
groupRemoveStatus,
|
||||
groupRemoveStatusRequest,
|
||||
groupRemoveStatusSuccess,
|
||||
groupRemoveStatusFail,
|
||||
};
|
|
@ -66,7 +66,5 @@ export const loadInstance = createAsyncThunk<void, void, { state: RootState }>(
|
|||
|
||||
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
|
||||
'nodeinfo/fetch',
|
||||
async(_arg, { getState }) => {
|
||||
return await api(getState).get('/nodeinfo/2.1.json');
|
||||
},
|
||||
async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'),
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
|||
|
||||
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
||||
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
||||
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
|
||||
|
||||
const fetchMutes = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
|
@ -103,6 +104,14 @@ const toggleHideNotifications = () =>
|
|||
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
|
||||
};
|
||||
|
||||
const changeMuteDuration = (duration: number) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: MUTES_CHANGE_DURATION,
|
||||
duration,
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
MUTES_FETCH_REQUEST,
|
||||
MUTES_FETCH_SUCCESS,
|
||||
|
@ -112,6 +121,7 @@ export {
|
|||
MUTES_EXPAND_FAIL,
|
||||
MUTES_INIT_MODAL,
|
||||
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||
MUTES_CHANGE_DURATION,
|
||||
fetchMutes,
|
||||
fetchMutesRequest,
|
||||
fetchMutesSuccess,
|
||||
|
@ -122,4 +132,5 @@ export {
|
|||
expandMutesFail,
|
||||
initMuteModal,
|
||||
toggleHideNotifications,
|
||||
changeMuteDuration,
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { getFilters, regexFromFilters } from 'soapbox/selectors';
|
|||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||
import { unescapeHTML } from 'soapbox/utils/html';
|
||||
import { NOTIFICATION_TYPES } from 'soapbox/utils/notification';
|
||||
import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification';
|
||||
import { joinPublicPath } from 'soapbox/utils/static';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
@ -195,7 +195,9 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
|||
|
||||
if (activeFilter === 'all') {
|
||||
if (features.notificationsIncludeTypes) {
|
||||
params.types = NOTIFICATION_TYPES;
|
||||
params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any));
|
||||
} else {
|
||||
params.exclude_types = EXCLUDE_TYPES;
|
||||
}
|
||||
} else {
|
||||
if (features.notificationsIncludeTypes) {
|
||||
|
|
|
@ -43,6 +43,11 @@ const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
|||
const STATUS_REVEAL = 'STATUS_REVEAL';
|
||||
const STATUS_HIDE = 'STATUS_HIDE';
|
||||
|
||||
const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
|
||||
const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
||||
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||
|
||||
const statusExists = (getState: () => RootState, statusId: string) => {
|
||||
return (getState().statuses.get(statusId) || null) !== null;
|
||||
};
|
||||
|
@ -305,6 +310,31 @@ const toggleStatusHidden = (status: Status) => {
|
|||
}
|
||||
};
|
||||
|
||||
const translateStatus = (id: string, targetLanguage?: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: STATUS_TRANSLATE_REQUEST, id });
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${id}/translate`, {
|
||||
target_language: targetLanguage,
|
||||
}).then(response => {
|
||||
dispatch({
|
||||
type: STATUS_TRANSLATE_SUCCESS,
|
||||
id,
|
||||
translation: response.data,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({
|
||||
type: STATUS_TRANSLATE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const undoStatusTranslation = (id: string) => ({
|
||||
type: STATUS_TRANSLATE_UNDO,
|
||||
id,
|
||||
});
|
||||
|
||||
export {
|
||||
STATUS_CREATE_REQUEST,
|
||||
STATUS_CREATE_SUCCESS,
|
||||
|
@ -329,6 +359,10 @@ export {
|
|||
STATUS_UNMUTE_FAIL,
|
||||
STATUS_REVEAL,
|
||||
STATUS_HIDE,
|
||||
STATUS_TRANSLATE_REQUEST,
|
||||
STATUS_TRANSLATE_SUCCESS,
|
||||
STATUS_TRANSLATE_FAIL,
|
||||
STATUS_TRANSLATE_UNDO,
|
||||
createStatus,
|
||||
editStatus,
|
||||
fetchStatus,
|
||||
|
@ -345,4 +379,6 @@ export {
|
|||
hideStatus,
|
||||
revealStatus,
|
||||
toggleStatusHidden,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
};
|
||||
|
|
|
@ -235,6 +235,14 @@ const Account = ({
|
|||
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{actionType === 'muting' && account.mute_expires_at ? (
|
||||
<>
|
||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
||||
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
|
||||
</>
|
||||
) : null}
|
||||
</HStack>
|
||||
|
||||
{withAccountNote && (
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -8,6 +8,7 @@ import { Text, Stack } from 'soapbox/components/ui';
|
|||
import { captureException } from 'soapbox/monitoring';
|
||||
import KVStore from 'soapbox/storage/kv_store';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { unregisterSw } from 'soapbox/utils/sw';
|
||||
|
||||
import SiteLogo from './site-logo';
|
||||
|
||||
|
@ -15,16 +16,6 @@ import type { RootState } from 'soapbox/store';
|
|||
|
||||
const goHome = () => location.href = '/';
|
||||
|
||||
/** Unregister the ServiceWorker */
|
||||
// https://stackoverflow.com/a/49771828/8811886
|
||||
const unregisterSw = async(): Promise<void> => {
|
||||
if (navigator.serviceWorker) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
const unregisterAll = registrations.map(r => r.unregister());
|
||||
await Promise.all(unregisterAll);
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: RootState) => {
|
||||
const { links, logo } = getSoapboxConfig(state);
|
||||
|
||||
|
|
Binary file not shown.
|
@ -116,7 +116,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
collapsable
|
||||
/>
|
||||
|
||||
{(status.media_attachments.size > 0) && (
|
||||
{(status.card || status.media_attachments.size > 0) && (
|
||||
<StatusMedia
|
||||
status={status}
|
||||
muted={compose}
|
||||
|
|
Binary file not shown.
|
@ -9,6 +9,7 @@ import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
|||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { toggleStatusHidden } from 'soapbox/actions/statuses';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import TranslateButton from 'soapbox/components/translate-button';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
@ -370,9 +371,12 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
status={actualStatus}
|
||||
onClick={handleClick}
|
||||
collapsable
|
||||
translatable
|
||||
/>
|
||||
|
||||
{(quote || actualStatus.media_attachments.size > 0) && (
|
||||
<TranslateButton status={actualStatus} />
|
||||
|
||||
{(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && (
|
||||
<Stack space={4}>
|
||||
<StatusMedia
|
||||
status={actualStatus}
|
||||
|
|
|
@ -39,10 +39,11 @@ interface IStatusContent {
|
|||
status: Status,
|
||||
onClick?: () => void,
|
||||
collapsable?: boolean,
|
||||
translatable?: boolean,
|
||||
}
|
||||
|
||||
/** Renders the text content of a status */
|
||||
const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable = false }) => {
|
||||
const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable = false, translatable }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
@ -154,14 +155,14 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
};
|
||||
|
||||
const parsedHtml = useMemo((): string => {
|
||||
const { contentHtml: html } = status;
|
||||
const html = translatable && status.translation ? status.translation.get('content')! : status.contentHtml;
|
||||
|
||||
if (greentext) {
|
||||
return addGreentext(html);
|
||||
} else {
|
||||
return html;
|
||||
}
|
||||
}, [status.contentHtml]);
|
||||
}, [status.contentHtml, status.translation]);
|
||||
|
||||
if (status.content.length === 0) {
|
||||
return null;
|
||||
|
|
|
@ -4,34 +4,22 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
// import { connect } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
// import { openModal } from 'soapbox/actions/modals';
|
||||
// import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import { CardHeader, CardTitle } from './ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
settings: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
});
|
||||
|
||||
interface ISubNavigation {
|
||||
message: String,
|
||||
message: React.ReactNode,
|
||||
/** @deprecated Unused. */
|
||||
settings?: React.ComponentType,
|
||||
}
|
||||
|
||||
const SubNavigation: React.FC<ISubNavigation> = ({ message }) => {
|
||||
const intl = useIntl();
|
||||
// const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
// const ref = useRef(null);
|
||||
|
||||
// const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
// const onOpenSettings = () => {
|
||||
// dispatch(openModal('COMPONENT', { component: Settings }));
|
||||
// };
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
history.push('/');
|
||||
|
@ -40,36 +28,6 @@ const SubNavigation: React.FC<ISubNavigation> = ({ message }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// const handleBackKeyUp = (e) => {
|
||||
// if (e.key === 'Enter') {
|
||||
// handleClick();
|
||||
// }
|
||||
// }
|
||||
|
||||
// const handleOpenSettings = () => {
|
||||
// onOpenSettings();
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleScroll = throttle(() => {
|
||||
// if (this.node) {
|
||||
// const { offsetTop } = this.node;
|
||||
|
||||
// if (offsetTop > 0) {
|
||||
// setScrolled(true);
|
||||
// } else {
|
||||
// setScrolled(false);
|
||||
// }
|
||||
// }
|
||||
// }, 150, { trailing: true });
|
||||
|
||||
// window.addEventListener('scroll', handleScroll);
|
||||
|
||||
// return () => {
|
||||
// window.removeEventListener('scroll', handleScroll);
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<CardHeader
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
|
|
59
app/soapbox/components/translate-button.tsx
Normal file
59
app/soapbox/components/translate-button.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
import { Stack } from './ui';
|
||||
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
interface ITranslateButton {
|
||||
status: Status,
|
||||
}
|
||||
|
||||
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const features = useFeatures();
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const renderTranslate = /* translationEnabled && */ me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||
|
||||
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (status.translation) {
|
||||
dispatch(undoStatusTranslation(status.id));
|
||||
} else {
|
||||
dispatch(translateStatus(status.id, intl.locale));
|
||||
}
|
||||
};
|
||||
|
||||
if (!features.translations || !renderTranslate) return null;
|
||||
|
||||
if (status.translation) {
|
||||
const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
|
||||
const languageName = languageNames.of(status.language!);
|
||||
const provider = status.translation.get('provider');
|
||||
|
||||
return (
|
||||
<Stack className='text-gray-700 dark:text-gray-600 text-sm' alignItems='start'>
|
||||
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||
|
||||
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
|
||||
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||
</button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
|
||||
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TranslateButton;
|
|
@ -40,6 +40,8 @@ interface IHStack {
|
|||
space?: keyof typeof spaces
|
||||
/** Whether to let the flexbox grow. */
|
||||
grow?: boolean
|
||||
/** HTML element to use for container. */
|
||||
element?: keyof JSX.IntrinsicElements,
|
||||
/** Extra CSS styles for the <div> */
|
||||
style?: React.CSSProperties
|
||||
/** Whether to let the flexbox wrap onto multiple lines. */
|
||||
|
@ -48,10 +50,12 @@ interface IHStack {
|
|||
|
||||
/** Horizontal row of child elements. */
|
||||
const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
|
||||
const { space, alignItems, grow, justifyContent, wrap, className, ...filteredProps } = props;
|
||||
const { space, alignItems, justifyContent, className, grow, element = 'div', wrap, ...filteredProps } = props;
|
||||
|
||||
const Elem = element as 'div';
|
||||
|
||||
return (
|
||||
<div
|
||||
<Elem
|
||||
{...filteredProps}
|
||||
ref={ref}
|
||||
className={classNames('flex', {
|
||||
|
|
|
@ -20,29 +20,35 @@ const justifyContentOptions = {
|
|||
};
|
||||
|
||||
const alignItemsOptions = {
|
||||
top: 'items-start',
|
||||
bottom: 'items-end',
|
||||
center: 'items-center',
|
||||
start: 'items-start',
|
||||
};
|
||||
|
||||
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Size of the gap between elements. */
|
||||
space?: keyof typeof spaces
|
||||
/** Horizontal alignment of children. */
|
||||
alignItems?: 'center' | 'start',
|
||||
alignItems?: keyof typeof alignItemsOptions
|
||||
/** Extra class names on the element. */
|
||||
className?: string
|
||||
/** Vertical alignment of children. */
|
||||
justifyContent?: keyof typeof justifyContentOptions
|
||||
/** Extra class names on the <div> element. */
|
||||
className?: string
|
||||
/** Size of the gap between elements. */
|
||||
space?: keyof typeof spaces
|
||||
/** Whether to let the flexbox grow. */
|
||||
grow?: boolean
|
||||
/** HTML element to use for container. */
|
||||
element?: keyof JSX.IntrinsicElements,
|
||||
}
|
||||
|
||||
/** Vertical stack of child elements. */
|
||||
const Stack = React.forwardRef<HTMLDivElement, IStack>((props, ref: React.LegacyRef<HTMLDivElement> | undefined) => {
|
||||
const { space, alignItems, justifyContent, className, grow, ...filteredProps } = props;
|
||||
const { space, alignItems, justifyContent, className, grow, element = 'div', ...filteredProps } = props;
|
||||
|
||||
const Elem = element as 'div';
|
||||
|
||||
return (
|
||||
<div
|
||||
<Elem
|
||||
{...filteredProps}
|
||||
ref={ref}
|
||||
className={classNames('flex flex-col', {
|
||||
|
|
Binary file not shown.
|
@ -1,26 +1,45 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedDate, useIntl } from 'react-intl';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';
|
||||
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { Token } from 'soapbox/reducers/security';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
|
||||
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
|
||||
revokeSessionHeading: { id: 'confirmations.revoke_session.heading', defaultMessage: 'Revoke current session' },
|
||||
revokeSessionMessage: { id: 'confirmations.revoke_session.message', defaultMessage: 'You are about to revoke your current session. You will be signed out.' },
|
||||
revokeSessionConfirm: { id: 'confirmations.revoke_session.confirm', defaultMessage: 'Revoke' },
|
||||
});
|
||||
|
||||
interface IAuthToken {
|
||||
token: Token,
|
||||
isCurrent: boolean,
|
||||
}
|
||||
|
||||
const AuthToken: React.FC<IAuthToken> = ({ token }) => {
|
||||
const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleRevoke = () => {
|
||||
dispatch(revokeOAuthTokenById(token.id));
|
||||
if (isCurrent)
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/alert-triangle.svg'),
|
||||
heading: intl.formatMessage(messages.revokeSessionHeading),
|
||||
message: intl.formatMessage(messages.revokeSessionMessage),
|
||||
confirm: intl.formatMessage(messages.revokeSessionConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(revokeOAuthTokenById(token.id));
|
||||
},
|
||||
}));
|
||||
else {
|
||||
dispatch(revokeOAuthTokenById(token.id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -42,7 +61,7 @@ const AuthToken: React.FC<IAuthToken> = ({ token }) => {
|
|||
</Stack>
|
||||
|
||||
<div className='flex justify-end'>
|
||||
<Button theme='primary' onClick={handleRevoke}>
|
||||
<Button theme={isCurrent ? 'danger' : 'primary'} onClick={handleRevoke}>
|
||||
{intl.formatMessage(messages.revoke)}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -55,6 +74,11 @@ const AuthTokenList: React.FC = () => {
|
|||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
|
||||
const currentTokenId = useAppSelector(state => {
|
||||
const currentToken = state.auth.get('tokens').valueSeq().find((token: ImmutableMap<string, any>) => token.get('me') === state.auth.get('me'));
|
||||
|
||||
return currentToken?.get('id');
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchOAuthTokens());
|
||||
|
@ -63,7 +87,7 @@ const AuthTokenList: React.FC = () => {
|
|||
const body = tokens ? (
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||
{tokens.map((token) => (
|
||||
<AuthToken key={token.id} token={token} />
|
||||
<AuthToken key={token.id} token={token} isCurrent={token.id === currentTokenId} />
|
||||
))}
|
||||
</div>
|
||||
) : <Spinner />;
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -10,8 +10,6 @@ import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
|||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
import ColumnSettings from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||
});
|
||||
|
@ -44,7 +42,10 @@ const CommunityTimeline = () => {
|
|||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
||||
<div className='px-4 sm:p-0'>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||
</div>
|
||||
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
|
|
|
@ -132,7 +132,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
setComposeFocused(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleSubmit = (e?: React.FormEvent<Element>) => {
|
||||
if (text !== autosuggestTextareaRef.current?.textarea?.value) {
|
||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||
// Update the state to match the current text
|
||||
|
@ -142,6 +142,10 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
// Submit disabled:
|
||||
const fulltext = [spoilerText, countableText(text)].join('');
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
return;
|
||||
}
|
||||
|
@ -261,7 +265,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
}
|
||||
|
||||
return (
|
||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick}>
|
||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||
{scheduledStatusCount > 0 && (
|
||||
<Warning
|
||||
message={(
|
||||
|
@ -339,7 +343,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Button theme='primary' text={publishText} onClick={handleSubmit} disabled={disabledButton} />
|
||||
<Button type='submit' theme='primary' text={publishText} disabled={disabledButton} />
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
|
|
|
@ -168,7 +168,7 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
|
|||
|
||||
<Divider />
|
||||
|
||||
<button onClick={handleToggleMultiple} className='text-left'>
|
||||
<button type='button' onClick={handleToggleMultiple} className='text-left'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Stack>
|
||||
<Text weight='medium'>
|
||||
|
@ -197,7 +197,7 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
|
|||
|
||||
{/* Remove Poll */}
|
||||
<div className='text-center'>
|
||||
<button className='text-danger-500' onClick={onRemovePoll}>
|
||||
<button type='button' className='text-danger-500' onClick={onRemovePoll}>
|
||||
{intl.formatMessage(messages.removePoll)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -68,7 +68,7 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
|||
/>
|
||||
|
||||
<div className='text-center'>
|
||||
<button className='text-danger-500' onClick={handleRemove}>
|
||||
<button type='button' className='text-danger-500' onClick={handleRemove}>
|
||||
{intl.formatMessage(messages.remove)}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -77,4 +77,4 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
|||
);
|
||||
});
|
||||
|
||||
export default SpoilerInput;
|
||||
export default SpoilerInput;
|
||||
|
|
24
app/soapbox/features/developers/components/indicator.tsx
Normal file
24
app/soapbox/features/developers/components/indicator.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IIndicator {
|
||||
state?: 'active' | 'pending' | 'error' | 'inactive',
|
||||
size?: 'sm',
|
||||
}
|
||||
|
||||
/** Indicator dot component. */
|
||||
const Indicator: React.FC<IIndicator> = ({ state = 'inactive', size = 'sm' }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('rounded-full outline-double', {
|
||||
'w-1.5 h-1.5 shadow-sm': size === 'sm',
|
||||
'bg-green-500 outline-green-400': state === 'active',
|
||||
'bg-yellow-500 outline-yellow-400': state === 'pending',
|
||||
'bg-red-500 outline-red-400': state === 'error',
|
||||
'bg-neutral-500 outline-neutral-400': state === 'inactive',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Indicator;
|
|
@ -89,6 +89,14 @@ const Developers: React.FC = () => {
|
|||
</Text>
|
||||
</DashWidget>
|
||||
|
||||
<DashWidget to='/developers/sw'>
|
||||
<SvgIcon src={require('@tabler/icons/script.svg')} className='text-gray-700 dark:text-gray-600' />
|
||||
|
||||
<Text>
|
||||
<FormattedMessage id='developers.navigation.service_worker_label' defaultMessage='Service Worker' />
|
||||
</Text>
|
||||
</DashWidget>
|
||||
|
||||
<DashWidget onClick={leaveDevelopers}>
|
||||
<SvgIcon src={require('@tabler/icons/logout.svg')} className='text-gray-700 dark:text-gray-600' />
|
||||
|
||||
|
|
140
app/soapbox/features/developers/service-worker-info.tsx
Normal file
140
app/soapbox/features/developers/service-worker-info.tsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { HStack, Text, Column, FormActions, Button, Stack, Icon } from 'soapbox/components/ui';
|
||||
import { unregisterSw } from 'soapbox/utils/sw';
|
||||
|
||||
import Indicator from './components/indicator';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.developers.service_worker', defaultMessage: 'Service Worker' },
|
||||
status: { id: 'sw.status', defaultMessage: 'Status' },
|
||||
url: { id: 'sw.url', defaultMessage: 'Script URL' },
|
||||
});
|
||||
|
||||
/** Hook that returns the active ServiceWorker registration. */
|
||||
const useRegistration = () => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [registration, setRegistration] = useState<ServiceWorkerRegistration>();
|
||||
|
||||
const isSupported = 'serviceWorker' in navigator;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSupported) {
|
||||
navigator.serviceWorker.getRegistration()
|
||||
.then(r => {
|
||||
setRegistration(r);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
registration,
|
||||
};
|
||||
};
|
||||
|
||||
interface IServiceWorkerInfo {
|
||||
}
|
||||
|
||||
/** Mini ServiceWorker debugging component. */
|
||||
const ServiceWorkerInfo: React.FC<IServiceWorkerInfo> = () => {
|
||||
const intl = useIntl();
|
||||
const { isLoading, registration } = useRegistration();
|
||||
|
||||
const url = registration?.active?.scriptURL;
|
||||
|
||||
const getState = () => {
|
||||
if (registration?.waiting) {
|
||||
return 'pending';
|
||||
} else if (registration?.active) {
|
||||
return 'active';
|
||||
} else {
|
||||
return 'inactive';
|
||||
}
|
||||
};
|
||||
|
||||
const getMessage = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='sw.state.loading'
|
||||
defaultMessage='Loading…'
|
||||
/>
|
||||
);
|
||||
} else if (!isLoading && !registration) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='sw.state.unavailable'
|
||||
defaultMessage='Unavailable'
|
||||
/>
|
||||
);
|
||||
} else if (registration?.waiting) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='sw.state.waiting'
|
||||
defaultMessage='Waiting'
|
||||
/>
|
||||
);
|
||||
} else if (registration?.active) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='sw.state.active'
|
||||
defaultMessage='Active'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='sw.state.unknown'
|
||||
defaultMessage='Unknown'
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestart = async() => {
|
||||
await unregisterSw();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)} backHref='/developers'>
|
||||
<Stack space={4}>
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.status)}>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Indicator state={getState()} />
|
||||
<Text size='md' theme='muted'>{getMessage()}</Text>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
|
||||
{url && (
|
||||
<ListItem label={intl.formatMessage(messages.url)}>
|
||||
<a href={url} target='_blank' className='flex space-x-1 items-center truncate'>
|
||||
<span className='truncate'>{url}</span>
|
||||
<Icon
|
||||
className='w-4 h-4'
|
||||
src={require('@tabler/icons/external-link.svg')}
|
||||
/>
|
||||
</a>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<FormActions>
|
||||
<Button theme='tertiary' type='button' onClick={handleRestart}>
|
||||
<FormattedMessage id='sw.restart' defaultMessage='Restart' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceWorkerInfo;
|
Binary file not shown.
|
@ -3,10 +3,10 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import { connectHashtagStream } from 'soapbox/actions/streaming';
|
||||
import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines';
|
||||
import ColumnHeader from 'soapbox/components/column_header';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import Timeline from 'soapbox/features/ui/components/timeline';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import type { Tag as TagEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -27,7 +27,6 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
const tags = params?.tags || { any: [], all: [], none: [] };
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const hasUnread = useAppSelector<boolean>(state => (state.timelines.getIn([`hashtag:${id}`, 'unread']) as number) > 0);
|
||||
const disconnects = useRef<(() => void)[]>([]);
|
||||
|
||||
// Mastodon supports displaying results from multiple hashtags.
|
||||
|
@ -100,7 +99,10 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
|
||||
return (
|
||||
<Column label={`#${id}`} transparent withHeader={false}>
|
||||
<ColumnHeader active={hasUnread} title={title()} />
|
||||
<div className='px-4 pt-4 sm:p-0'>
|
||||
<SubNavigation message={title()} />
|
||||
</div>
|
||||
|
||||
<Timeline
|
||||
scrollKey='hashtag_timeline'
|
||||
timelineId={`hashtag:${id}`}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -14,8 +14,6 @@ import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
|
|||
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
import ColumnSettings from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.public', defaultMessage: 'Fediverse timeline' },
|
||||
dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' },
|
||||
|
@ -65,8 +63,12 @@ const CommunityTimeline = () => {
|
|||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
||||
<div className='px-4 sm:p-0'>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||
</div>
|
||||
|
||||
<PinnedHostsPicker />
|
||||
|
||||
{showExplanationBox && <div className='mb-4'>
|
||||
<Accordion
|
||||
headline={<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -7,6 +7,7 @@ import StatusMedia from 'soapbox/components/status-media';
|
|||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||
import StatusContent from 'soapbox/components/status_content';
|
||||
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
||||
import TranslateButton from 'soapbox/components/translate-button';
|
||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
||||
|
@ -101,9 +102,11 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
)}
|
||||
|
||||
<Stack space={4}>
|
||||
<StatusContent status={actualStatus} />
|
||||
<StatusContent status={actualStatus} translatable />
|
||||
|
||||
{(quote || actualStatus.media_attachments.size > 0) && (
|
||||
<TranslateButton status={actualStatus} />
|
||||
|
||||
{(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && (
|
||||
<Stack space={4}>
|
||||
<StatusMedia
|
||||
status={actualStatus}
|
||||
|
|
Binary file not shown.
|
@ -4,9 +4,10 @@ import Toggle from 'react-toggle';
|
|||
|
||||
import { muteAccount } from 'soapbox/actions/accounts';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { toggleHideNotifications } from 'soapbox/actions/mutes';
|
||||
import { toggleHideNotifications, changeMuteDuration } from 'soapbox/actions/mutes';
|
||||
import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import DurationSelector from 'soapbox/features/compose/components/polls/duration-selector';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
@ -16,12 +17,14 @@ const MuteModal = () => {
|
|||
|
||||
const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!));
|
||||
const notifications = useAppSelector((state) => state.mutes.new.notifications);
|
||||
const duration = useAppSelector((state) => state.mutes.new.duration);
|
||||
const mutesDuration = useFeatures().mutesDuration;
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(closeModal());
|
||||
dispatch(muteAccount(account.id, notifications));
|
||||
dispatch(muteAccount(account.id, notifications, duration));
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
|
@ -32,6 +35,12 @@ const MuteModal = () => {
|
|||
dispatch(toggleHideNotifications());
|
||||
};
|
||||
|
||||
const handleChangeMuteDuration = (expiresIn: number): void => {
|
||||
dispatch(changeMuteDuration(expiresIn));
|
||||
};
|
||||
|
||||
const toggleAutoExpire = () => handleChangeMuteDuration(duration ? 0 : 2 * 60 * 60 * 24);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
|
@ -69,6 +78,32 @@ const MuteModal = () => {
|
|||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
|
||||
{mutesDuration && (
|
||||
<>
|
||||
<label>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Text tag='span'>
|
||||
<FormattedMessage id='mute_modal.auto_expire' defaultMessage='Automatically expire mute?' />
|
||||
</Text>
|
||||
|
||||
<Toggle
|
||||
checked={duration !== 0}
|
||||
onChange={toggleAutoExpire}
|
||||
icons={false}
|
||||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
|
||||
{duration !== 0 && (
|
||||
<Stack space={2}>
|
||||
<Text weight='medium'><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </Text>
|
||||
|
||||
<DurationSelector onDurationChange={handleChangeMuteDuration} />
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -39,6 +39,8 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
|||
const features = useFeatures();
|
||||
const intl = useIntl();
|
||||
|
||||
useAppSelector((state) => console.log(state.auth.toJS()));
|
||||
|
||||
const authUsers = useAppSelector((state) => state.auth.get('users'));
|
||||
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.get('id'))));
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { useIntl, MessageDescriptor } from 'react-intl';
|
||||
import { NotificationStack, NotificationObject, StyleFactoryFn } from 'react-notification';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { dismissAlert } from 'soapbox/actions/alerts';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { NotificationStack, NotificationObject, StyleFactoryFn } from 'soapbox/react-notification';
|
||||
|
||||
import type { Alert } from 'soapbox/reducers/alerts';
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ import {
|
|||
TestTimeline,
|
||||
LogoutPage,
|
||||
AuthTokenList,
|
||||
ServiceWorkerInfo,
|
||||
} from './util/async-components';
|
||||
import { WrappedRoute } from './util/react_router_helpers';
|
||||
|
||||
|
@ -295,6 +296,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
<WrappedRoute path='/developers/apps/create' developerOnly page={DefaultPage} component={CreateApp} content={children} />
|
||||
<WrappedRoute path='/developers/settings_store' developerOnly page={DefaultPage} component={SettingsStore} content={children} />
|
||||
<WrappedRoute path='/developers/timeline' developerOnly page={DefaultPage} component={TestTimeline} content={children} />
|
||||
<WrappedRoute path='/developers/sw' developerOnly page={DefaultPage} component={ServiceWorkerInfo} content={children} />
|
||||
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
||||
<WrappedRoute path='/error/network' developerOnly page={EmptyPage} component={() => new Promise((_resolve, reject) => reject())} content={children} />
|
||||
<WrappedRoute path='/error' developerOnly page={EmptyPage} component={IntentionalError} content={children} />
|
||||
|
|
|
@ -470,6 +470,10 @@ export function TestTimeline() {
|
|||
return import(/* webpackChunkName: "features/test_timeline" */'../../test_timeline');
|
||||
}
|
||||
|
||||
export function ServiceWorkerInfo() {
|
||||
return import(/* webpackChunkName: "features/developers" */'../../developers/service-worker-info');
|
||||
}
|
||||
|
||||
export function DatePicker() {
|
||||
return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker');
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1211,8 +1211,11 @@
|
|||
"status.show_less_all": "Zwiń wszystkie",
|
||||
"status.show_more": "Rozwiń",
|
||||
"status.show_more_all": "Rozwiń wszystkie",
|
||||
"status.show_original": "Pokaż oryginalny wpis",
|
||||
"status.title": "Wpis",
|
||||
"status.title_direct": "Wiadomość bezpośrednia",
|
||||
"status.translated_from_with": "Przetłumaczono z {lang} z użyciem {provider}",
|
||||
"status.translate": "Przetłumacz wpis",
|
||||
"status.unbookmark": "Usuń z zakładek",
|
||||
"status.unbookmarked": "Usunięto z zakładek.",
|
||||
"status.unmute_conversation": "Cofnij wyciszenie konwersacji",
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -44,6 +44,7 @@ export const AccountRecord = ImmutableRecord({
|
|||
location: '',
|
||||
locked: false,
|
||||
moved: null as EmbeddedEntity<any>,
|
||||
mute_expires_at: null as string | null,
|
||||
note: '',
|
||||
pleroma: ImmutableMap<string, any>(),
|
||||
source: ImmutableMap<string, any>(),
|
||||
|
|
|
@ -63,6 +63,7 @@ export const StatusRecord = ImmutableRecord({
|
|||
hidden: false,
|
||||
search_index: '',
|
||||
spoilerHtml: '',
|
||||
translation: null as ImmutableMap<string, string> | null,
|
||||
});
|
||||
|
||||
const normalizeAttachments = (status: ImmutableMap<string, any>) => {
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
* @module soapbox/precheck
|
||||
*/
|
||||
|
||||
/** Whether pre-rendered data exists in Mastodon's format. */
|
||||
/** Whether pre-rendered data exists in Pleroma's format. */
|
||||
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
|
||||
|
||||
/** Whether pre-rendered data exists in Pleroma's format. */
|
||||
/** Whether pre-rendered data exists in Mastodon's format. */
|
||||
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
|
||||
|
||||
/** Whether initial data was loaded into the page by server-side-rendering (SSR). */
|
||||
|
|
BIN
app/soapbox/react-notification/defaultPropTypes.js
vendored
BIN
app/soapbox/react-notification/defaultPropTypes.js
vendored
Binary file not shown.
88
app/soapbox/react-notification/index.d.ts
vendored
88
app/soapbox/react-notification/index.d.ts
vendored
|
@ -1,88 +0,0 @@
|
|||
declare module 'soapbox/react-notification' {
|
||||
import { Component, ReactElement } from 'react';
|
||||
|
||||
interface StyleFactoryFn {
|
||||
(index: number, style: object | void, notification: NotificationProps): object;
|
||||
}
|
||||
|
||||
interface OnClickNotificationProps {
|
||||
/**
|
||||
* Callback function to run when the action is clicked.
|
||||
* @param notification Notification currently being clicked
|
||||
* @param deactivate Function that can be called to set the notification to inactive.
|
||||
* Used to activate notification exit animation on click.
|
||||
*/
|
||||
onClick?(notification: NotificationProps, deactivate: () => void): void;
|
||||
}
|
||||
|
||||
interface NotificationProps extends OnClickNotificationProps {
|
||||
/** The name of the action, e.g., "close" or "undo". */
|
||||
action?: string;
|
||||
/** Custom action styles. */
|
||||
actionStyle?: object;
|
||||
/** Custom snackbar styles when the bar is active. */
|
||||
activeBarStyle?: object;
|
||||
/**
|
||||
* Custom class to apply to the top-level component when active.
|
||||
* @default 'notification-bar-active'
|
||||
*/
|
||||
activeClassName?: string;
|
||||
/** Custom snackbar styles. */
|
||||
barStyle?: object;
|
||||
/** Custom class to apply to the top-level component. */
|
||||
className?: string;
|
||||
/**
|
||||
* Timeout for onDismiss event.
|
||||
* @default 2000
|
||||
*/
|
||||
dismissAfter?: boolean | number;
|
||||
/**
|
||||
* If true, the notification is visible.
|
||||
* @default false
|
||||
*/
|
||||
isActive?: boolean;
|
||||
/** The message or component for the notification. */
|
||||
message: string | ReactElement<NotificationProps>;
|
||||
/** Setting this prop to `false` will disable all inline styles. */
|
||||
style?: boolean;
|
||||
/** The title for the notification. */
|
||||
title?: string | ReactElement<any>;
|
||||
/** Custom title styles. */
|
||||
titleStyle?: object;
|
||||
|
||||
/**
|
||||
* Callback function to run when dismissAfter timer runs out
|
||||
* @param notification Notification currently being dismissed.
|
||||
*/
|
||||
onDismiss?(notification: NotificationProps): void;
|
||||
}
|
||||
|
||||
interface NotificationStackProps extends OnClickNotificationProps {
|
||||
/** Create the style of the actions. */
|
||||
actionStyleFactory?: StyleFactoryFn;
|
||||
/** Create the style of the active notification. */
|
||||
activeBarStyleFactory?: StyleFactoryFn;
|
||||
/** Create the style of the notification. */
|
||||
barStyleFactory?: StyleFactoryFn;
|
||||
/**
|
||||
* If false, notification dismiss timers start immediately.
|
||||
* @default true
|
||||
*/
|
||||
dismissInOrder?: boolean;
|
||||
/** Array of notifications to render. */
|
||||
notifications: NotificationObject[];
|
||||
/**
|
||||
* Callback function to run when dismissAfter timer runs out
|
||||
* @param notification Notification currently being dismissed.
|
||||
*/
|
||||
onDismiss?(notification: NotificationObject): void;
|
||||
}
|
||||
|
||||
export interface NotificationObject extends NotificationProps {
|
||||
key: number | string;
|
||||
}
|
||||
|
||||
export class Notification extends Component<NotificationProps, {}> {}
|
||||
|
||||
export class NotificationStack extends Component<NotificationStackProps, {}> {}
|
||||
}
|
BIN
app/soapbox/react-notification/index.js
vendored
BIN
app/soapbox/react-notification/index.js
vendored
Binary file not shown.
BIN
app/soapbox/react-notification/notification.js
vendored
BIN
app/soapbox/react-notification/notification.js
vendored
Binary file not shown.
BIN
app/soapbox/react-notification/notificationStack.js
vendored
BIN
app/soapbox/react-notification/notificationStack.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -1,16 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import reducer from '../group_editor';
|
||||
|
||||
describe('group_editor reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap({
|
||||
groupId: null,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: '',
|
||||
description: '',
|
||||
coverImage: null,
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import reducer from '../group_lists';
|
||||
|
||||
describe('group_lists reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap({
|
||||
featured: ImmutableList(),
|
||||
member: ImmutableList(),
|
||||
admin: ImmutableList(),
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import reducer from '../group_relationships';
|
||||
|
||||
describe('group_relationships reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
|
||||
});
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import reducer from '../groups';
|
||||
|
||||
describe('groups reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
|
||||
});
|
||||
});
|
Binary file not shown.
|
@ -14,6 +14,7 @@ describe('mutes reducer', () => {
|
|||
isSubmitting: false,
|
||||
accountId: null,
|
||||
notifications: true,
|
||||
duration: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -24,6 +25,7 @@ describe('mutes reducer', () => {
|
|||
isSubmitting: false,
|
||||
accountId: null,
|
||||
notifications: true,
|
||||
duration: 0,
|
||||
})(),
|
||||
})();
|
||||
const action = {
|
||||
|
@ -35,6 +37,7 @@ describe('mutes reducer', () => {
|
|||
isSubmitting: false,
|
||||
accountId: 'account1',
|
||||
notifications: true,
|
||||
duration: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -45,6 +48,7 @@ describe('mutes reducer', () => {
|
|||
isSubmitting: false,
|
||||
accountId: null,
|
||||
notifications: true,
|
||||
duration: 0,
|
||||
})(),
|
||||
})();
|
||||
const action = {
|
||||
|
@ -55,6 +59,7 @@ describe('mutes reducer', () => {
|
|||
isSubmitting: false,
|
||||
accountId: null,
|
||||
notifications: false,
|
||||
duration: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -14,8 +14,6 @@ describe('user_lists reducer', () => {
|
|||
blocks: { next: null, items: ImmutableOrderedSet(), isLoading: false },
|
||||
mutes: { next: null, items: ImmutableOrderedSet(), isLoading: false },
|
||||
directory: { next: null, items: ImmutableOrderedSet(), isLoading: true },
|
||||
groups: {},
|
||||
groups_removed_accounts: {},
|
||||
pinned: {},
|
||||
birthday_reminders: {},
|
||||
familiar_followers: {},
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -25,10 +25,6 @@ import custom_emojis from './custom_emojis';
|
|||
import domain_lists from './domain_lists';
|
||||
import dropdown_menu from './dropdown_menu';
|
||||
import filters from './filters';
|
||||
import group_editor from './group_editor';
|
||||
import group_lists from './group_lists';
|
||||
import group_relationships from './group_relationships';
|
||||
import groups from './groups';
|
||||
import history from './history';
|
||||
import instance from './instance';
|
||||
import listAdder from './list_adder';
|
||||
|
@ -95,10 +91,6 @@ const reducers = {
|
|||
suggestions,
|
||||
polls,
|
||||
trends,
|
||||
groups,
|
||||
group_relationships,
|
||||
group_lists,
|
||||
group_editor,
|
||||
sidebar,
|
||||
patron,
|
||||
soapbox,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Record as ImmutableRecord } from 'immutable';
|
|||
import {
|
||||
MUTES_INIT_MODAL,
|
||||
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||
MUTES_CHANGE_DURATION,
|
||||
} from '../actions/mutes';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
@ -11,6 +12,7 @@ const NewMuteRecord = ImmutableRecord({
|
|||
isSubmitting: false,
|
||||
accountId: null,
|
||||
notifications: true,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
|
@ -29,6 +31,8 @@ export default function mutes(state: State = ReducerRecord(), action: AnyAction)
|
|||
});
|
||||
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
|
||||
return state.updateIn(['new', 'notifications'], (old) => !old);
|
||||
case MUTES_CHANGE_DURATION:
|
||||
return state.setIn(['new', 'duration'], action.duration);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { normalizeStatus } from 'soapbox/normalizers';
|
||||
|
@ -30,6 +30,8 @@ import {
|
|||
STATUS_HIDE,
|
||||
STATUS_DELETE_REQUEST,
|
||||
STATUS_DELETE_FAIL,
|
||||
STATUS_TRANSLATE_SUCCESS,
|
||||
STATUS_TRANSLATE_UNDO,
|
||||
} from '../actions/statuses';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
|
||||
|
@ -255,6 +257,10 @@ export default function statuses(state = initialState, action: AnyAction): State
|
|||
return decrementReplyCount(state, action.params);
|
||||
case STATUS_DELETE_FAIL:
|
||||
return incrementReplyCount(state, action.params);
|
||||
case STATUS_TRANSLATE_SUCCESS:
|
||||
return state.setIn([action.id, 'translation'], fromJS(action.translation));
|
||||
case STATUS_TRANSLATE_UNDO:
|
||||
return state.deleteIn([action.id, 'translation']);
|
||||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.references);
|
||||
default:
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
ACCOUNT_MUTE_SUCCESS,
|
||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||
} from '../actions/accounts';
|
||||
import { GROUP_REMOVE_STATUS_SUCCESS } from '../actions/groups';
|
||||
import {
|
||||
STATUS_CREATE_REQUEST,
|
||||
STATUS_CREATE_SUCCESS,
|
||||
|
@ -210,10 +209,6 @@ const filterTimelines = (state: State, relationship: APIEntity, statuses: Immuta
|
|||
});
|
||||
};
|
||||
|
||||
const removeStatusFromGroup = (state: State, groupId: string, statusId: string) => {
|
||||
return state.updateIn([`group:${groupId}`, 'items'], ImmutableOrderedSet(), ids => (ids as ImmutableOrderedSet<string>).delete(statusId));
|
||||
};
|
||||
|
||||
const timelineDequeue = (state: State, timelineId: string) => {
|
||||
const top = state.getIn([timelineId, 'top']);
|
||||
|
||||
|
@ -348,8 +343,6 @@ export default function timelines(state: State = initialState, action: AnyAction
|
|||
return timelineConnect(state, action.timeline);
|
||||
case TIMELINE_DISCONNECT:
|
||||
return timelineDisconnect(state, action.timeline);
|
||||
case GROUP_REMOVE_STATUS_SUCCESS:
|
||||
return removeStatusFromGroup(state, action.groupId, action.id);
|
||||
case TIMELINE_REPLACE:
|
||||
return state
|
||||
.update('home', TimelineRecord(), timeline => timeline.withMutations(timeline => {
|
||||
|
|
|
@ -32,13 +32,6 @@ import {
|
|||
import {
|
||||
FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
|
||||
} from '../actions/familiar_followers';
|
||||
import {
|
||||
GROUP_MEMBERS_FETCH_SUCCESS,
|
||||
GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||
GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
|
||||
GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
|
||||
GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
|
||||
} from '../actions/groups';
|
||||
import {
|
||||
REBLOGS_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
|
@ -82,8 +75,6 @@ export const ReducerRecord = ImmutableRecord({
|
|||
blocks: ListRecord(),
|
||||
mutes: ListRecord(),
|
||||
directory: ListRecord({ isLoading: true }),
|
||||
groups: ImmutableMap<string, List>(),
|
||||
groups_removed_accounts: ImmutableMap<string, List>(),
|
||||
pinned: ImmutableMap<string, List>(),
|
||||
birthday_reminders: ImmutableMap<string, List>(),
|
||||
familiar_followers: ImmutableMap<string, List>(),
|
||||
|
@ -94,7 +85,7 @@ export type List = ReturnType<typeof ListRecord>;
|
|||
type Reaction = ReturnType<typeof ReactionRecord>;
|
||||
type ReactionList = ReturnType<typeof ReactionListRecord>;
|
||||
type Items = ImmutableOrderedSet<string>;
|
||||
type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'reactions' | 'groups' | 'groups_removed_accounts' | 'pinned' | 'birthday_reminders' | 'familiar_followers', string];
|
||||
type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers', string];
|
||||
type ListPath = ['follow_requests' | 'blocks' | 'mutes' | 'directory'];
|
||||
|
||||
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next?: string | null) => {
|
||||
|
@ -170,16 +161,6 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) {
|
|||
case DIRECTORY_FETCH_FAIL:
|
||||
case DIRECTORY_EXPAND_FAIL:
|
||||
return state.setIn(['directory', 'isLoading'], false);
|
||||
case GROUP_MEMBERS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['groups', action.id], action.accounts, action.next);
|
||||
case GROUP_MEMBERS_EXPAND_SUCCESS:
|
||||
return appendToList(state, ['groups', action.id], action.accounts, action.next);
|
||||
case GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['groups_removed_accounts', action.id], action.accounts, action.next);
|
||||
case GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS:
|
||||
return appendToList(state, ['groups_removed_accounts', action.id], action.accounts, action.next);
|
||||
case GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS:
|
||||
return removeFromList(state, ['groups_removed_accounts', action.groupId], action.id);
|
||||
case PINNED_ACCOUNTS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['pinned', action.id], action.accounts, action.next);
|
||||
case BIRTHDAY_REMINDERS_FETCH_SUCCESS:
|
||||
|
|
|
@ -424,6 +424,15 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
*/
|
||||
muteStrangers: v.software === PLEROMA,
|
||||
|
||||
/**
|
||||
* Ability to specify how long the account mute should last.
|
||||
* @see PUT /api/v1/accounts/:id/mute
|
||||
*/
|
||||
mutesDuration: any([
|
||||
v.software === PLEROMA && gte(v.version, '2.3.0'),
|
||||
v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
|
||||
]),
|
||||
|
||||
/**
|
||||
* Add private notes to accounts.
|
||||
* @see POST /api/v1/accounts/:id/note
|
||||
|
@ -639,6 +648,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
features.includes('v2_suggestions'),
|
||||
]),
|
||||
|
||||
/**
|
||||
* Can translate statuses.
|
||||
* @see POST /api/v1/statuses/:id/translate
|
||||
*/
|
||||
translations: features.includes('translation'),
|
||||
|
||||
/**
|
||||
* Trending statuses.
|
||||
* @see GET /api/v1/trends/statuses
|
||||
|
|
|
@ -14,6 +14,12 @@ const NOTIFICATION_TYPES = [
|
|||
'update',
|
||||
] as const;
|
||||
|
||||
/** Notification types to exclude from the "All" filter by default. */
|
||||
const EXCLUDE_TYPES = [
|
||||
'pleroma:chat_mention',
|
||||
'chat', // TruthSocial
|
||||
] as const;
|
||||
|
||||
type NotificationType = typeof NOTIFICATION_TYPES[number];
|
||||
|
||||
/** Ensure the Notification is a valid, known type. */
|
||||
|
@ -21,6 +27,7 @@ const validType = (type: string): type is NotificationType => NOTIFICATION_TYPES
|
|||
|
||||
export {
|
||||
NOTIFICATION_TYPES,
|
||||
EXCLUDE_TYPES,
|
||||
NotificationType,
|
||||
validType,
|
||||
};
|
||||
|
|
15
app/soapbox/utils/sw.ts
Normal file
15
app/soapbox/utils/sw.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/** Unregister the ServiceWorker */
|
||||
// https://stackoverflow.com/a/49771828/8811886
|
||||
const unregisterSw = async(): Promise<void> => {
|
||||
if (navigator.serviceWorker) {
|
||||
// FIXME: this only works if using a single tab.
|
||||
// Send a message to sw.js instead to refresh all tabs.
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
const unregisterAll = registrations.map(r => r.unregister());
|
||||
await Promise.all(unregisterAll);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
unregisterSw,
|
||||
};
|
|
@ -102,7 +102,7 @@
|
|||
}
|
||||
|
||||
.status-card {
|
||||
@apply flex text-sm border border-solid border-gray-200 dark:border-gray-800 rounded-lg text-gray-800 dark:text-gray-200 mt-3 min-h-[150px] no-underline overflow-hidden;
|
||||
@apply flex text-sm border border-solid border-gray-200 dark:border-gray-800 rounded-lg text-gray-800 dark:text-gray-200 min-h-[150px] no-underline overflow-hidden;
|
||||
}
|
||||
|
||||
a.status-card {
|
||||
|
|
BIN
babel.config.js
BIN
babel.config.js
Binary file not shown.
|
@ -39,7 +39,6 @@
|
|||
"dependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.17.12",
|
||||
"@babel/plugin-proposal-decorators": "^7.18.2",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.18.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.16.7",
|
||||
|
@ -67,7 +66,7 @@
|
|||
"@sentry/browser": "^7.11.1",
|
||||
"@sentry/react": "^7.11.1",
|
||||
"@sentry/tracing": "^7.11.1",
|
||||
"@tabler/icons": "^1.73.0",
|
||||
"@tabler/icons": "^1.109.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@tanstack/react-query": "^4.0.10",
|
||||
|
@ -167,6 +166,7 @@
|
|||
"react-inlinesvg": "^3.0.0",
|
||||
"react-intl": "^5.0.0",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-notification": "^6.8.5",
|
||||
"react-otp-input": "^2.4.0",
|
||||
"react-overlays": "^0.9.0",
|
||||
"react-popper": "^2.3.0",
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": [ "./types", "./node_modules/@types"]
|
||||
},
|
||||
|
|
39
yarn.lock
39
yarn.lock
|
@ -538,18 +538,6 @@
|
|||
"@babel/helper-plugin-utils" "^7.17.12"
|
||||
"@babel/plugin-syntax-class-static-block" "^7.14.5"
|
||||
|
||||
"@babel/plugin-proposal-decorators@^7.18.2":
|
||||
version "7.18.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.2.tgz#dbe4086d2d42db489399783c3aa9272e9700afd4"
|
||||
integrity sha512-kbDISufFOxeczi0v4NQP3p5kIeW6izn/6klfWBrIIdGZZe4UpHR+QU03FAoWjGGd9SUXAwbw2pup1kaL4OQsJQ==
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin" "^7.18.0"
|
||||
"@babel/helper-plugin-utils" "^7.17.12"
|
||||
"@babel/helper-replace-supers" "^7.18.2"
|
||||
"@babel/helper-split-export-declaration" "^7.16.7"
|
||||
"@babel/plugin-syntax-decorators" "^7.17.12"
|
||||
charcodes "^0.2.0"
|
||||
|
||||
"@babel/plugin-proposal-dynamic-import@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2"
|
||||
|
@ -688,13 +676,6 @@
|
|||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
|
||||
"@babel/plugin-syntax-decorators@^7.17.12":
|
||||
version "7.17.12"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.12.tgz#02e8f678602f0af8222235271efea945cfdb018a"
|
||||
integrity sha512-D1Hz0qtGTza8K2xGyEdVNCYLdVHukAcbQr4K3/s6r/esadyEriZovpJimQOpu8ju4/jV8dW/1xdaE0UpDroidw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.17.12"
|
||||
|
||||
"@babel/plugin-syntax-dynamic-import@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
|
||||
|
@ -2290,10 +2271,10 @@
|
|||
remark "^13.0.0"
|
||||
unist-util-find-all-after "^3.0.2"
|
||||
|
||||
"@tabler/icons@^1.73.0":
|
||||
version "1.73.0"
|
||||
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.73.0.tgz#26d81858baf41be939504e1f9b4b32835eda6fdb"
|
||||
integrity sha512-MhAHFzVj79ZWlAIRD++7Mk55PZsdlEdkfkjO3DD257mqj8iJZQRAQtkx2UFJXVs2mMrcOUu1qtj4rlVC8BfnKA==
|
||||
"@tabler/icons@^1.109.0":
|
||||
version "1.109.0"
|
||||
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.109.0.tgz#11626c3fc097f2f70c4c197e4b9909fb05380752"
|
||||
integrity sha512-B0YetE4pB6HY2Wa57v/LJ3NgkJzKYPze4U0DurIqPoKSptatKv2ga76FZSkO6EUpkYfHMtGPM6QjpJljfuCmAQ==
|
||||
|
||||
"@tailwindcss/forms@^0.5.3":
|
||||
version "0.5.3"
|
||||
|
@ -4196,11 +4177,6 @@ character-reference-invalid@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
|
||||
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
|
||||
|
||||
charcodes@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4"
|
||||
integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==
|
||||
|
||||
cheerio-select@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823"
|
||||
|
@ -9946,6 +9922,13 @@ react-motion@^0.5.2:
|
|||
prop-types "^15.5.8"
|
||||
raf "^3.1.0"
|
||||
|
||||
react-notification@^6.8.5:
|
||||
version "6.8.5"
|
||||
resolved "https://registry.yarnpkg.com/react-notification/-/react-notification-6.8.5.tgz#7ea90a633bb2a280d899e30c93cf372265cce4f0"
|
||||
integrity sha512-3pJPhSsWNYizpyeMeWuC+jVthqE9WKqQ6rHq2naiiP4fLGN4irwL2Xp2Q8Qn7agW/e4BIDxarab6fJOUp1cKUw==
|
||||
dependencies:
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-onclickoutside@^6.12.0:
|
||||
version "6.12.1"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz#92dddd28f55e483a1838c5c2930e051168c1e96b"
|
||||
|
|
Loading…
Reference in a new issue