environments/review-chats-g56n7m/deployments/1301
commit
acede4b519
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,155 +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';
|
|
||||||
|
|
||||||
export default @withRouter
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
|
||||||
|
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import { Stack } from './ui';
|
||||||
|
|
||||||
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface ITranslateButton {
|
||||||
|
status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const me = useAppSelector((state) => state.me);
|
||||||
|
|
||||||
|
const renderTranslate = /* translationEnabled && */ me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||||
|
|
||||||
|
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (status.translation) {
|
||||||
|
dispatch(undoStatusTranslation(status.id));
|
||||||
|
} else {
|
||||||
|
dispatch(translateStatus(status.id, intl.locale));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!features.translations || !renderTranslate) return null;
|
||||||
|
|
||||||
|
if (status.translation) {
|
||||||
|
const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
|
||||||
|
const languageName = languageNames.of(status.language!);
|
||||||
|
const provider = status.translation.get('provider');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className='text-gray-700 dark:text-gray-600 text-sm' alignItems='start'>
|
||||||
|
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||||
|
|
||||||
|
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
|
||||||
|
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||||
|
</button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
|
||||||
|
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TranslateButton;
|
@ -1,51 +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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
@ -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;
|
@ -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;
|
@ -1,55 +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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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,192 +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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,60 +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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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,55 +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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
@ -0,0 +1,154 @@
|
|||||||
|
import classNames from 'clsx';
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
||||||
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
||||||
|
emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' },
|
||||||
|
custom: { id: 'icon_button.icons', defaultMessage: 'Icons' },
|
||||||
|
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const backgroundImageFn = () => '';
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
const categoriesSort = ['custom'];
|
||||||
|
|
||||||
|
|
||||||
|
class IconPickerMenu extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
custom_emojis: PropTypes.object,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onPick: PropTypes.func.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
placement: PropTypes.string,
|
||||||
|
arrowOffsetLeft: PropTypes.string,
|
||||||
|
arrowOffsetTop: PropTypes.string,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
style: {},
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
modifierOpen: false,
|
||||||
|
placement: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDocumentClick = e => {
|
||||||
|
if (this.node && !this.node.contains(e.target)) {
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||||
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
|
||||||
|
if (!c) return;
|
||||||
|
|
||||||
|
// Nice and dirty hack to display the icons
|
||||||
|
c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => {
|
||||||
|
const newIcon = document.createElement('span');
|
||||||
|
newIcon.innerHTML = `<i class="fa fa-${elem.parentNode.getAttribute('title')} fa-hack"></i>`;
|
||||||
|
elem.parentNode.replaceChild(newIcon, elem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getI18n = () => {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return {
|
||||||
|
search: intl.formatMessage(messages.emoji_search),
|
||||||
|
notfound: intl.formatMessage(messages.emoji_not_found),
|
||||||
|
categories: {
|
||||||
|
search: intl.formatMessage(messages.search_results),
|
||||||
|
custom: intl.formatMessage(messages.custom),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = emoji => {
|
||||||
|
emoji.native = emoji.colons;
|
||||||
|
|
||||||
|
this.props.onClose();
|
||||||
|
this.props.onPick(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildIcons = (customEmojis, autoplay = false) => {
|
||||||
|
const emojis = [];
|
||||||
|
|
||||||
|
Object.values(customEmojis).forEach(category => {
|
||||||
|
category.forEach(function(icon) {
|
||||||
|
const name = icon.replace('fa fa-', '');
|
||||||
|
if (icon !== 'email' && icon !== 'memo') {
|
||||||
|
emojis.push({
|
||||||
|
id: name,
|
||||||
|
name,
|
||||||
|
short_names: [name],
|
||||||
|
emoticons: [],
|
||||||
|
keywords: [name],
|
||||||
|
imageUrl: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return emojis;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, style, intl, custom_emojis } = this.props;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div style={{ width: 299 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = { compressed: true, categories: [], aliases: [], emojis: [] };
|
||||||
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
const { modifierOpen } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('font-icon-picker emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||||
|
<Picker
|
||||||
|
perLine={8}
|
||||||
|
emojiSize={22}
|
||||||
|
include={categoriesSort}
|
||||||
|
sheetSize={32}
|
||||||
|
custom={this.buildIcons(custom_emojis)}
|
||||||
|
color=''
|
||||||
|
emoji=''
|
||||||
|
set=''
|
||||||
|
title={title}
|
||||||
|
i18n={this.getI18n()}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
showPreview={false}
|
||||||
|
backgroundImageFn={backgroundImageFn}
|
||||||
|
emojiTooltip
|
||||||
|
noShowAnchors
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(IconPickerMenu);
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
||||||
|
};
|
Loading…
Reference in new issue