kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge remote-tracking branch 'origin/develop' into remove-at-decorators
commit
6855decf88
|
@ -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,
|
||||
};
|
|
@ -1,89 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
renderItem(account, size, index) {
|
||||
|
||||
let width = 50;
|
||||
let height = 100;
|
||||
let top = 'auto';
|
||||
let left = 'auto';
|
||||
let bottom = 'auto';
|
||||
let right = 'auto';
|
||||
|
||||
if (size === 1) {
|
||||
width = 100;
|
||||
}
|
||||
|
||||
if (size === 4 || (size === 3 && index > 0)) {
|
||||
height = 50;
|
||||
}
|
||||
|
||||
if (size === 2) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
} else {
|
||||
left = '2px';
|
||||
}
|
||||
} else if (size === 3) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
} else if (index > 0) {
|
||||
left = '2px';
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
bottom = '2px';
|
||||
} else if (index > 1) {
|
||||
top = '2px';
|
||||
}
|
||||
} else if (size === 4) {
|
||||
if (index === 0 || index === 2) {
|
||||
right = '2px';
|
||||
}
|
||||
|
||||
if (index === 1 || index === 3) {
|
||||
left = '2px';
|
||||
}
|
||||
|
||||
if (index < 2) {
|
||||
bottom = '2px';
|
||||
} else {
|
||||
top = '2px';
|
||||
}
|
||||
}
|
||||
|
||||
const style = {
|
||||
left: left,
|
||||
top: top,
|
||||
right: right,
|
||||
bottom: bottom,
|
||||
width: `${width}%`,
|
||||
height: `${height}%`,
|
||||
};
|
||||
|
||||
return (
|
||||
<StillImage key={account.get('id')} src={account.get('avatar')} style={style} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { accounts, size } = this.props;
|
||||
|
||||
return (
|
||||
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
||||
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
class FilterBar extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
active: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.node.addEventListener('keydown', this.handleKeyDown, false);
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
|
||||
const { left, width } = this.getActiveTabIndicationSize();
|
||||
this.setState({ mounted: true, left, width });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.node.removeEventListener('keydown', this.handleKeyDown, false);
|
||||
document.removeEventListener('resize', this.handleResize, false);
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
this.setState(this.getActiveTabIndicationSize());
|
||||
}, 300, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.active !== prevProps.active) {
|
||||
this.setState(this.getActiveTabIndicationSize());
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
||||
const index = items.indexOf(document.activeElement);
|
||||
let element = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
element = items[index + 1] || items[0];
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
element = items[index - 1] || items[items.length - 1];
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
handleItemKeyPress = e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this.handleClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const { action, to } = this.props.items[i];
|
||||
|
||||
if (typeof action === 'function') {
|
||||
e.preventDefault();
|
||||
action(e);
|
||||
} else if (to) {
|
||||
e.preventDefault();
|
||||
this.props.history.push(to);
|
||||
}
|
||||
}
|
||||
|
||||
getActiveTabIndicationSize() {
|
||||
const { active, items } = this.props;
|
||||
|
||||
if (!active || !this.node) return { width: null };
|
||||
|
||||
const index = items.findIndex(({ name }) => name === active);
|
||||
const elements = Array.from(this.node.getElementsByTagName('a'));
|
||||
const element = elements[index];
|
||||
|
||||
if (!element) return { width: null };
|
||||
|
||||
const left = element.offsetLeft;
|
||||
const { width } = element.getBoundingClientRect();
|
||||
|
||||
return { left, width };
|
||||
}
|
||||
|
||||
renderActiveTabIndicator() {
|
||||
const { left, width } = this.state;
|
||||
|
||||
return (
|
||||
<div className='filter-bar__active' style={{ left, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
renderItem(option, i) {
|
||||
if (option === null) {
|
||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||
}
|
||||
|
||||
const { name, text, href, to, title } = option;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={name}
|
||||
href={href || to || '#'}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
ref={i === 0 ? this.setFocusRef : null}
|
||||
onClick={this.handleClick}
|
||||
onKeyPress={this.handleItemKeyPress}
|
||||
data-index={i}
|
||||
title={title}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, items } = this.props;
|
||||
const { mounted } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames('filter-bar', className)} ref={this.setRef}>
|
||||
{mounted && this.renderActiveTabIndicator()}
|
||||
{items.map((option, i) => this.renderItem(option, i))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(FilterBar);
|
|
@ -1,15 +1,18 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage, FormattedList } from 'react-intl';
|
||||
import React, { MouseEventHandler, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import StatusMedia from 'soapbox/components/status-media';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
||||
|
||||
import OutlineBox from './outline-box';
|
||||
import StatusReplyMentions from './status-reply-mentions';
|
||||
import StatusContent from './status_content';
|
||||
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||
|
||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -36,7 +39,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
|
||||
const [showMedia, setShowMedia] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
|
||||
|
||||
const handleExpandClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleExpandClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (!status) return;
|
||||
const account = status.account as AccountEntity;
|
||||
|
||||
|
@ -57,57 +60,6 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
setShowMedia(!showMedia);
|
||||
};
|
||||
|
||||
const renderReplyMentions = () => {
|
||||
if (!status?.in_reply_to_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = status.account as AccountEntity;
|
||||
const to = status.mentions || [];
|
||||
|
||||
if (to.size === 0) {
|
||||
if (status.in_reply_to_account_id === account.id) {
|
||||
return (
|
||||
<div className='reply-mentions'>
|
||||
<FormattedMessage
|
||||
id='reply_mentions.reply'
|
||||
defaultMessage='Replying to {accounts}'
|
||||
values={{
|
||||
accounts: `@${account.username}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='reply-mentions'>
|
||||
<FormattedMessage id='reply_mentions.reply_empty' defaultMessage='Replying to post' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = to.slice(0, 2).map(account => <>@{account.username}</>).toArray();
|
||||
|
||||
if (to.size > 2) {
|
||||
accounts.push(
|
||||
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='reply-mentions'>
|
||||
<FormattedMessage
|
||||
id='reply_mentions.reply'
|
||||
defaultMessage='Replying to {accounts}'
|
||||
values={{
|
||||
accounts: <FormattedList type='conjunction' value={accounts} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
@ -127,7 +79,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
return (
|
||||
<OutlineBox
|
||||
data-testid='quoted-status'
|
||||
className={classNames('mt-3 cursor-pointer', {
|
||||
className={classNames('cursor-pointer', {
|
||||
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
|
||||
})}
|
||||
>
|
||||
|
@ -144,20 +96,36 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
withLinkToProfile={!compose}
|
||||
/>
|
||||
|
||||
{renderReplyMentions()}
|
||||
<StatusReplyMentions status={status} hoverable={false} />
|
||||
|
||||
<Text
|
||||
className='break-words status__content status__content--quote'
|
||||
size='sm'
|
||||
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
|
||||
/>
|
||||
<Stack className={classNames('relative', {
|
||||
'min-h-[220px]': status.hidden,
|
||||
})}
|
||||
>
|
||||
{(status.hidden) && (
|
||||
<SensitiveContentOverlay
|
||||
status={status}
|
||||
visible={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StatusMedia
|
||||
status={status}
|
||||
muted={compose}
|
||||
showMedia={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
<Stack space={4}>
|
||||
<StatusContent
|
||||
status={status}
|
||||
collapsable
|
||||
/>
|
||||
|
||||
{(status.media_attachments.size > 0) && (
|
||||
<StatusMedia
|
||||
status={status}
|
||||
muted={compose}
|
||||
showMedia={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</OutlineBox>
|
||||
);
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
export default class SettingText extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingKey: PropTypes.array.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
this.props.onChange(this.props.settingKey, e.target.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { settings, settingKey, label } = this.props;
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
<input
|
||||
className='setting-text'
|
||||
value={settings.getIn(settingKey)}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
.status-content p {
|
||||
@apply mb-5 whitespace-pre-wrap;
|
||||
@apply mb-4 whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
.status-content p:last-child {
|
||||
@apply mb-0.5;
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.status-content a {
|
||||
|
@ -20,7 +20,7 @@
|
|||
|
||||
.status-content ul,
|
||||
.status-content ol {
|
||||
@apply pl-10 mb-5;
|
||||
@apply pl-10 mb-4;
|
||||
}
|
||||
|
||||
.status-content ul {
|
||||
|
@ -32,7 +32,7 @@
|
|||
}
|
||||
|
||||
.status-content blockquote {
|
||||
@apply py-1 pl-4 mb-5 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
|
||||
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.status-content code {
|
||||
|
@ -51,7 +51,7 @@
|
|||
|
||||
/* Code block */
|
||||
.status-content pre {
|
||||
@apply py-2 px-3 mb-5 leading-6 overflow-x-auto rounded-md break-all;
|
||||
@apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all;
|
||||
}
|
||||
|
||||
.status-content pre:last-child {
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
/* Markdown images */
|
||||
.status-content img:not(.emojione):not([width][height]) {
|
||||
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-5 block;
|
||||
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block;
|
||||
}
|
||||
|
||||
/* User setting to underline links */
|
||||
|
|
|
@ -65,7 +65,6 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
hidden,
|
||||
featured,
|
||||
unread,
|
||||
group,
|
||||
hideActionBar,
|
||||
variant = 'rounded',
|
||||
withDismiss,
|
||||
|
@ -296,8 +295,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
const accountAction = props.accountAction || reblogElement;
|
||||
|
||||
const inReview = status.visibility === 'self';
|
||||
const isSensitive = status.hidden;
|
||||
const inReview = actualStatus.visibility === 'self';
|
||||
const isSensitive = actualStatus.hidden;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers} data-testid='status'>
|
||||
|
@ -349,6 +348,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
</div>
|
||||
|
||||
<div className='status__content-wrapper'>
|
||||
<StatusReplyMentions status={actualStatus} hoverable={hoverable} />
|
||||
|
||||
<Stack
|
||||
className={
|
||||
classNames('relative', {
|
||||
|
@ -356,40 +357,35 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
})
|
||||
}
|
||||
>
|
||||
{(inReview || isSensitive) ? (
|
||||
{(inReview || isSensitive) && (
|
||||
<SensitiveContentOverlay
|
||||
status={status}
|
||||
visible={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!group && actualStatus.group && (
|
||||
<div className='status__meta'>
|
||||
Posted in <NavLink to={`/groups/${actualStatus.getIn(['group', 'id'])}`}>{String(actualStatus.getIn(['group', 'title']))}</NavLink>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StatusReplyMentions
|
||||
status={actualStatus}
|
||||
hoverable={hoverable}
|
||||
/>
|
||||
<Stack space={4}>
|
||||
<StatusContent
|
||||
status={actualStatus}
|
||||
onClick={handleClick}
|
||||
collapsable
|
||||
/>
|
||||
|
||||
<StatusContent
|
||||
status={actualStatus}
|
||||
onClick={handleClick}
|
||||
collapsable
|
||||
/>
|
||||
{(quote || actualStatus.media_attachments.size > 0) && (
|
||||
<Stack space={4}>
|
||||
<StatusMedia
|
||||
status={actualStatus}
|
||||
muted={muted}
|
||||
onClick={handleClick}
|
||||
showMedia={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
|
||||
<StatusMedia
|
||||
status={actualStatus}
|
||||
muted={muted}
|
||||
onClick={handleClick}
|
||||
showMedia={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
|
||||
{quote}
|
||||
{quote}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{!hideActionBar && (
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl, settings, onChange, onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
<div className='column-settings__header'>
|
||||
<h1 className='column-settings__title'>
|
||||
<FormattedMessage id='community.column_settings.title' defaultMessage='Local timeline settings' />
|
||||
</h1>
|
||||
<div className='column-settings__close'>
|
||||
<IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/x.svg')} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='column-settings__content'>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='community_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ColumnSettings);
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { getSettings, changeSetting } from '../../../actions/settings';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: getSettings(state).get('community'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
onChange(key, checked) {
|
||||
dispatch(changeSetting(['community', ...key], checked));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
|
@ -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`}
|
||||
|
|
|
@ -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' />
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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}`}
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl, settings, onChange, onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
<div className='column-settings__header'>
|
||||
<h1 className='column-settings__title'>
|
||||
<FormattedMessage id='home.column_settings.title' defaultMessage='Home settings' />
|
||||
</h1>
|
||||
<div className='column-settings__close'>
|
||||
<IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/x.svg')} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='column-settings__content'>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'direct']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_direct' defaultMessage='Show direct messages' />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ColumnSettings);
|
|
@ -1,26 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
getSettings,
|
||||
changeSetting,
|
||||
saveSettings,
|
||||
} from '../../../actions/settings';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: getSettings(state).get('home'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange(key, checked) {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
},
|
||||
|
||||
onSave() {
|
||||
dispatch(saveSettings());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
|
@ -1,193 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
import ClearColumnButton from './clear_column_button';
|
||||
import MultiSettingToggle from './multi_setting_toggle';
|
||||
import SettingToggle from './setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
pushSettings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
supportsEmojiReacts: PropTypes.bool,
|
||||
supportsBirthdays: PropTypes.bool,
|
||||
};
|
||||
|
||||
onPushChange = (path, checked) => {
|
||||
this.props.onChange(['push', ...path], checked);
|
||||
}
|
||||
|
||||
onAllSoundsChange = (path, checked) => {
|
||||
const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']];
|
||||
|
||||
for (let i = 0; i < soundSettings.length; i++) {
|
||||
this.props.onChange(soundSettings[i], checked);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, settings, pushSettings, onChange, onClear, onClose, supportsEmojiReacts, supportsBirthdays } = this.props;
|
||||
|
||||
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const allSoundsStr = <FormattedMessage id='notifications.column_settings.sounds.all_sounds' defaultMessage='Play sound for all notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']];
|
||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||
const birthdaysStr = <FormattedMessage id='notifications.column_settings.birthdays.show' defaultMessage='Show birthday reminders' />;
|
||||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
<div className='column-settings__header'>
|
||||
<h1 className='column-settings__title'>
|
||||
<FormattedMessage id='notifications.column_settings.title' defaultMessage='Notification settings' />
|
||||
</h1>
|
||||
<div className='column-settings__close'>
|
||||
<IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/x.svg')} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='column-settings__content'>
|
||||
<div className='column-settings__row'>
|
||||
<ClearColumnButton onClick={onClear} />
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-all_sounds'>
|
||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.sounds' defaultMessage='Sounds' />
|
||||
</span>
|
||||
<MultiSettingToggle prefix='notifications_all_sounds' settings={settings} settingPaths={soundSettings} onChange={this.onAllSoundsChange} label={allSoundsStr} />
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
|
||||
</span>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{supportsBirthdays &&
|
||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.birthdays.category' defaultMessage='Birthdays' />
|
||||
</span>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['birthdays', 'show']} onChange={onChange} label={birthdaysStr} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div role='group' aria-labelledby='notifications-follow'>
|
||||
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-follow-request'>
|
||||
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-favourite'>
|
||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Likes:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{supportsEmojiReacts && <div role='group' aria-labelledby='notifications-emoji-react'>
|
||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.emoji_react' defaultMessage='Emoji reacts:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'pleroma:emoji_reaction']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'pleroma:emoji_reaction']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'pleroma:emoji_reaction']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'pleroma:emoji_reaction']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div role='group' aria-labelledby='notifications-mention'>
|
||||
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-reblog'>
|
||||
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Reposts:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-poll'>
|
||||
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'poll']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-move'>
|
||||
<span id='notifications-move' className='column-settings__section'><FormattedMessage id='notifications.column_settings.move' defaultMessage='Moves:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'move']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'move']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'move']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'move']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ColumnSettings);
|
|
@ -1,61 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
|
||||
const messages = defineMessages({
|
||||
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||
});
|
||||
|
||||
class FollowRequest extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
onAuthorize: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl, hidden, account, onAuthorize, onReject } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<Fragment>
|
||||
{account.get('display_name')}
|
||||
{account.get('username')}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<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={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
<div className='account__relationship'>
|
||||
<IconButton title={intl.formatMessage(messages.authorize)} src={require('@tabler/icons/check.svg')} onClick={onAuthorize} />
|
||||
<IconButton title={intl.formatMessage(messages.reject)} src={require('@tabler/icons/x.svg')} onClick={onReject} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(FollowRequest);
|
|
@ -1,39 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
export default class MultiSettingToggle extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
prefix: PropTypes.string,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingPaths: PropTypes.array.isRequired,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
ariaLabel: PropTypes.string,
|
||||
}
|
||||
|
||||
onChange = ({ target }) => {
|
||||
for (let i = 0; i < this.props.settingPaths.length; i++) {
|
||||
this.props.onChange(this.props.settingPaths[i], target.checked);
|
||||
}
|
||||
}
|
||||
|
||||
areTrue = (settingPath) => {
|
||||
return this.props.settings.getIn(settingPath) === true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { prefix, settingPaths, label, ariaLabel } = this.props;
|
||||
const id = ['setting-toggle', prefix].filter(Boolean).join('-');
|
||||
|
||||
return (
|
||||
<div className='setting-toggle' aria-label={ariaLabel}>
|
||||
<Toggle id={id} checked={settingPaths.every(this.areTrue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
|
||||
{label && (<label htmlFor={id} className='setting-toggle__label'>{label}</label>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { clearNotifications, setFilter } from 'soapbox/actions/notifications';
|
||||
import { changeAlerts as changePushNotifications } from 'soapbox/actions/push_notifications';
|
||||
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
|
||||
const messages = defineMessages({
|
||||
clearHeading: { id: 'notifications.clear_heading', defaultMessage: 'Clear notifications' },
|
||||
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
|
||||
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
|
||||
return {
|
||||
settings: getSettings(state).get('notifications'),
|
||||
pushSettings: state.get('push_notifications'),
|
||||
supportsEmojiReacts: features.emojiReacts,
|
||||
supportsBirthdays: features.birthdays,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onChange(path, checked) {
|
||||
if (path[0] === 'push') {
|
||||
dispatch(changePushNotifications(path.slice(1), checked));
|
||||
} else if (path[0] === 'quickFilter') {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
dispatch(setFilter('all'));
|
||||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
}
|
||||
},
|
||||
|
||||
onClear() {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/eraser.svg'),
|
||||
heading: intl.formatMessage(messages.clearHeading),
|
||||
message: intl.formatMessage(messages.clearMessage),
|
||||
confirm: intl.formatMessage(messages.clearConfirm),
|
||||
onConfirm: () => dispatch(clearNotifications()),
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
|
@ -1,28 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
import FollowRequest from '../components/follow_request';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: getAccount(state, props.id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
onAuthorize() {
|
||||
dispatch(authorizeFollowRequest(id));
|
||||
},
|
||||
|
||||
onReject() {
|
||||
dispatch(rejectFollowRequest(id));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest);
|
|
@ -1,56 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl, settings, onChange, onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
<div className='column-settings__header'>
|
||||
<h1 className='column-settings__title'>
|
||||
<FormattedMessage id='public.column_settings.title' defaultMessage='Fediverse timeline settings' />
|
||||
</h1>
|
||||
<div className='column-settings__close'>
|
||||
<IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/x.svg')} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='column-settings__content'>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='public_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='public_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ColumnSettings);
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { getSettings, changeSetting } from '../../../actions/settings';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: getSettings(state).get('public'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
onChange(key, checked) {
|
||||
dispatch(changeSetting(['public', ...key], checked));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
|
@ -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?' />}
|
||||
|
|
|
@ -29,7 +29,6 @@ interface IDetailedStatus {
|
|||
|
||||
const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||
status,
|
||||
onToggleHidden,
|
||||
onOpenCompareHistoryModal,
|
||||
onToggleMediaVisibility,
|
||||
showMedia,
|
||||
|
@ -93,23 +92,29 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
})
|
||||
}
|
||||
>
|
||||
{(isUnderReview || isSensitive) ? (
|
||||
{(isUnderReview || isSensitive) && (
|
||||
<SensitiveContentOverlay
|
||||
status={status}
|
||||
visible={showMedia}
|
||||
onToggleVisibility={onToggleMediaVisibility}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<StatusContent status={actualStatus} />
|
||||
<Stack space={4}>
|
||||
<StatusContent status={actualStatus} />
|
||||
|
||||
<StatusMedia
|
||||
status={actualStatus}
|
||||
showMedia={showMedia}
|
||||
onToggleVisibility={onToggleMediaVisibility}
|
||||
/>
|
||||
{(quote || actualStatus.media_attachments.size > 0) && (
|
||||
<Stack space={4}>
|
||||
<StatusMedia
|
||||
status={actualStatus}
|
||||
showMedia={showMedia}
|
||||
onToggleVisibility={onToggleMediaVisibility}
|
||||
/>
|
||||
|
||||
{quote}
|
||||
{quote}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<HStack justifyContent='between' alignItems='center' className='py-2' wrap>
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
|
||||
export default class AccountListPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
accountIds: ImmutablePropTypes.orderedSet.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
limit: PropTypes.number,
|
||||
total: PropTypes.number,
|
||||
expandMessage: PropTypes.string,
|
||||
expandRoute: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
limit: Infinity,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, icon, accountIds, limit, total, expandMessage, expandRoute, ...props } = this.props;
|
||||
|
||||
if (!accountIds || accountIds.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canExpand = expandMessage && expandRoute && (accountIds.size < total);
|
||||
|
||||
return (
|
||||
<div className='wtf-panel'>
|
||||
<div className='wtf-panel-header'>
|
||||
<Icon src={icon} className='wtf-panel-header__icon' />
|
||||
<span className='wtf-panel-header__label'>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<div className='wtf-panel__content'>
|
||||
<div className='wtf-panel__list'>
|
||||
{accountIds.take(limit).map(accountId => (
|
||||
<AccountContainer key={accountId} id={accountId} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{canExpand && <Link className='wtf-panel__expand-btn' to={expandRoute}>
|
||||
{expandMessage}
|
||||
</Link>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -112,6 +112,7 @@ import {
|
|||
TestTimeline,
|
||||
LogoutPage,
|
||||
AuthTokenList,
|
||||
ServiceWorkerInfo,
|
||||
} from './util/async-components';
|
||||
import { WrappedRoute } from './util/react_router_helpers';
|
||||
|
||||
|
@ -311,6 +312,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');
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export default {
|
||||
message: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element,
|
||||
]).isRequired,
|
||||
action: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
]),
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.bool,
|
||||
actionStyle: PropTypes.object,
|
||||
titleStyle: PropTypes.object,
|
||||
barStyle: PropTypes.object,
|
||||
activeBarStyle: PropTypes.object,
|
||||
dismissAfter: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
]),
|
||||
onDismiss: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
activeClassName: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
]),
|
||||
};
|
|
@ -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, {}> {}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { default as Notification } from './notification';
|
||||
export { default as NotificationStack } from './notificationStack';
|
|
@ -1,175 +0,0 @@
|
|||
/* linting temp disabled while working on updates */
|
||||
/* eslint-disable */
|
||||
import React, { Component } from 'react';
|
||||
import defaultPropTypes from './defaultPropTypes';
|
||||
|
||||
class Notification extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getBarStyle = this.getBarStyle.bind(this);
|
||||
this.getActionStyle = this.getActionStyle.bind(this);
|
||||
this.getTitleStyle = this.getTitleStyle.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
|
||||
if (props.onDismiss && props.isActive) {
|
||||
this.dismissTimeout = setTimeout(
|
||||
props.onDismiss,
|
||||
props.dismissAfter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.dismissAfter === false) return;
|
||||
|
||||
// See http://eslint.org/docs/rules/no-prototype-builtins
|
||||
if (!{}.hasOwnProperty.call(nextProps, 'isLast')) {
|
||||
clearTimeout(this.dismissTimeout);
|
||||
}
|
||||
|
||||
if (nextProps.onDismiss) {
|
||||
if (
|
||||
(nextProps.isActive && !this.props.isActive) ||
|
||||
(nextProps.dismissAfter && this.props.dismissAfter === false)
|
||||
) {
|
||||
this.dismissTimeout = setTimeout(
|
||||
nextProps.onDismiss,
|
||||
nextProps.dismissAfter
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.dismissAfter) clearTimeout(this.dismissTimeout);
|
||||
}
|
||||
|
||||
/*
|
||||
* @description Dynamically get the styles for the bar.
|
||||
* @returns {object} result The style.
|
||||
*/
|
||||
getBarStyle() {
|
||||
if (this.props.style === false) return {};
|
||||
|
||||
const { isActive, barStyle, activeBarStyle } = this.props;
|
||||
|
||||
const baseStyle = {
|
||||
position: 'fixed',
|
||||
bottom: '2rem',
|
||||
left: '-100%',
|
||||
width: 'auto',
|
||||
padding: '1rem',
|
||||
margin: 0,
|
||||
color: '#fafafa',
|
||||
font: '1rem normal Roboto, sans-serif',
|
||||
borderRadius: '5px',
|
||||
background: '#212121',
|
||||
borderSizing: 'border-box',
|
||||
boxShadow: '0 0 1px 1px rgba(10, 10, 11, .125)',
|
||||
cursor: 'default',
|
||||
WebKitTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)',
|
||||
MozTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)',
|
||||
msTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)',
|
||||
OTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)',
|
||||
transition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)',
|
||||
WebkitTransform: 'translatez(0)',
|
||||
MozTransform: 'translatez(0)',
|
||||
msTransform: 'translatez(0)',
|
||||
OTransform: 'translatez(0)',
|
||||
transform: 'translatez(0)'
|
||||
};
|
||||
|
||||
return isActive ?
|
||||
Object.assign({}, baseStyle, { left: '1rem' }, barStyle, activeBarStyle) :
|
||||
Object.assign({}, baseStyle, barStyle);
|
||||
}
|
||||
|
||||
/*
|
||||
* @function getActionStyle
|
||||
* @description Dynamically get the styles for the action text.
|
||||
* @returns {object} result The style.
|
||||
*/
|
||||
getActionStyle() {
|
||||
return this.props.style !== false ? Object.assign({}, {
|
||||
padding: '0.125rem',
|
||||
marginLeft: '1rem',
|
||||
color: '#f44336',
|
||||
font: '.75rem normal Roboto, sans-serif',
|
||||
lineHeight: '1rem',
|
||||
letterSpacing: '.125ex',
|
||||
textTransform: 'uppercase',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer'
|
||||
}, this.props.actionStyle) : {};
|
||||
}
|
||||
|
||||
/*
|
||||
* @function getTitleStyle
|
||||
* @description Dynamically get the styles for the title.
|
||||
* @returns {object} result The style.
|
||||
*/
|
||||
getTitleStyle() {
|
||||
return this.props.style !== false ? Object.assign({}, {
|
||||
fontWeight: '700',
|
||||
marginRight: '.5rem'
|
||||
}, this.props.titleStyle) : {};
|
||||
}
|
||||
|
||||
/*
|
||||
* @function handleClick
|
||||
* @description Handle click events on the action button.
|
||||
*/
|
||||
handleClick() {
|
||||
if (this.props.onClick && typeof this.props.onClick === 'function') {
|
||||
return this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = 'notification-bar';
|
||||
|
||||
if (this.props.isActive) className += ` ${this.props.activeClassName}`;
|
||||
if (this.props.className) className += ` ${this.props.className}`;
|
||||
|
||||
return (
|
||||
<div className={className} style={this.getBarStyle()}>
|
||||
<div className="notification-bar-wrapper">
|
||||
{this.props.title ? (
|
||||
<span
|
||||
className="notification-bar-title"
|
||||
style={this.getTitleStyle()}
|
||||
>
|
||||
{this.props.title}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{/* eslint-disable */}
|
||||
<span className="notification-bar-message">
|
||||
{this.props.message}
|
||||
</span>
|
||||
|
||||
{this.props.action ? (
|
||||
<span
|
||||
className="notification-bar-action"
|
||||
onClick={this.handleClick}
|
||||
style={this.getActionStyle()}
|
||||
>
|
||||
{this.props.action}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Notification.propTypes = defaultPropTypes;
|
||||
|
||||
Notification.defaultProps = {
|
||||
isActive: false,
|
||||
dismissAfter: 2000,
|
||||
activeClassName: 'notification-bar-active'
|
||||
};
|
||||
|
||||
export default Notification;
|
|
@ -1,95 +0,0 @@
|
|||
/* linting temp disabled while working on updates */
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import StackedNotification from './stackedNotification';
|
||||
import defaultPropTypes from './defaultPropTypes';
|
||||
|
||||
function defaultBarStyleFactory(index, style) {
|
||||
return Object.assign(
|
||||
{},
|
||||
style,
|
||||
{ bottom: `${2 + (index * 4)}rem` }
|
||||
);
|
||||
}
|
||||
|
||||
function defaultActionStyleFactory(index, style) {
|
||||
return Object.assign(
|
||||
{},
|
||||
style,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification list does not have any state, so use a
|
||||
* pure function here. It just needs to return the stacked array
|
||||
* of notification components.
|
||||
*/
|
||||
const NotificationStack = props => (
|
||||
<div className="notification-list">
|
||||
{props.notifications.map((notification, index) => {
|
||||
const isLast = index === 0 && props.notifications.length === 1;
|
||||
const dismissNow = isLast || !props.dismissInOrder;
|
||||
|
||||
// Handle styles
|
||||
const barStyle = props.barStyleFactory(index, notification.barStyle, notification);
|
||||
const actionStyle = props.actionStyleFactory(index, notification.actionStyle, notification);
|
||||
const activeBarStyle = props.activeBarStyleFactory(
|
||||
index,
|
||||
notification.activeBarStyle,
|
||||
notification
|
||||
);
|
||||
|
||||
// Allow onClick from notification stack or individual notifications
|
||||
const onClick = notification.onClick || props.onClick;
|
||||
const onDismiss = props.onDismiss;
|
||||
|
||||
let { dismissAfter } = notification;
|
||||
|
||||
if (dismissAfter !== false) {
|
||||
if (dismissAfter == null) dismissAfter = props.dismissAfter;
|
||||
if (!dismissNow) dismissAfter += index * 1000;
|
||||
}
|
||||
|
||||
return (
|
||||
<StackedNotification
|
||||
{...notification}
|
||||
key={notification.key}
|
||||
isLast={isLast}
|
||||
action={notification.action || props.action}
|
||||
dismissAfter={dismissAfter}
|
||||
onDismiss={onDismiss.bind(this, notification)}
|
||||
onClick={onClick.bind(this, notification)}
|
||||
activeBarStyle={activeBarStyle}
|
||||
barStyle={barStyle}
|
||||
actionStyle={actionStyle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
/* eslint-disable react/no-unused-prop-types, react/forbid-prop-types */
|
||||
NotificationStack.propTypes = {
|
||||
activeBarStyleFactory: PropTypes.func,
|
||||
barStyleFactory: PropTypes.func,
|
||||
actionStyleFactory: PropTypes.func,
|
||||
dismissInOrder: PropTypes.bool,
|
||||
notifications: PropTypes.array.isRequired,
|
||||
onDismiss: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
action: defaultPropTypes.action
|
||||
};
|
||||
|
||||
NotificationStack.defaultProps = {
|
||||
activeBarStyleFactory: defaultBarStyleFactory,
|
||||
barStyleFactory: defaultBarStyleFactory,
|
||||
actionStyleFactory: defaultActionStyleFactory,
|
||||
dismissInOrder: true,
|
||||
dismissAfter: 1000,
|
||||
onClick: () => {}
|
||||
};
|
||||
/* eslint-enable no-alert, no-console */
|
||||
|
||||
export default NotificationStack;
|
|
@ -1,69 +0,0 @@
|
|||
/* linting temp disabled while working on updates */
|
||||
/* eslint-disable */
|
||||
import React, { Component } from 'react';
|
||||
import defaultPropTypes from './defaultPropTypes';
|
||||
import Notification from './notification';
|
||||
|
||||
class StackedNotification extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isActive: false
|
||||
};
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.activeTimeout = setTimeout(this.setState.bind(this, {
|
||||
isActive: true
|
||||
}), 1);
|
||||
|
||||
this.dismiss(this.props.dismissAfter);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.dismissAfter !== this.props.dismissAfter) {
|
||||
this.dismiss(nextProps.dismissAfter);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.activeTimeout);
|
||||
clearTimeout(this.dismissTimeout);
|
||||
}
|
||||
|
||||
dismiss(dismissAfter) {
|
||||
if (dismissAfter === false) return;
|
||||
|
||||
this.dismissTimeout = setTimeout(this.setState.bind(this, {
|
||||
isActive: false
|
||||
}), dismissAfter);
|
||||
}
|
||||
|
||||
/*
|
||||
* @function handleClick
|
||||
* @description Bind deactivate Notification function to Notification click handler
|
||||
*/
|
||||
handleClick() {
|
||||
if (this.props.onClick && typeof this.props.onClick === 'function') {
|
||||
return this.props.onClick(this.setState.bind(this, { isActive: false }));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Notification
|
||||
{...this.props}
|
||||
onClick={this.handleClick}
|
||||
onDismiss={() => setTimeout(this.props.onDismiss, 300)}
|
||||
isActive={this.state.isActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StackedNotification.propTypes = defaultPropTypes;
|
||||
|
||||
export default StackedNotification;
|
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -1,153 +0,0 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import * as actions from 'soapbox/actions/lists';
|
||||
|
||||
import reducer from '../list_editor';
|
||||
|
||||
describe('list_editor reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toMatchObject({
|
||||
listId: null,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: '',
|
||||
|
||||
accounts: {
|
||||
items: ImmutableList(),
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
},
|
||||
|
||||
suggestions: {
|
||||
value: '',
|
||||
items: ImmutableList(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LIST_EDITOR_RESET', () => {
|
||||
const state = ImmutableRecord({
|
||||
listId: null,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: '',
|
||||
|
||||
accounts: ImmutableRecord({
|
||||
items: ImmutableList(),
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
})(),
|
||||
|
||||
suggestions: ImmutableRecord({
|
||||
value: '',
|
||||
items: ImmutableList(),
|
||||
})(),
|
||||
})();
|
||||
const action = {
|
||||
type: actions.LIST_EDITOR_RESET,
|
||||
};
|
||||
expect(reducer(state, action)).toMatchObject({
|
||||
listId: null,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: '',
|
||||
|
||||
accounts: {
|
||||
items: ImmutableList(),
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
},
|
||||
|
||||
suggestions: {
|
||||
value: '',
|
||||
items: ImmutableList(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LIST_EDITOR_SETUP', () => {
|
||||
const state = ImmutableRecord({
|
||||
listId: null,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: '',
|
||||
|
||||
accounts: ImmutableRecord({
|
||||
items: ImmutableList(),
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
})(),
|
||||
|
||||
suggestions: ImmutableRecord({
|
||||
value: '',
|
||||
items: ImmutableList(),
|
||||
})(),
|
||||
})();
|
||||
const action = {
|
||||
type: actions.LIST_EDITOR_SETUP,
|
||||
list: ImmutableMap({
|
||||
id: '22',
|
||||
title: 'list 1',
|
||||
}),
|
||||
};
|
||||
expect(reducer(state, action)).toMatchObject({
|
||||
listId: '22',
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: 'list 1',
|
||||
|
||||
accounts: {
|
||||
items: ImmutableList(),
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
},
|
||||
|
||||
suggestions: {
|
||||
value: '',
|
||||
items: ImmutableList(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LIST_EDITOR_TITLE_CHANGE', () => {
|
||||
const state = ImmutableMap({
|
||||
title: 'list 1',
|
||||
isChanged: false,
|
||||
});
|
||||
const action = {
|
||||
type: actions.LIST_EDITOR_TITLE_CHANGE,
|
||||
value: 'list 1 edited',
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
isChanged: true,
|
||||
title: 'list 1 edited',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LIST_UPDATE_REQUEST', () => {
|
||||
const state = ImmutableMap({
|
||||
isSubmitting: false,
|
||||
isChanged: true,
|
||||
});
|
||||
const action = {
|
||||
type: actions.LIST_UPDATE_REQUEST,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
isSubmitting: true,
|
||||
isChanged: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LIST_UPDATE_FAIL', () => {
|
||||
const state = ImmutableMap({
|
||||
isSubmitting: true,
|
||||
});
|
||||
const action = {
|
||||
type: actions.LIST_UPDATE_FAIL,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
isSubmitting: false,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -1,673 +0,0 @@
|
|||
import {
|
||||
Map as ImmutableMap,
|
||||
OrderedMap as ImmutableOrderedMap,
|
||||
Record as ImmutableRecord,
|
||||
} from 'immutable';
|
||||
import take from 'lodash/take';
|
||||
|
||||
import intlMessages from 'soapbox/__fixtures__/intlMessages.json';
|
||||
import notification from 'soapbox/__fixtures__/notification.json';
|
||||
import notifications from 'soapbox/__fixtures__/notifications.json';
|
||||
import relationship from 'soapbox/__fixtures__/relationship.json';
|
||||
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
|
||||
import {
|
||||
MARKER_FETCH_SUCCESS,
|
||||
MARKER_SAVE_REQUEST,
|
||||
MARKER_SAVE_SUCCESS,
|
||||
} from 'soapbox/actions/markers';
|
||||
import {
|
||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
NOTIFICATIONS_EXPAND_REQUEST,
|
||||
NOTIFICATIONS_EXPAND_FAIL,
|
||||
NOTIFICATIONS_FILTER_SET,
|
||||
NOTIFICATIONS_SCROLL_TOP,
|
||||
NOTIFICATIONS_UPDATE,
|
||||
NOTIFICATIONS_UPDATE_QUEUE,
|
||||
NOTIFICATIONS_DEQUEUE,
|
||||
NOTIFICATIONS_CLEAR,
|
||||
NOTIFICATIONS_MARK_READ_REQUEST,
|
||||
} from 'soapbox/actions/notifications';
|
||||
import { TIMELINE_DELETE } from 'soapbox/actions/timelines';
|
||||
import { applyActions } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import reducer from '../notifications';
|
||||
|
||||
const initialState = reducer(undefined, {});
|
||||
|
||||
describe('notifications reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
const expected = {
|
||||
items: {},
|
||||
hasMore: true,
|
||||
top: false,
|
||||
unread: 0,
|
||||
isLoading: false,
|
||||
queuedNotifications: {},
|
||||
totalQueuedNotificationsCount: 0,
|
||||
lastRead: -1,
|
||||
};
|
||||
|
||||
expect(ImmutableRecord.isRecord(initialState)).toBe(true);
|
||||
expect(initialState.toJS()).toMatchObject(expected);
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_EXPAND_SUCCESS', () => {
|
||||
it('imports the notifications', () => {
|
||||
const action = {
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications: take(notifications, 3),
|
||||
next: null,
|
||||
skipLoading: true,
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
|
||||
// The items are parsed as records
|
||||
expect(ImmutableOrderedMap.isOrderedMap(result.items)).toBe(true);
|
||||
expect(ImmutableRecord.isRecord(result.items.get('10743'))).toBe(true);
|
||||
|
||||
// We can get an item
|
||||
expect(result.items.get('10744').emoji).toEqual('😢');
|
||||
|
||||
// hasMore is set to false because `next` is null
|
||||
expect(result.hasMore).toBe(false);
|
||||
});
|
||||
|
||||
it('drops invalid notifications', () => {
|
||||
const action = {
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications: [
|
||||
{ id: '1', type: 'mention', status: null, account: { id: '10' } },
|
||||
{ id: '2', type: 'reblog', status: null, account: { id: '9' } },
|
||||
{ id: '3', type: 'favourite', status: null, account: { id: '8' } },
|
||||
{ id: '4', type: 'mention', status: { id: 'a' }, account: { id: '7' } },
|
||||
{ id: '5', type: 'reblog', status: { id: 'b' }, account: null },
|
||||
],
|
||||
next: null,
|
||||
skipLoading: true,
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
|
||||
// Only '4' is valid
|
||||
expect(result.items.size).toEqual(1);
|
||||
expect(result.items.get('4').id).toEqual('4');
|
||||
});
|
||||
|
||||
it('imports move notification', () => {
|
||||
const action = {
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications: [
|
||||
require('soapbox/__fixtures__/pleroma-notification-move.json'),
|
||||
],
|
||||
next: null,
|
||||
skipLoading: true,
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action).items.get('406814');
|
||||
|
||||
expect(result.account).toEqual('AFmHQ18XZ7Lco68MW8');
|
||||
expect(result.target).toEqual('A5c5LK7EJTFR0u26Pg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_EXPAND_REQUEST', () => {
|
||||
it('sets isLoading to true', () => {
|
||||
const state = initialState.set('isLoading', false);
|
||||
const action = { type: NOTIFICATIONS_EXPAND_REQUEST };
|
||||
|
||||
expect(reducer(state, action).isLoading).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_EXPAND_FAIL', () => {
|
||||
it('sets isLoading to false', () => {
|
||||
const state = initialState.set('isLoading', true);
|
||||
const action = { type: NOTIFICATIONS_EXPAND_FAIL };
|
||||
|
||||
expect(reducer(state, action).isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_FILTER_SET', () => {
|
||||
it('clears the items', () => {
|
||||
const actions = [{
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications: [
|
||||
{ id: '1', type: 'mention', status: { id: '4' }, account: { id: '7' } },
|
||||
{ id: '2', type: 'mention', status: { id: '5' }, account: { id: '8' } },
|
||||
{ id: '3', type: 'mention', status: { id: '6' }, account: { id: '9' } },
|
||||
],
|
||||
next: null,
|
||||
skipLoading: true,
|
||||
}, {
|
||||
type: NOTIFICATIONS_FILTER_SET,
|
||||
}];
|
||||
|
||||
// Setup by expanding, then calling `NOTIFICATIONS_FILTER_SET`
|
||||
const result = applyActions(initialState, actions, reducer);
|
||||
|
||||
// Setting the filter wipes notifications
|
||||
expect(result.items.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets hasMore to true', () => {
|
||||
const state = initialState.set('hasMore', false);
|
||||
const action = { type: NOTIFICATIONS_FILTER_SET };
|
||||
const result = reducer(state, action);
|
||||
|
||||
expect(result.hasMore).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_SCROLL_TOP', () => {
|
||||
it('resets `unread` counter to 0 when top is true (ie, scrolled to the top)', () => {
|
||||
const state = initialState.set('unread', 1);
|
||||
const action = { type: NOTIFICATIONS_SCROLL_TOP, top: true };
|
||||
const result = reducer(state, action);
|
||||
|
||||
expect(result.unread).toEqual(0);
|
||||
expect(result.top).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves `unread` alone when top is false (ie, not scrolled to top)', () => {
|
||||
const state = initialState.set('unread', 3);
|
||||
const action = { type: NOTIFICATIONS_SCROLL_TOP, top: false };
|
||||
const result = reducer(state, action);
|
||||
|
||||
expect(result.unread).toEqual(3);
|
||||
expect(result.top).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_UPDATE', () => {
|
||||
it('imports the notification', () => {
|
||||
const action = { type: NOTIFICATIONS_UPDATE, notification };
|
||||
const result = reducer(initialState, action);
|
||||
|
||||
expect(result.items.get('10743').type).toEqual('favourite');
|
||||
});
|
||||
|
||||
it('imports follow_request notification', () => {
|
||||
const action = {
|
||||
type: NOTIFICATIONS_UPDATE,
|
||||
notification: require('soapbox/__fixtures__/notification-follow_request.json'),
|
||||
};
|
||||
|
||||
const result = reducer(initialState, action);
|
||||
expect(result.items.get('87967').type).toEqual('follow_request');
|
||||
});
|
||||
|
||||
it('increments `unread` counter when top is false', () => {
|
||||
const action = { type: NOTIFICATIONS_UPDATE, notification };
|
||||
const result = reducer(initialState, action);
|
||||
|
||||
expect(result.unread).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_UPDATE_QUEUE', () => {
|
||||
it('adds the notification to the queue (and increases the counter)', () => {
|
||||
const action = {
|
||||
type: NOTIFICATIONS_UPDATE_QUEUE,
|
||||
notification,
|
||||
intlMessages,
|
||||
intlLocale: 'en',
|
||||
};
|
||||
|
||||
const result = reducer(initialState, action);
|
||||
|
||||
// Doesn't add it as a regular item
|
||||
expect(result.items.isEmpty()).toBe(true);
|
||||
|
||||
// Adds it to the queued items
|
||||
expect(result.queuedNotifications.size).toEqual(1);
|
||||
expect(result.totalQueuedNotificationsCount).toEqual(1);
|
||||
expect(result.queuedNotifications.getIn(['10743', 'notification', 'type'])).toEqual('favourite');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_DEQUEUE', () => {
|
||||
it('resets the queued counter to 0', () => {
|
||||
const state = initialState.set('totalQueuedNotificationsCount', 1);
|
||||
const action = { type: NOTIFICATIONS_DEQUEUE };
|
||||
const result = reducer(state, action);
|
||||
|
||||
expect(result.totalQueuedNotificationsCount).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_EXPAND_SUCCESS', () => {
|
||||
it('with non-empty items and next set true', () => {
|
||||
const state = ImmutableMap({
|
||||
items: ImmutableOrderedMap([
|
||||
['10734', ImmutableMap({
|
||||
id: '10734',
|
||||
type: 'pleroma:emoji_reaction',
|
||||
account: '9vMAje101ngtjlMj7w',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:54:39.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: '😢',
|
||||
chat_message: null,
|
||||
})],
|
||||
]),
|
||||
unread: 1,
|
||||
hasMore: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const action = {
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications: take(notifications, 3),
|
||||
next: true,
|
||||
};
|
||||
|
||||
const expected = ImmutableMap({
|
||||
items: ImmutableOrderedMap([
|
||||
['10744', ImmutableMap({
|
||||
id: '10744',
|
||||
type: 'pleroma:emoji_reaction',
|
||||
account: '9vMAje101ngtjlMj7w',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:54:39.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: '😢',
|
||||
chat_message: null,
|
||||
total_count: null,
|
||||
})],
|
||||
['10743', ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
total_count: null,
|
||||
})],
|
||||
['10741', ImmutableMap({
|
||||
id: '10741',
|
||||
type: 'favourite',
|
||||
account: '9v5cKMOPGqPcgfcWp6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
total_count: null,
|
||||
})],
|
||||
['10734', ImmutableMap({
|
||||
id: '10734',
|
||||
type: 'pleroma:emoji_reaction',
|
||||
account: '9vMAje101ngtjlMj7w',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:54:39.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: '😢',
|
||||
chat_message: null,
|
||||
})],
|
||||
]),
|
||||
unread: 1,
|
||||
hasMore: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
expect(reducer(state, action).toJS()).toEqual(expected.toJS());
|
||||
});
|
||||
|
||||
it('with empty items and next set true', () => {
|
||||
const state = ImmutableMap({
|
||||
items: ImmutableOrderedMap(),
|
||||
unread: 1,
|
||||
hasMore: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const action = {
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications: take(notifications, 3),
|
||||
next: true,
|
||||
};
|
||||
|
||||
const expected = ImmutableMap({
|
||||
items: ImmutableOrderedMap([
|
||||
['10744', ImmutableMap({
|
||||
id: '10744',
|
||||
type: 'pleroma:emoji_reaction',
|
||||
account: '9vMAje101ngtjlMj7w',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:54:39.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: '😢',
|
||||
chat_message: null,
|
||||
total_count: null,
|
||||
})],
|
||||
['10743', ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
total_count: null,
|
||||
})],
|
||||
['10741', ImmutableMap({
|
||||
id: '10741',
|
||||
type: 'favourite',
|
||||
account: '9v5cKMOPGqPcgfcWp6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
total_count: null,
|
||||
})],
|
||||
]),
|
||||
unread: 1,
|
||||
hasMore: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
expect(reducer(state, action).toJS()).toEqual(expected.toJS());
|
||||
});
|
||||
});
|
||||
|
||||
describe('ACCOUNT_BLOCK_SUCCESS', () => {
|
||||
it('should handle', () => {
|
||||
const state = ImmutableMap({
|
||||
items: ImmutableOrderedMap([
|
||||
['10744', ImmutableMap({
|
||||
id: '10744',
|
||||
type: 'pleroma:emoji_reaction',
|
||||
account: '9vMAje101ngtjlMj7w',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:54:39.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: '😢',
|
||||
chat_message: null,
|
||||
})],
|
||||
['10743', ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
['10741', ImmutableMap({
|
||||
id: '10741',
|
||||
type: 'favourite',
|
||||
account: '9v5cKMOPGqPcgfcWp6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
]),
|
||||
});
|
||||
const action = {
|
||||
type: ACCOUNT_BLOCK_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
expect(reducer(state, action)).toEqual(ImmutableMap({
|
||||
items: ImmutableOrderedMap([
|
||||
['10743', ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
['10741', ImmutableMap({
|
||||
id: '10741',
|
||||
type: 'favourite',
|
||||
account: '9v5cKMOPGqPcgfcWp6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
]),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('ACCOUNT_MUTE_SUCCESS', () => {
|
||||
it('should handle', () => {
|
||||
const state = ImmutableMap({
|
||||
items: ImmutableOrderedMap([
|
||||
['10744', ImmutableMap({
|
||||
id: '10744',
|
||||
type: 'pleroma:emoji_reaction',
|
||||
account: '9vMAje101ngtjlMj7w',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:54:39.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: '😢',
|
||||
chat_message: null,
|
||||
})],
|
||||
['10743', ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
['10741', ImmutableMap({
|
||||
id: '10741',
|
||||
type: 'favourite',
|
||||
account: '9v5cKMOPGqPcgfcWp6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
]),
|
||||
});
|
||||
const action = {
|
||||
type: ACCOUNT_MUTE_SUCCESS,
|
||||
relationship: relationship,
|
||||
};
|
||||
expect(reducer(state, action)).toEqual(ImmutableMap({
|
||||
items: ImmutableOrderedMap([
|
||||
['10743', ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
['10741', ImmutableMap({
|
||||
id: '10741',
|
||||
type: 'favourite',
|
||||
account: '9v5cKMOPGqPcgfcWp6',
|
||||
target: null,
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: null,
|
||||
chat_message: null,
|
||||
})],
|
||||
]),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_CLEAR', () => {
|
||||
it('clears the items', () => {
|
||||
const state = initialState.set('items', ImmutableOrderedMap([['1', {}], ['2', {}]]));
|
||||
const action = { type: NOTIFICATIONS_CLEAR };
|
||||
const result = reducer(state, action);
|
||||
|
||||
expect(result.items.isEmpty()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOTIFICATIONS_MARK_READ_REQUEST', () => {
|
||||
it('sets lastRead to the one in the action', () => {
|
||||
const action = { type: NOTIFICATIONS_MARK_READ_REQUEST, lastRead: '1234' };
|
||||
const result = reducer(undefined, action);
|
||||
|
||||
expect(result.lastRead).toEqual('1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TIMELINE_DELETE', () => {
|
||||
it('deletes notifications corresponding to the status ID', () => {
|
||||
const actions = [{
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications: [
|
||||
{ id: '1', type: 'mention', status: { id: '4' }, account: { id: '7' } },
|
||||
{ id: '2', type: 'mention', status: { id: '5' }, account: { id: '8' } },
|
||||
{ id: '3', type: 'mention', status: { id: '6' }, account: { id: '9' } },
|
||||
{ id: '4', type: 'mention', status: { id: '5' }, account: { id: '7' } },
|
||||
],
|
||||
next: null,
|
||||
skipLoading: true,
|
||||
}, {
|
||||
type: TIMELINE_DELETE,
|
||||
id: '5',
|
||||
}];
|
||||
|
||||
// Setup by expanding, then calling `NOTIFICATIONS_FILTER_SET`
|
||||
const result = applyActions(initialState, actions, reducer);
|
||||
|
||||
expect(result.items.size).toEqual(2);
|
||||
expect(result.items.get('5')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MARKER_FETCH_SUCCESS', () => {
|
||||
it('sets lastRead', () => {
|
||||
const action = {
|
||||
type: MARKER_FETCH_SUCCESS,
|
||||
timeline: ['notifications'],
|
||||
marker: {
|
||||
notifications: {
|
||||
last_read_id: '1234',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(reducer(undefined, action).get('lastRead')).toEqual('1234');
|
||||
});
|
||||
|
||||
it('updates the unread count', () => {
|
||||
const action = {
|
||||
type: MARKER_FETCH_SUCCESS,
|
||||
timeline: ['notifications'],
|
||||
marker: {
|
||||
notifications: {
|
||||
last_read_id: '5678',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = ImmutableMap({
|
||||
items: ImmutableOrderedMap({
|
||||
'9012': ImmutableMap({ id: '9012' }),
|
||||
'5678': ImmutableMap({ id: '5678' }),
|
||||
'1234': ImmutableMap({ id: '1234' }),
|
||||
}),
|
||||
unread: 3,
|
||||
});
|
||||
|
||||
expect(reducer(state, action).get('unread')).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MARKER_SAVE_REQUEST', () => {
|
||||
it('sets lastRead', () => {
|
||||
const action = {
|
||||
type: MARKER_SAVE_REQUEST,
|
||||
timeline: ['notifications'],
|
||||
marker: {
|
||||
notifications: {
|
||||
last_read_id: '1234',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(reducer(undefined, action).get('lastRead')).toEqual('1234');
|
||||
});
|
||||
|
||||
it('updates the unread count', () => {
|
||||
const action = {
|
||||
type: MARKER_SAVE_REQUEST,
|
||||
timeline: ['notifications'],
|
||||
marker: {
|
||||
notifications: {
|
||||
last_read_id: '5678',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = ImmutableMap({
|
||||
items: ImmutableOrderedMap({
|
||||
'9012': ImmutableMap({ id: '9012' }),
|
||||
'5678': ImmutableMap({ id: '5678' }),
|
||||
'1234': ImmutableMap({ id: '1234' }),
|
||||
}),
|
||||
unread: 3,
|
||||
});
|
||||
|
||||
expect(reducer(state, action).get('unread')).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MARKER_SAVE_SUCCESS', () => {
|
||||
it('sets lastRead', () => {
|
||||
const action = {
|
||||
type: MARKER_SAVE_SUCCESS,
|
||||
timeline: ['notifications'],
|
||||
marker: {
|
||||
notifications: {
|
||||
last_read_id: '5678',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(reducer(undefined, action).get('lastRead')).toEqual('5678');
|
||||
});
|
||||
|
||||
it('updates the unread count', () => {
|
||||
const action = {
|
||||
type: MARKER_SAVE_SUCCESS,
|
||||
timeline: ['notifications'],
|
||||
marker: {
|
||||
notifications: {
|
||||
last_read_id: '9012',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = ImmutableMap({
|
||||
items: ImmutableOrderedMap({
|
||||
'9012': ImmutableMap({ id: '9012' }),
|
||||
'5678': ImmutableMap({ id: '5678' }),
|
||||
'1234': ImmutableMap({ id: '1234' }),
|
||||
}),
|
||||
unread: 3,
|
||||
});
|
||||
|
||||
expect(reducer(state, action).get('unread')).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,131 +0,0 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import {
|
||||
SEARCH_CHANGE,
|
||||
SEARCH_CLEAR,
|
||||
SEARCH_EXPAND_SUCCESS,
|
||||
} from 'soapbox/actions/search';
|
||||
|
||||
import reducer from '../search';
|
||||
|
||||
describe('search reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {}).toJS()).toEqual({
|
||||
value: '',
|
||||
submitted: false,
|
||||
submittedValue: '',
|
||||
hidden: false,
|
||||
results: {
|
||||
accounts: [],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
accountsHasMore: false,
|
||||
statusesHasMore: false,
|
||||
hashtagsHasMore: false,
|
||||
accountsLoaded: false,
|
||||
statusesLoaded: false,
|
||||
hashtagsLoaded: false,
|
||||
},
|
||||
filter: 'accounts',
|
||||
accountId: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEARCH_CHANGE', () => {
|
||||
it('sets the value', () => {
|
||||
const state = ImmutableMap({ value: 'hell' });
|
||||
const action = { type: SEARCH_CHANGE, value: 'hello' };
|
||||
expect(reducer(state, action).get('value')).toEqual('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEARCH_CLEAR', () => {
|
||||
it('resets the state', () => {
|
||||
const state = ImmutableRecord({
|
||||
value: 'hello world',
|
||||
submitted: true,
|
||||
submittedValue: 'hello world',
|
||||
hidden: false,
|
||||
results: ImmutableRecord({})(),
|
||||
filter: 'statuses',
|
||||
})();
|
||||
|
||||
const action = { type: SEARCH_CLEAR };
|
||||
|
||||
const expected = {
|
||||
value: '',
|
||||
submitted: false,
|
||||
submittedValue: '',
|
||||
hidden: false,
|
||||
results: {
|
||||
accounts: [],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
accountsHasMore: false,
|
||||
statusesHasMore: false,
|
||||
hashtagsHasMore: false,
|
||||
accountsLoaded: false,
|
||||
statusesLoaded: false,
|
||||
hashtagsLoaded: false,
|
||||
},
|
||||
filter: 'accounts',
|
||||
accountId: null,
|
||||
};
|
||||
|
||||
expect(reducer(state, action).toJS()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe(SEARCH_EXPAND_SUCCESS, () => {
|
||||
it('imports hashtags as maps', () => {
|
||||
const state = ImmutableRecord({
|
||||
value: 'artist',
|
||||
submitted: true,
|
||||
submittedValue: 'artist',
|
||||
hidden: false,
|
||||
results: ImmutableRecord({
|
||||
hashtags: ImmutableList(),
|
||||
hashtagsHasMore: false,
|
||||
hashtagsLoaded: true,
|
||||
})(),
|
||||
filter: 'hashtags',
|
||||
})();
|
||||
|
||||
const action = {
|
||||
type: SEARCH_EXPAND_SUCCESS,
|
||||
results: {
|
||||
accounts: [],
|
||||
statuses: [],
|
||||
hashtags: [{
|
||||
name: 'artist',
|
||||
url: 'https://gleasonator.com/tags/artist',
|
||||
history: [],
|
||||
}],
|
||||
},
|
||||
searchTerm: 'artist',
|
||||
searchType: 'hashtags',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
value: 'artist',
|
||||
submitted: true,
|
||||
submittedValue: 'artist',
|
||||
hidden: false,
|
||||
results: {
|
||||
hashtags: [
|
||||
{
|
||||
name: 'artist',
|
||||
url: 'https://gleasonator.com/tags/artist',
|
||||
history: [],
|
||||
},
|
||||
],
|
||||
hashtagsHasMore: false,
|
||||
hashtagsLoaded: true,
|
||||
},
|
||||
filter: 'hashtags',
|
||||
};
|
||||
|
||||
expect(reducer(state, action).toJS()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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: {},
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import {
|
||||
GROUP_CREATE_REQUEST,
|
||||
GROUP_CREATE_FAIL,
|
||||
GROUP_CREATE_SUCCESS,
|
||||
GROUP_UPDATE_REQUEST,
|
||||
GROUP_UPDATE_FAIL,
|
||||
GROUP_UPDATE_SUCCESS,
|
||||
GROUP_EDITOR_RESET,
|
||||
GROUP_EDITOR_SETUP,
|
||||
GROUP_EDITOR_VALUE_CHANGE,
|
||||
} from '../actions/group_editor';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
groupId: null,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: '',
|
||||
description: '',
|
||||
coverImage: null,
|
||||
});
|
||||
|
||||
export default function groupEditorReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case GROUP_EDITOR_RESET:
|
||||
return initialState;
|
||||
case GROUP_EDITOR_SETUP:
|
||||
return state.withMutations(map => {
|
||||
map.set('groupId', action.group.get('id'));
|
||||
map.set('title', action.group.get('title'));
|
||||
map.set('description', action.group.get('description'));
|
||||
map.set('isSubmitting', false);
|
||||
});
|
||||
case GROUP_EDITOR_VALUE_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set(action.field, action.value);
|
||||
map.set('isChanged', true);
|
||||
});
|
||||
case GROUP_CREATE_REQUEST:
|
||||
case GROUP_UPDATE_REQUEST:
|
||||
return state.withMutations(map => {
|
||||
map.set('isSubmitting', true);
|
||||
map.set('isChanged', false);
|
||||
});
|
||||
case GROUP_CREATE_FAIL:
|
||||
case GROUP_UPDATE_FAIL:
|
||||
return state.set('isSubmitting', false);
|
||||
case GROUP_CREATE_SUCCESS:
|
||||
case GROUP_UPDATE_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.set('isSubmitting', false);
|
||||
map.set('groupId', action.group.id);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { GROUPS_FETCH_SUCCESS } from '../actions/groups';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
featured: ImmutableList(),
|
||||
member: ImmutableList(),
|
||||
admin: ImmutableList(),
|
||||
});
|
||||
|
||||
const normalizeList = (state, type, id, groups) => {
|
||||
return state.set(type, ImmutableList(groups.map(item => item.id)));
|
||||
};
|
||||
|
||||
export default function groupLists(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case GROUPS_FETCH_SUCCESS:
|
||||
return normalizeList(state, action.tab, action.id, action.groups);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import { GROUP_RELATIONSHIPS_FETCH_SUCCESS, GROUP_JOIN_SUCCESS, GROUP_LEAVE_SUCCESS } from '../actions/groups';
|
||||
|
||||
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
|
||||
|
||||
const normalizeRelationships = (state, relationships) => {
|
||||
relationships.forEach(relationship => {
|
||||
state = normalizeRelationship(state, relationship);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
export default function group_relationships(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case GROUP_JOIN_SUCCESS:
|
||||
case GROUP_LEAVE_SUCCESS:
|
||||
return normalizeRelationship(state, action.relationship);
|
||||
case GROUP_RELATIONSHIPS_FETCH_SUCCESS:
|
||||
return normalizeRelationships(state, action.relationships);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import { GROUP_UPDATE_SUCCESS } from '../actions/group_editor';
|
||||
import {
|
||||
GROUP_FETCH_SUCCESS,
|
||||
GROUP_FETCH_FAIL,
|
||||
GROUPS_FETCH_SUCCESS,
|
||||
} from '../actions/groups';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
const normalizeGroup = (state, group) => state.set(group.id, fromJS(group));
|
||||
|
||||
const normalizeGroups = (state, groups) => {
|
||||
groups.forEach(group => {
|
||||
state = normalizeGroup(state, group);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default function groups(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case GROUP_FETCH_SUCCESS:
|
||||
case GROUP_UPDATE_SUCCESS:
|
||||
return normalizeGroup(state, action.group);
|
||||
case GROUPS_FETCH_SUCCESS:
|
||||
return normalizeGroups(state, action.groups);
|
||||
case GROUP_FETCH_FAIL:
|
||||
return state.set(action.id, false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
.media-gallery {
|
||||
box-sizing: border-box;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
.detailed-status {
|
||||
.reply-mentions {
|
||||
display: block;
|
||||
margin: 4px 0 0 0;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -14,11 +14,6 @@
|
|||
opacity: 1;
|
||||
animation: fade 150ms linear;
|
||||
|
||||
.video-player,
|
||||
.audio-player {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&.light {
|
||||
.display-name {
|
||||
strong {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
z-index: 100;
|
||||
|
|
|
@ -66,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",
|
||||
|
@ -166,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",
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -2271,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"
|
||||
|
@ -9922,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"
|
||||
|
|
Ładowanie…
Reference in New Issue