Merge remote-tracking branch 'soapbox/develop' into events-
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
7b2193d753
|
@ -0,0 +1,18 @@
|
||||||
|
FROM node:18
|
||||||
|
|
||||||
|
RUN apt-get update &&\
|
||||||
|
apt-get install -y inotify-tools &&\
|
||||||
|
# clean up apt
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
COPY yarn.lock .
|
||||||
|
RUN yarn
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV DEVSERVER_URL=http://0.0.0.0:3036
|
||||||
|
CMD yarn dev
|
|
@ -8,7 +8,6 @@
|
||||||
<meta name="referrer" content="same-origin" />
|
<meta name="referrer" content="same-origin" />
|
||||||
<link href="/manifest.json" rel="manifest">
|
<link href="/manifest.json" rel="manifest">
|
||||||
<!--server-generated-meta-->
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
|
||||||
<%= snippets %>
|
<%= snippets %>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-mode-light no-reduce-motion">
|
<body class="theme-mode-light no-reduce-motion">
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
{
|
||||||
|
"approval_required": false,
|
||||||
|
"avatar_upload_limit": 2000000,
|
||||||
|
"background_image": "https://fe.disroot.org/images/city.jpg",
|
||||||
|
"background_upload_limit": 4000000,
|
||||||
|
"banner_upload_limit": 4000000,
|
||||||
|
"description": "FEDIsroot - Federated social network powered by Pleroma (open beta)",
|
||||||
|
"description_limit": 5000,
|
||||||
|
"email": "admin@example.lan",
|
||||||
|
"languages": [
|
||||||
|
"en"
|
||||||
|
],
|
||||||
|
"max_toot_chars": 5000,
|
||||||
|
"pleroma": {
|
||||||
|
"metadata": {
|
||||||
|
"account_activation_required": false,
|
||||||
|
"features": [
|
||||||
|
"pleroma_api",
|
||||||
|
"akkoma_api",
|
||||||
|
"mastodon_api",
|
||||||
|
"mastodon_api_streaming",
|
||||||
|
"polls",
|
||||||
|
"v2_suggestions",
|
||||||
|
"pleroma_explicit_addressing",
|
||||||
|
"shareable_emoji_packs",
|
||||||
|
"multifetch",
|
||||||
|
"pleroma:api/v1/notifications:include_types_filter",
|
||||||
|
"editing",
|
||||||
|
"media_proxy",
|
||||||
|
"relay",
|
||||||
|
"pleroma_emoji_reactions",
|
||||||
|
"exposable_reactions",
|
||||||
|
"profile_directory",
|
||||||
|
"custom_emoji_reactions",
|
||||||
|
"pleroma:get:main/ostatus"
|
||||||
|
],
|
||||||
|
"federation": {
|
||||||
|
"enabled": true,
|
||||||
|
"exclusions": false,
|
||||||
|
"mrf_hashtag": {
|
||||||
|
"federated_timeline_removal": [],
|
||||||
|
"reject": [],
|
||||||
|
"sensitive": [
|
||||||
|
"nsfw"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mrf_object_age": {
|
||||||
|
"actions": [
|
||||||
|
"delist",
|
||||||
|
"strip_followers"
|
||||||
|
],
|
||||||
|
"threshold": 604800
|
||||||
|
},
|
||||||
|
"mrf_policies": [
|
||||||
|
"ObjectAgePolicy",
|
||||||
|
"TagPolicy",
|
||||||
|
"HashtagPolicy",
|
||||||
|
"InlineQuotePolicy"
|
||||||
|
],
|
||||||
|
"quarantined_instances": [],
|
||||||
|
"quarantined_instances_info": {
|
||||||
|
"quarantined_instances": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields_limits": {
|
||||||
|
"max_fields": 10,
|
||||||
|
"max_remote_fields": 20,
|
||||||
|
"name_length": 512,
|
||||||
|
"value_length": 2048
|
||||||
|
},
|
||||||
|
"post_formats": [
|
||||||
|
"text/plain",
|
||||||
|
"text/html",
|
||||||
|
"text/markdown",
|
||||||
|
"text/bbcode",
|
||||||
|
"text/x.misskeymarkdown"
|
||||||
|
],
|
||||||
|
"privileged_staff": false
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"mau": 83
|
||||||
|
},
|
||||||
|
"vapid_public_key": null
|
||||||
|
},
|
||||||
|
"poll_limits": {
|
||||||
|
"max_expiration": 31536000,
|
||||||
|
"max_option_chars": 200,
|
||||||
|
"max_options": 20,
|
||||||
|
"min_expiration": 0
|
||||||
|
},
|
||||||
|
"registrations": false,
|
||||||
|
"stats": {
|
||||||
|
"domain_count": 6972,
|
||||||
|
"status_count": 8081,
|
||||||
|
"user_count": 357
|
||||||
|
},
|
||||||
|
"thumbnail": "https://fe.disroot.org/instance/thumbnail.jpeg",
|
||||||
|
"title": "FEDIsroot",
|
||||||
|
"upload_limit": 16000000,
|
||||||
|
"uri": "https://fe.disroot.org",
|
||||||
|
"urls": {
|
||||||
|
"streaming_api": "wss://fe.disroot.org"
|
||||||
|
},
|
||||||
|
"version": "2.7.2 (compatible; Akkoma 3.3.1-0-gaf90a4e51)"
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
@ -359,14 +359,30 @@ const unblockAccountFail = (error: AxiosError) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
const muteAccount = (id: string, notifications?: boolean) =>
|
const muteAccount = (id: string, notifications?: boolean, duration = 0) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return null;
|
if (!isLoggedIn(getState)) return null;
|
||||||
|
|
||||||
dispatch(muteAccountRequest(id));
|
dispatch(muteAccountRequest(id));
|
||||||
|
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
notifications,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (duration) {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.instance;
|
||||||
|
const v = parseVersion(instance.version);
|
||||||
|
|
||||||
|
if (v.software === PLEROMA) {
|
||||||
|
params.expires_in = duration;
|
||||||
|
} else {
|
||||||
|
params.duration = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api(getState)
|
return api(getState)
|
||||||
.post(`/api/v1/accounts/${id}/mute`, { notifications })
|
.post(`/api/v1/accounts/${id}/mute`, params)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||||
return dispatch(muteAccountSuccess(response.data, getState().statuses));
|
return dispatch(muteAccountSuccess(response.data, getState().statuses));
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { httpErrorMessages } from 'soapbox/utils/errors';
|
||||||
import type { SnackbarActionSeverity } from './snackbar';
|
import type { SnackbarActionSeverity } from './snackbar';
|
||||||
import type { AnyAction } from '@reduxjs/toolkit';
|
import type { AnyAction } from '@reduxjs/toolkit';
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
import type { NotificationObject } from 'soapbox/react-notification';
|
import type { NotificationObject } from 'react-notification';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
|
|
|
@ -55,7 +55,6 @@ const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
||||||
|
|
||||||
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
||||||
|
|
||||||
const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
|
||||||
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||||
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
||||||
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||||
|
@ -601,11 +600,6 @@ const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], te
|
||||||
dispatch(updateTagHistory(composeId, newHistory));
|
dispatch(updateTagHistory(composeId, newHistory));
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeComposeSensitivity = (composeId: string) => ({
|
|
||||||
type: COMPOSE_SENSITIVITY_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeComposeSpoilerness = (composeId: string) => ({
|
const changeComposeSpoilerness = (composeId: string) => ({
|
||||||
type: COMPOSE_SPOILERNESS_CHANGE,
|
type: COMPOSE_SPOILERNESS_CHANGE,
|
||||||
id: composeId,
|
id: composeId,
|
||||||
|
@ -758,7 +752,6 @@ export {
|
||||||
COMPOSE_SUGGESTION_SELECT,
|
COMPOSE_SUGGESTION_SELECT,
|
||||||
COMPOSE_SUGGESTION_TAGS_UPDATE,
|
COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||||
COMPOSE_TAG_HISTORY_UPDATE,
|
COMPOSE_TAG_HISTORY_UPDATE,
|
||||||
COMPOSE_SENSITIVITY_CHANGE,
|
|
||||||
COMPOSE_SPOILERNESS_CHANGE,
|
COMPOSE_SPOILERNESS_CHANGE,
|
||||||
COMPOSE_TYPE_CHANGE,
|
COMPOSE_TYPE_CHANGE,
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
|
@ -813,7 +806,6 @@ export {
|
||||||
selectComposeSuggestion,
|
selectComposeSuggestion,
|
||||||
updateSuggestionTags,
|
updateSuggestionTags,
|
||||||
updateTagHistory,
|
updateTagHistory,
|
||||||
changeComposeSensitivity,
|
|
||||||
changeComposeSpoilerness,
|
changeComposeSpoilerness,
|
||||||
changeComposeContentType,
|
changeComposeContentType,
|
||||||
changeComposeSpoilerText,
|
changeComposeSpoilerText,
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
|
||||||
|
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
|
||||||
import type { History } from 'history';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
|
|
||||||
const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
|
|
||||||
const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
|
|
||||||
|
|
||||||
const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
|
|
||||||
const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
|
|
||||||
const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
|
|
||||||
|
|
||||||
const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE';
|
|
||||||
const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET';
|
|
||||||
const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP';
|
|
||||||
|
|
||||||
const submit = (routerHistory: History) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const groupId = getState().group_editor.get('groupId') as string;
|
|
||||||
const title = getState().group_editor.get('title') as string;
|
|
||||||
const description = getState().group_editor.get('description') as string;
|
|
||||||
const coverImage = getState().group_editor.get('coverImage') as any;
|
|
||||||
|
|
||||||
if (groupId === null) {
|
|
||||||
dispatch(create(title, description, coverImage, routerHistory));
|
|
||||||
} else {
|
|
||||||
dispatch(update(groupId, title, description, coverImage, routerHistory));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const create = (title: string, description: string, coverImage: File, routerHistory: History) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(createRequest());
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('title', title);
|
|
||||||
formData.append('description', description);
|
|
||||||
|
|
||||||
if (coverImage !== null) {
|
|
||||||
formData.append('cover_image', coverImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
|
|
||||||
dispatch(createSuccess(data));
|
|
||||||
routerHistory.push(`/groups/${data.id}`);
|
|
||||||
}).catch(err => dispatch(createFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRequest = (id?: string) => ({
|
|
||||||
type: GROUP_CREATE_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createSuccess = (group: APIEntity) => ({
|
|
||||||
type: GROUP_CREATE_SUCCESS,
|
|
||||||
group,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createFail = (error: AxiosError) => ({
|
|
||||||
type: GROUP_CREATE_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const update = (groupId: string, title: string, description: string, coverImage: File, routerHistory: History) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(updateRequest(groupId));
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('title', title);
|
|
||||||
formData.append('description', description);
|
|
||||||
|
|
||||||
if (coverImage !== null) {
|
|
||||||
formData.append('cover_image', coverImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
|
|
||||||
dispatch(updateSuccess(data));
|
|
||||||
routerHistory.push(`/groups/${data.id}`);
|
|
||||||
}).catch(err => dispatch(updateFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRequest = (id: string) => ({
|
|
||||||
type: GROUP_UPDATE_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateSuccess = (group: APIEntity) => ({
|
|
||||||
type: GROUP_UPDATE_SUCCESS,
|
|
||||||
group,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateFail = (error: AxiosError) => ({
|
|
||||||
type: GROUP_UPDATE_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeValue = (field: string, value: string | File) => ({
|
|
||||||
type: GROUP_EDITOR_VALUE_CHANGE,
|
|
||||||
field,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const reset = () => ({
|
|
||||||
type: GROUP_EDITOR_RESET,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setUp = (group: string) => ({
|
|
||||||
type: GROUP_EDITOR_SETUP,
|
|
||||||
group,
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
GROUP_CREATE_REQUEST,
|
|
||||||
GROUP_CREATE_SUCCESS,
|
|
||||||
GROUP_CREATE_FAIL,
|
|
||||||
GROUP_UPDATE_REQUEST,
|
|
||||||
GROUP_UPDATE_SUCCESS,
|
|
||||||
GROUP_UPDATE_FAIL,
|
|
||||||
GROUP_EDITOR_VALUE_CHANGE,
|
|
||||||
GROUP_EDITOR_RESET,
|
|
||||||
GROUP_EDITOR_SETUP,
|
|
||||||
submit,
|
|
||||||
create,
|
|
||||||
createRequest,
|
|
||||||
createSuccess,
|
|
||||||
createFail,
|
|
||||||
update,
|
|
||||||
updateRequest,
|
|
||||||
updateSuccess,
|
|
||||||
updateFail,
|
|
||||||
changeValue,
|
|
||||||
reset,
|
|
||||||
setUp,
|
|
||||||
};
|
|
|
@ -1,550 +0,0 @@
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts';
|
|
||||||
import { importFetchedAccounts } from './importer';
|
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
|
|
||||||
const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
|
|
||||||
const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
|
|
||||||
|
|
||||||
const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
|
|
||||||
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
|
|
||||||
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
|
|
||||||
|
|
||||||
const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST';
|
|
||||||
const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS';
|
|
||||||
const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL';
|
|
||||||
|
|
||||||
const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST';
|
|
||||||
const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS';
|
|
||||||
const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL';
|
|
||||||
|
|
||||||
const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
|
|
||||||
const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
|
|
||||||
const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
|
|
||||||
|
|
||||||
const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST';
|
|
||||||
const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS';
|
|
||||||
const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL';
|
|
||||||
|
|
||||||
const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST';
|
|
||||||
const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS';
|
|
||||||
const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL';
|
|
||||||
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL';
|
|
||||||
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL';
|
|
||||||
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL';
|
|
||||||
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS';
|
|
||||||
const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL';
|
|
||||||
|
|
||||||
const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST';
|
|
||||||
const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
|
|
||||||
const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
|
|
||||||
|
|
||||||
const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(fetchGroupRelationships([id]));
|
|
||||||
|
|
||||||
if (getState().groups.get(id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(fetchGroupRequest(id));
|
|
||||||
|
|
||||||
api(getState).get(`/api/v1/groups/${id}`)
|
|
||||||
.then(({ data }) => dispatch(fetchGroupSuccess(data)))
|
|
||||||
.catch(err => dispatch(fetchGroupFail(id, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchGroupRequest = (id: string) => ({
|
|
||||||
type: GROUP_FETCH_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroupSuccess = (group: APIEntity) => ({
|
|
||||||
type: GROUP_FETCH_SUCCESS,
|
|
||||||
group,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroupFail = (id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_FETCH_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroupRelationships = (groupIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
const loadedRelationships = getState().group_relationships;
|
|
||||||
const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null);
|
|
||||||
|
|
||||||
if (newGroupIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(fetchGroupRelationshipsRequest(newGroupIds));
|
|
||||||
|
|
||||||
api(getState).get(`/api/v1/groups/${newGroupIds[0]}/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
|
||||||
dispatch(fetchGroupRelationshipsSuccess(response.data));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(fetchGroupRelationshipsFail(error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchGroupRelationshipsRequest = (ids: string[]) => ({
|
|
||||||
type: GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
|
||||||
ids,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({
|
|
||||||
type: GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
|
||||||
relationships,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroupRelationshipsFail = (error: AxiosError) => ({
|
|
||||||
type: GROUP_RELATIONSHIPS_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroups = (tab: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(fetchGroupsRequest());
|
|
||||||
|
|
||||||
api(getState).get('/api/v1/groups?tab=' + tab)
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch(fetchGroupsSuccess(data, tab));
|
|
||||||
dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id)));
|
|
||||||
})
|
|
||||||
.catch(err => dispatch(fetchGroupsFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchGroupsRequest = () => ({
|
|
||||||
type: GROUPS_FETCH_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroupsSuccess = (groups: APIEntity[], tab: string) => ({
|
|
||||||
type: GROUPS_FETCH_SUCCESS,
|
|
||||||
groups,
|
|
||||||
tab,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchGroupsFail = (error: AxiosError) => ({
|
|
||||||
type: GROUPS_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const joinGroup = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(joinGroupRequest(id));
|
|
||||||
|
|
||||||
api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => {
|
|
||||||
dispatch(joinGroupSuccess(response.data));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(joinGroupFail(id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const leaveGroup = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(leaveGroupRequest(id));
|
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => {
|
|
||||||
dispatch(leaveGroupSuccess(response.data));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(leaveGroupFail(id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const joinGroupRequest = (id: string) => ({
|
|
||||||
type: GROUP_JOIN_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const joinGroupSuccess = (relationship: APIEntity) => ({
|
|
||||||
type: GROUP_JOIN_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
});
|
|
||||||
|
|
||||||
const joinGroupFail = (id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_JOIN_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const leaveGroupRequest = (id: string) => ({
|
|
||||||
type: GROUP_LEAVE_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const leaveGroupSuccess = (relationship: APIEntity) => ({
|
|
||||||
type: GROUP_LEAVE_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
});
|
|
||||||
|
|
||||||
const leaveGroupFail = (id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_LEAVE_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchMembers = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(fetchMembersRequest(id));
|
|
||||||
|
|
||||||
api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(response.data));
|
|
||||||
dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null));
|
|
||||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(fetchMembersFail(id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMembersRequest = (id: string) => ({
|
|
||||||
type: GROUP_MEMBERS_FETCH_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
|
||||||
type: GROUP_MEMBERS_FETCH_SUCCESS,
|
|
||||||
id,
|
|
||||||
accounts,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchMembersFail = (id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_MEMBERS_FETCH_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandMembers = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
const url = getState().user_lists.groups.get(id)!.next;
|
|
||||||
|
|
||||||
if (url === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(expandMembersRequest(id));
|
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(response.data));
|
|
||||||
dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null));
|
|
||||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(expandMembersFail(id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandMembersRequest = (id: string) => ({
|
|
||||||
type: GROUP_MEMBERS_EXPAND_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
|
||||||
type: GROUP_MEMBERS_EXPAND_SUCCESS,
|
|
||||||
id,
|
|
||||||
accounts,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandMembersFail = (id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_MEMBERS_EXPAND_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchRemovedAccounts = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(fetchRemovedAccountsRequest(id));
|
|
||||||
|
|
||||||
api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(response.data));
|
|
||||||
dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
|
|
||||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(fetchRemovedAccountsFail(id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchRemovedAccountsRequest = (id: string) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
|
|
||||||
id,
|
|
||||||
accounts,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchRemovedAccountsFail = (id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandRemovedAccounts = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
const url = getState().user_lists.groups_removed_accounts.get(id)!.next;
|
|
||||||
|
|
||||||
if (url === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(expandRemovedAccountsRequest(id));
|
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(response.data));
|
|
||||||
dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
|
|
||||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(expandRemovedAccountsFail(id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandRemovedAccountsRequest = (id: string) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
|
|
||||||
id,
|
|
||||||
accounts,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandRemovedAccountsFail = (id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeRemovedAccount = (groupId: string, id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(removeRemovedAccountRequest(groupId, id));
|
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
|
|
||||||
dispatch(removeRemovedAccountSuccess(groupId, id));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(removeRemovedAccountFail(groupId, id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeRemovedAccountRequest = (groupId: string, id: string) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeRemovedAccountSuccess = (groupId: string, id: string) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createRemovedAccount = (groupId: string, id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(createRemovedAccountRequest(groupId, id));
|
|
||||||
|
|
||||||
api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
|
|
||||||
dispatch(createRemovedAccountSuccess(groupId, id));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(createRemovedAccountFail(groupId, id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRemovedAccountRequest = (groupId: string, id: string) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createRemovedAccountSuccess = (groupId: string, id: string) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupRemoveStatus = (groupId: string, id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(groupRemoveStatusRequest(groupId, id));
|
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => {
|
|
||||||
dispatch(groupRemoveStatusSuccess(groupId, id));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(groupRemoveStatusFail(groupId, id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupRemoveStatusRequest = (groupId: string, id: string) => ({
|
|
||||||
type: GROUP_REMOVE_STATUS_REQUEST,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupRemoveStatusSuccess = (groupId: string, id: string) => ({
|
|
||||||
type: GROUP_REMOVE_STATUS_SUCCESS,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupRemoveStatusFail = (groupId: string, id: string, error: AxiosError) => ({
|
|
||||||
type: GROUP_REMOVE_STATUS_FAIL,
|
|
||||||
groupId,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
GROUP_FETCH_REQUEST,
|
|
||||||
GROUP_FETCH_SUCCESS,
|
|
||||||
GROUP_FETCH_FAIL,
|
|
||||||
GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
|
||||||
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
|
||||||
GROUP_RELATIONSHIPS_FETCH_FAIL,
|
|
||||||
GROUPS_FETCH_REQUEST,
|
|
||||||
GROUPS_FETCH_SUCCESS,
|
|
||||||
GROUPS_FETCH_FAIL,
|
|
||||||
GROUP_JOIN_REQUEST,
|
|
||||||
GROUP_JOIN_SUCCESS,
|
|
||||||
GROUP_JOIN_FAIL,
|
|
||||||
GROUP_LEAVE_REQUEST,
|
|
||||||
GROUP_LEAVE_SUCCESS,
|
|
||||||
GROUP_LEAVE_FAIL,
|
|
||||||
GROUP_MEMBERS_FETCH_REQUEST,
|
|
||||||
GROUP_MEMBERS_FETCH_SUCCESS,
|
|
||||||
GROUP_MEMBERS_FETCH_FAIL,
|
|
||||||
GROUP_MEMBERS_EXPAND_REQUEST,
|
|
||||||
GROUP_MEMBERS_EXPAND_SUCCESS,
|
|
||||||
GROUP_MEMBERS_EXPAND_FAIL,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
|
|
||||||
GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
|
|
||||||
GROUP_REMOVE_STATUS_REQUEST,
|
|
||||||
GROUP_REMOVE_STATUS_SUCCESS,
|
|
||||||
GROUP_REMOVE_STATUS_FAIL,
|
|
||||||
fetchGroup,
|
|
||||||
fetchGroupRequest,
|
|
||||||
fetchGroupSuccess,
|
|
||||||
fetchGroupFail,
|
|
||||||
fetchGroupRelationships,
|
|
||||||
fetchGroupRelationshipsRequest,
|
|
||||||
fetchGroupRelationshipsSuccess,
|
|
||||||
fetchGroupRelationshipsFail,
|
|
||||||
fetchGroups,
|
|
||||||
fetchGroupsRequest,
|
|
||||||
fetchGroupsSuccess,
|
|
||||||
fetchGroupsFail,
|
|
||||||
joinGroup,
|
|
||||||
leaveGroup,
|
|
||||||
joinGroupRequest,
|
|
||||||
joinGroupSuccess,
|
|
||||||
joinGroupFail,
|
|
||||||
leaveGroupRequest,
|
|
||||||
leaveGroupSuccess,
|
|
||||||
leaveGroupFail,
|
|
||||||
fetchMembers,
|
|
||||||
fetchMembersRequest,
|
|
||||||
fetchMembersSuccess,
|
|
||||||
fetchMembersFail,
|
|
||||||
expandMembers,
|
|
||||||
expandMembersRequest,
|
|
||||||
expandMembersSuccess,
|
|
||||||
expandMembersFail,
|
|
||||||
fetchRemovedAccounts,
|
|
||||||
fetchRemovedAccountsRequest,
|
|
||||||
fetchRemovedAccountsSuccess,
|
|
||||||
fetchRemovedAccountsFail,
|
|
||||||
expandRemovedAccounts,
|
|
||||||
expandRemovedAccountsRequest,
|
|
||||||
expandRemovedAccountsSuccess,
|
|
||||||
expandRemovedAccountsFail,
|
|
||||||
removeRemovedAccount,
|
|
||||||
removeRemovedAccountRequest,
|
|
||||||
removeRemovedAccountSuccess,
|
|
||||||
removeRemovedAccountFail,
|
|
||||||
createRemovedAccount,
|
|
||||||
createRemovedAccountRequest,
|
|
||||||
createRemovedAccountSuccess,
|
|
||||||
createRemovedAccountFail,
|
|
||||||
groupRemoveStatus,
|
|
||||||
groupRemoveStatusRequest,
|
|
||||||
groupRemoveStatusSuccess,
|
|
||||||
groupRemoveStatusFail,
|
|
||||||
};
|
|
|
@ -66,7 +66,5 @@ export const loadInstance = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
|
|
||||||
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
|
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
'nodeinfo/fetch',
|
'nodeinfo/fetch',
|
||||||
async(_arg, { getState }) => {
|
async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'),
|
||||||
return await api(getState).get('/nodeinfo/2.1.json');
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||||
|
|
||||||
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
||||||
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
||||||
|
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
|
||||||
|
|
||||||
const fetchMutes = () =>
|
const fetchMutes = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -103,6 +104,14 @@ const toggleHideNotifications = () =>
|
||||||
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
|
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeMuteDuration = (duration: number) =>
|
||||||
|
(dispatch: AppDispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: MUTES_CHANGE_DURATION,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MUTES_FETCH_REQUEST,
|
MUTES_FETCH_REQUEST,
|
||||||
MUTES_FETCH_SUCCESS,
|
MUTES_FETCH_SUCCESS,
|
||||||
|
@ -112,6 +121,7 @@ export {
|
||||||
MUTES_EXPAND_FAIL,
|
MUTES_EXPAND_FAIL,
|
||||||
MUTES_INIT_MODAL,
|
MUTES_INIT_MODAL,
|
||||||
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||||
|
MUTES_CHANGE_DURATION,
|
||||||
fetchMutes,
|
fetchMutes,
|
||||||
fetchMutesRequest,
|
fetchMutesRequest,
|
||||||
fetchMutesSuccess,
|
fetchMutesSuccess,
|
||||||
|
@ -122,4 +132,5 @@ export {
|
||||||
expandMutesFail,
|
expandMutesFail,
|
||||||
initMuteModal,
|
initMuteModal,
|
||||||
toggleHideNotifications,
|
toggleHideNotifications,
|
||||||
|
changeMuteDuration,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
List as ImmutableList,
|
|
||||||
Map as ImmutableMap,
|
Map as ImmutableMap,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
import IntlMessageFormat from 'intl-messageformat';
|
import IntlMessageFormat from 'intl-messageformat';
|
||||||
|
@ -12,6 +11,7 @@ import { getFilters, regexFromFilters } from 'soapbox/selectors';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||||
import { unescapeHTML } from 'soapbox/utils/html';
|
import { unescapeHTML } from 'soapbox/utils/html';
|
||||||
|
import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static';
|
import { joinPublicPath } from 'soapbox/utils/static';
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
@ -168,11 +168,8 @@ const dequeueNotifications = () =>
|
||||||
dispatch(markReadNotifications());
|
dispatch(markReadNotifications());
|
||||||
};
|
};
|
||||||
|
|
||||||
// const excludeTypesFromSettings = (getState: () => RootState) => (getSettings(getState()).getIn(['notifications', 'shows']) as ImmutableMap<string, boolean>).filter(enabled => !enabled).keySeq().toJS();
|
|
||||||
|
|
||||||
const excludeTypesFromFilter = (filter: string) => {
|
const excludeTypesFromFilter = (filter: string) => {
|
||||||
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'status', 'poll', 'move', 'pleroma:emoji_reaction']);
|
return NOTIFICATION_TYPES.filter(item => item !== filter);
|
||||||
return allTypes.filterNot(item => item === filter).toJS();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const noOp = () => new Promise(f => f(undefined));
|
const noOp = () => new Promise(f => f(undefined));
|
||||||
|
@ -182,6 +179,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
||||||
if (!isLoggedIn(getState)) return dispatch(noOp);
|
if (!isLoggedIn(getState)) return dispatch(noOp);
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const features = getFeatures(state.instance);
|
||||||
const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as string;
|
const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as string;
|
||||||
const notifications = state.notifications;
|
const notifications = state.notifications;
|
||||||
const isLoadingMore = !!maxId;
|
const isLoadingMore = !!maxId;
|
||||||
|
@ -195,10 +193,13 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (activeFilter !== 'all') {
|
if (activeFilter === 'all') {
|
||||||
const instance = state.instance;
|
if (features.notificationsIncludeTypes) {
|
||||||
const features = getFeatures(instance);
|
params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any));
|
||||||
|
} else {
|
||||||
|
params.exclude_types = EXCLUDE_TYPES;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (features.notificationsIncludeTypes) {
|
if (features.notificationsIncludeTypes) {
|
||||||
params.types = [activeFilter];
|
params.types = [activeFilter];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -43,6 +43,11 @@ const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
||||||
const STATUS_REVEAL = 'STATUS_REVEAL';
|
const STATUS_REVEAL = 'STATUS_REVEAL';
|
||||||
const STATUS_HIDE = 'STATUS_HIDE';
|
const STATUS_HIDE = 'STATUS_HIDE';
|
||||||
|
|
||||||
|
const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
|
||||||
|
const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
||||||
|
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||||
|
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||||
|
|
||||||
const statusExists = (getState: () => RootState, statusId: string) => {
|
const statusExists = (getState: () => RootState, statusId: string) => {
|
||||||
return (getState().statuses.get(statusId) || null) !== null;
|
return (getState().statuses.get(statusId) || null) !== null;
|
||||||
};
|
};
|
||||||
|
@ -305,6 +310,31 @@ const toggleStatusHidden = (status: Status) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const translateStatus = (id: string, targetLanguage?: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: STATUS_TRANSLATE_REQUEST, id });
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/statuses/${id}/translate`, {
|
||||||
|
target_language: targetLanguage,
|
||||||
|
}).then(response => {
|
||||||
|
dispatch({
|
||||||
|
type: STATUS_TRANSLATE_SUCCESS,
|
||||||
|
id,
|
||||||
|
translation: response.data,
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({
|
||||||
|
type: STATUS_TRANSLATE_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const undoStatusTranslation = (id: string) => ({
|
||||||
|
type: STATUS_TRANSLATE_UNDO,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
STATUS_CREATE_REQUEST,
|
STATUS_CREATE_REQUEST,
|
||||||
STATUS_CREATE_SUCCESS,
|
STATUS_CREATE_SUCCESS,
|
||||||
|
@ -329,6 +359,10 @@ export {
|
||||||
STATUS_UNMUTE_FAIL,
|
STATUS_UNMUTE_FAIL,
|
||||||
STATUS_REVEAL,
|
STATUS_REVEAL,
|
||||||
STATUS_HIDE,
|
STATUS_HIDE,
|
||||||
|
STATUS_TRANSLATE_REQUEST,
|
||||||
|
STATUS_TRANSLATE_SUCCESS,
|
||||||
|
STATUS_TRANSLATE_FAIL,
|
||||||
|
STATUS_TRANSLATE_UNDO,
|
||||||
createStatus,
|
createStatus,
|
||||||
editStatus,
|
editStatus,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
|
@ -345,4 +379,6 @@ export {
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
toggleStatusHidden,
|
toggleStatusHidden,
|
||||||
|
translateStatus,
|
||||||
|
undoStatusTranslation,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen, rootState } from '../../jest/test-helpers';
|
||||||
|
import { normalizeStatus, normalizeAccount } from '../../normalizers';
|
||||||
|
import Status from '../status';
|
||||||
|
|
||||||
|
import type { ReducerStatus } from 'soapbox/reducers/statuses';
|
||||||
|
|
||||||
|
const account = normalizeAccount({
|
||||||
|
id: '1',
|
||||||
|
acct: 'alex',
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = normalizeStatus({
|
||||||
|
id: '1',
|
||||||
|
account,
|
||||||
|
content: 'hello world',
|
||||||
|
contentHtml: 'hello world',
|
||||||
|
}) as ReducerStatus;
|
||||||
|
|
||||||
|
describe('<Status />', () => {
|
||||||
|
const state = rootState.setIn(['accounts', '1'], account);
|
||||||
|
|
||||||
|
it('renders content', () => {
|
||||||
|
render(<Status status={status} />, undefined, state);
|
||||||
|
screen.getByText(/hello world/i);
|
||||||
|
expect(screen.getByTestId('status')).toHaveTextContent(/hello world/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the Status Action Bar', () => {
|
||||||
|
it('is rendered', () => {
|
||||||
|
render(<Status status={status} />, undefined, state);
|
||||||
|
expect(screen.getByTestId('status-action-bar')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is not rendered if status is under review', () => {
|
||||||
|
const inReviewStatus = normalizeStatus({ ...status, visibility: 'self' });
|
||||||
|
render(<Status status={inReviewStatus as ReducerStatus} />, undefined, state);
|
||||||
|
expect(screen.queryAllByTestId('status-action-bar')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -22,7 +22,13 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account }) => {
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler = (e) => {
|
const handleClick: React.MouseEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
history.push(`/timeline/${account.domain}`);
|
|
||||||
|
const timelineUrl = `/timeline/${account.domain}`;
|
||||||
|
if (!(e.ctrlKey || e.metaKey)) {
|
||||||
|
history.push(timelineUrl);
|
||||||
|
} else {
|
||||||
|
window.open(timelineUrl, '_blank');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -221,7 +227,7 @@ const Account = ({
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
||||||
{timestampUrl ? (
|
{timestampUrl ? (
|
||||||
<Link to={timestampUrl} className='hover:underline'>
|
<Link to={timestampUrl} className='hover:underline' onClick={(event) => event.stopPropagation()}>
|
||||||
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
|
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
@ -237,6 +243,14 @@ const Account = ({
|
||||||
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
|
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{actionType === 'muting' && account.mute_expires_at ? (
|
||||||
|
<>
|
||||||
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
||||||
|
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{note ? (
|
{note ? (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Portal from '@reach/portal';
|
import { Portal } from '@reach/portal';
|
||||||
import classNames from 'clsx';
|
import classNames from 'clsx';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -45,7 +45,7 @@ const textAtCursorMatchesToken = (str: string, caretPosition: number, searchToke
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
||||||
value: string,
|
value: string,
|
||||||
suggestions: ImmutableList<any>,
|
suggestions: ImmutableList<any>,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Portal from '@reach/portal';
|
import { Portal } from '@reach/portal';
|
||||||
import classNames from 'clsx';
|
import classNames from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -14,7 +14,6 @@ import SubNavigation from 'soapbox/components/sub_navigation';
|
||||||
// hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
// hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||||
// });
|
// });
|
||||||
|
|
||||||
export default @withRouter
|
|
||||||
class ColumnHeader extends React.PureComponent {
|
class ColumnHeader extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -126,3 +125,5 @@ class ColumnHeader extends React.PureComponent {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withRouter(ColumnHeader);
|
|
@ -143,12 +143,14 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
||||||
|
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
|
|
||||||
if (typeof action === 'function') {
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
|
||||||
action(e);
|
if (to) {
|
||||||
} else if (to) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.history.push(to);
|
this.props.history.push(to);
|
||||||
|
} else if (typeof action === 'function') {
|
||||||
|
e.preventDefault();
|
||||||
|
action(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,6 +194,7 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
||||||
data-index={i}
|
data-index={i}
|
||||||
target={newTab ? '_blank' : undefined}
|
target={newTab ? '_blank' : undefined}
|
||||||
data-method={isLogout ? 'delete' : undefined}
|
data-method={isLogout ? 'delete' : undefined}
|
||||||
|
title={text}
|
||||||
>
|
>
|
||||||
{icon && <SvgIcon src={icon} className='mr-3 h-5 w-5 flex-none' />}
|
{icon && <SvgIcon src={icon} className='mr-3 h-5 w-5 flex-none' />}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Text, Stack } from 'soapbox/components/ui';
|
||||||
import { captureException } from 'soapbox/monitoring';
|
import { captureException } from 'soapbox/monitoring';
|
||||||
import KVStore from 'soapbox/storage/kv_store';
|
import KVStore from 'soapbox/storage/kv_store';
|
||||||
import sourceCode from 'soapbox/utils/code';
|
import sourceCode from 'soapbox/utils/code';
|
||||||
|
import { unregisterSw } from 'soapbox/utils/sw';
|
||||||
|
|
||||||
import SiteLogo from './site-logo';
|
import SiteLogo from './site-logo';
|
||||||
|
|
||||||
|
@ -15,16 +16,6 @@ import type { RootState } from 'soapbox/store';
|
||||||
|
|
||||||
const goHome = () => location.href = '/';
|
const goHome = () => location.href = '/';
|
||||||
|
|
||||||
/** Unregister the ServiceWorker */
|
|
||||||
// https://stackoverflow.com/a/49771828/8811886
|
|
||||||
const unregisterSw = async(): Promise<void> => {
|
|
||||||
if (navigator.serviceWorker) {
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
||||||
const unregisterAll = registrations.map(r => r.unregister());
|
|
||||||
await Promise.all(unregisterAll);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state: RootState) => {
|
const mapStateToProps = (state: RootState) => {
|
||||||
const { links, logo } = getSoapboxConfig(state);
|
const { links, logo } = getSoapboxConfig(state);
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -47,12 +47,13 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
|
|
||||||
const isEditing = useAppSelector(state => state.compose.get('compose-modal')?.id !== null);
|
const isEditing = useAppSelector(state => state.compose.get('compose-modal')?.id !== null);
|
||||||
|
|
||||||
const handleKeyUp = useCallback((e) => {
|
const visible = !!children;
|
||||||
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
|
|
||||||
&& !!children) {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) {
|
||||||
handleOnClose();
|
handleOnClose();
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
|
@ -136,6 +137,8 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
window.addEventListener('keyup', handleKeyUp, false);
|
window.addEventListener('keyup', handleKeyUp, false);
|
||||||
window.addEventListener('keydown', handleKeyDown, false);
|
window.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
|
||||||
|
@ -143,7 +146,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [visible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!children && !prevChildren) {
|
if (!!children && !prevChildren) {
|
||||||
|
@ -172,8 +175,6 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const visible = !!children;
|
|
||||||
|
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return (
|
return (
|
||||||
<div className='z-50 transition-all' ref={ref} style={{ opacity: 0 }} />
|
<div className='z-50 transition-all' ref={ref} style={{ opacity: 0 }} />
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import classNames from 'clsx';
|
import classNames from 'clsx';
|
||||||
import React, { useState } from 'react';
|
import React, { MouseEventHandler, useState } from 'react';
|
||||||
import { defineMessages, useIntl, FormattedMessage, FormattedList } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import StatusMedia from 'soapbox/components/status-media';
|
import StatusMedia from 'soapbox/components/status-media';
|
||||||
import { Stack, Text } from 'soapbox/components/ui';
|
import { Stack } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import { useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
||||||
|
|
||||||
import EventPreview from './event-preview';
|
import EventPreview from './event-preview';
|
||||||
import OutlineBox from './outline-box';
|
import OutlineBox from './outline-box';
|
||||||
|
import StatusReplyMentions from './status-reply-mentions';
|
||||||
|
import StatusContent from './status_content';
|
||||||
|
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||||
|
|
||||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
@ -37,12 +40,17 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
||||||
|
|
||||||
const [showMedia, setShowMedia] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
|
const [showMedia, setShowMedia] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
|
||||||
|
|
||||||
const handleExpandClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleExpandClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
const account = status.account as AccountEntity;
|
const account = status.account as AccountEntity;
|
||||||
|
|
||||||
if (!compose && e.button === 0) {
|
if (!compose && e.button === 0) {
|
||||||
history.push(`/@${account.acct}/posts/${status.id}`);
|
const statusUrl = `/@${account.acct}/posts/${status.id}`;
|
||||||
|
if (!(e.ctrlKey || e.metaKey)) {
|
||||||
|
history.push(statusUrl);
|
||||||
|
} else {
|
||||||
|
window.open(statusUrl, '_blank');
|
||||||
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -58,57 +66,6 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
||||||
setShowMedia(!showMedia);
|
setShowMedia(!showMedia);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderReplyMentions = () => {
|
|
||||||
if (!status?.in_reply_to_id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = status.account as AccountEntity;
|
|
||||||
const to = status.mentions || [];
|
|
||||||
|
|
||||||
if (to.size === 0) {
|
|
||||||
if (status.in_reply_to_account_id === account.id) {
|
|
||||||
return (
|
|
||||||
<div className='reply-mentions'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='reply_mentions.reply'
|
|
||||||
defaultMessage='Replying to {accounts}'
|
|
||||||
values={{
|
|
||||||
accounts: `@${account.username}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className='reply-mentions'>
|
|
||||||
<FormattedMessage id='reply_mentions.reply_empty' defaultMessage='Replying to post' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const accounts = to.slice(0, 2).map(account => <>@{account.username}</>).toArray();
|
|
||||||
|
|
||||||
if (to.size > 2) {
|
|
||||||
accounts.push(
|
|
||||||
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='reply-mentions'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='reply_mentions.reply'
|
|
||||||
defaultMessage='Replying to {accounts}'
|
|
||||||
values={{
|
|
||||||
accounts: <FormattedList type='conjunction' value={accounts} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -128,7 +85,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
||||||
return (
|
return (
|
||||||
<OutlineBox
|
<OutlineBox
|
||||||
data-testid='quoted-status'
|
data-testid='quoted-status'
|
||||||
className={classNames('mt-3 cursor-pointer', {
|
className={classNames('cursor-pointer', {
|
||||||
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
|
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -145,23 +102,37 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
||||||
withLinkToProfile={!compose}
|
withLinkToProfile={!compose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{renderReplyMentions()}
|
<StatusReplyMentions status={status} hoverable={false} />
|
||||||
|
|
||||||
{status.event ? <EventPreview status={status} hideAction /> : (
|
{status.event ? <EventPreview status={status} hideAction /> : (
|
||||||
<>
|
<Stack className={classNames('relative', {
|
||||||
<Text
|
'min-h-[220px]': status.hidden,
|
||||||
className='break-words status__content status__content--quote'
|
})}
|
||||||
size='sm'
|
>
|
||||||
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
|
{(status.hidden) && (
|
||||||
/>
|
<SensitiveContentOverlay
|
||||||
|
status={status}
|
||||||
|
visible={showMedia}
|
||||||
|
onToggleVisibility={handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<StatusMedia
|
<Stack space={4}>
|
||||||
status={status}
|
<StatusContent
|
||||||
muted={compose}
|
status={status}
|
||||||
showMedia={showMedia}
|
collapsable
|
||||||
onToggleVisibility={handleToggleMediaVisibility}
|
/>
|
||||||
/>
|
|
||||||
</>
|
{(status.card || status.media_attachments.size > 0) && (
|
||||||
|
<StatusMedia
|
||||||
|
status={status}
|
||||||
|
muted={compose}
|
||||||
|
showMedia={showMedia}
|
||||||
|
onToggleVisibility={handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</OutlineBox>
|
</OutlineBox>
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
|
|
||||||
export default class SettingText extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
|
||||||
settingKey: PropTypes.array.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange = (e) => {
|
|
||||||
this.props.onChange(this.props.settingKey, e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { settings, settingKey, label } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label>
|
|
||||||
<span style={{ display: 'none' }}>{label}</span>
|
|
||||||
<input
|
|
||||||
className='setting-text'
|
|
||||||
value={settings.getIn(settingKey)}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
placeholder={label}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -37,16 +37,17 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rounded-full group text-gray-600 hover:text-primary-600 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-100 dark:hover:bg-primary-700': true,
|
'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rounded-full group text-gray-600 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-200 dark:hover:bg-primary-900': true,
|
||||||
'dark:text-gray-100 text-primary-600': isActive,
|
'dark:text-gray-100 text-gray-900': isActive,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span className='relative'>
|
<span className='relative'>
|
||||||
<Icon
|
<Icon
|
||||||
src={icon}
|
src={icon}
|
||||||
count={count}
|
count={count}
|
||||||
className={classNames('h-5 w-5 group-hover:text-primary-500', {
|
className={classNames('h-5 w-5', {
|
||||||
'text-primary-500': isActive,
|
'text-gray-600 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400': !isActive,
|
||||||
|
'text-primary-500 dark:text-primary-400': isActive,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -281,12 +281,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleCopy: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
const { uri } = status;
|
const { uri } = status;
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
textarea.textContent = uri;
|
textarea.textContent = uri;
|
||||||
textarea.style.position = 'fixed';
|
textarea.style.position = 'fixed';
|
||||||
|
|
||||||
document.body.appendChild(textarea);
|
document.body.appendChild(textarea);
|
||||||
|
@ -461,7 +461,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
text: intl.formatMessage(messages.admin_status),
|
text: intl.formatMessage(messages.admin_status),
|
||||||
href: `/pleroma/admin/#/statuses/${status.id}/`,
|
href: `/pleroma/admin/#/statuses/${status.id}/`,
|
||||||
icon: require('@tabler/icons/pencil.svg'),
|
icon: require('@tabler/icons/pencil.svg'),
|
||||||
action: (event) => event.stopPropagation(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -554,6 +553,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-testid='status-action-bar'
|
||||||
className={classNames('flex flex-row', {
|
className={classNames('flex flex-row', {
|
||||||
'justify-between': space === 'expand',
|
'justify-between': space === 'expand',
|
||||||
'space-x-2': space === 'compact',
|
'space-x-2': space === 'compact',
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
.status-content p {
|
.status-content p {
|
||||||
@apply mb-5 whitespace-pre-wrap;
|
@apply mb-4 whitespace-pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-content p:last-child {
|
.status-content p:last-child {
|
||||||
@apply mb-0.5;
|
@apply mb-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-content a {
|
.status-content a {
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
.status-content ul,
|
.status-content ul,
|
||||||
.status-content ol {
|
.status-content ol {
|
||||||
@apply pl-10 mb-5;
|
@apply pl-10 mb-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-content ul {
|
.status-content ul {
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-content blockquote {
|
.status-content blockquote {
|
||||||
@apply py-1 pl-4 mb-5 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
|
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-content code {
|
.status-content code {
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
/* Code block */
|
/* Code block */
|
||||||
.status-content pre {
|
.status-content pre {
|
||||||
@apply py-2 px-3 mb-5 leading-6 overflow-x-auto rounded-md break-all;
|
@apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-content pre:last-child {
|
.status-content pre:last-child {
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
/* Markdown images */
|
/* Markdown images */
|
||||||
.status-content img:not(.emojione):not([width][height]) {
|
.status-content img:not(.emojione):not([width][height]) {
|
||||||
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-5 block;
|
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* User setting to underline links */
|
/* User setting to underline links */
|
||||||
|
|
|
@ -50,7 +50,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
||||||
// The typical case with a reply-to and a list of mentions.
|
// The typical case with a reply-to and a list of mentions.
|
||||||
const accounts = to.slice(0, 2).map(account => {
|
const accounts = to.slice(0, 2).map(account => {
|
||||||
const link = (
|
const link = (
|
||||||
<Link to={`/@${account.acct}`} className='reply-mentions__account'>@{account.username}</Link>
|
<Link to={`/@${account.acct}`} className='reply-mentions__account' onClick={(e) => e.stopPropagation()}>@{account.username}</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hoverable) {
|
if (hoverable) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { toggleStatusHidden } from 'soapbox/actions/statuses';
|
import { toggleStatusHidden } from 'soapbox/actions/statuses';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import TranslateButton from 'soapbox/components/translate-button';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
||||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||||
|
@ -66,7 +67,6 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
hidden,
|
hidden,
|
||||||
featured,
|
featured,
|
||||||
unread,
|
unread,
|
||||||
group,
|
|
||||||
hideActionBar,
|
hideActionBar,
|
||||||
variant = 'rounded',
|
variant = 'rounded',
|
||||||
withDismiss,
|
withDismiss,
|
||||||
|
@ -85,6 +85,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
|
|
||||||
const actualStatus = getActualStatus(status);
|
const actualStatus = getActualStatus(status);
|
||||||
|
|
||||||
|
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
||||||
|
|
||||||
// Track height changes we know about to compensate scrolling.
|
// Track height changes we know about to compensate scrolling.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
didShowCard.current = Boolean(!muted && !hidden && status?.card);
|
didShowCard.current = Boolean(!muted && !hidden && status?.card);
|
||||||
|
@ -98,16 +100,18 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
setShowMedia(!showMedia);
|
setShowMedia(!showMedia);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (): void => {
|
const handleClick = (e?: React.MouseEvent): void => {
|
||||||
if (onClick) {
|
e?.stopPropagation();
|
||||||
onClick();
|
|
||||||
} else {
|
|
||||||
history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExpandedToggle = (): void => {
|
if (!e || !(e.ctrlKey || e.metaKey)) {
|
||||||
dispatch(toggleStatusHidden(actualStatus));
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
} else {
|
||||||
|
history.push(statusUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.open(statusUrl, '_blank');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => {
|
const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => {
|
||||||
|
@ -150,7 +154,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHotkeyOpen = (): void => {
|
const handleHotkeyOpen = (): void => {
|
||||||
history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`);
|
history.push(statusUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHotkeyOpenProfile = (): void => {
|
const handleHotkeyOpenProfile = (): void => {
|
||||||
|
@ -297,12 +301,10 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
react: handleHotkeyReact,
|
react: handleHotkeyReact,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
|
||||||
|
|
||||||
const accountAction = props.accountAction || reblogElement;
|
const accountAction = props.accountAction || reblogElement;
|
||||||
|
|
||||||
const inReview = status.visibility === 'self';
|
const isUnderReview = actualStatus.visibility === 'self';
|
||||||
const isSensitive = status.sensitive;
|
const isSensitive = actualStatus.hidden;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers} data-testid='status'>
|
<HotKeys handlers={handlers} data-testid='status'>
|
||||||
|
@ -312,7 +314,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
data-featured={featured ? 'true' : null}
|
data-featured={featured ? 'true' : null}
|
||||||
aria-label={textForScreenReader(intl, actualStatus, rebloggedByText)}
|
aria-label={textForScreenReader(intl, actualStatus, rebloggedByText)}
|
||||||
ref={node}
|
ref={node}
|
||||||
onClick={() => history.push(statusUrl)}
|
onClick={handleClick}
|
||||||
role='link'
|
role='link'
|
||||||
>
|
>
|
||||||
{featured && (
|
{featured && (
|
||||||
|
@ -354,56 +356,52 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='status__content-wrapper'>
|
<div className='status__content-wrapper'>
|
||||||
|
<StatusReplyMentions status={actualStatus} hoverable={hoverable} />
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
className={
|
className={
|
||||||
classNames('relative', {
|
classNames('relative', {
|
||||||
'min-h-[220px]': inReview || isSensitive,
|
'min-h-[220px]': isUnderReview || isSensitive,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(inReview || isSensitive) ? (
|
{(isUnderReview || isSensitive) && (
|
||||||
<SensitiveContentOverlay
|
<SensitiveContentOverlay
|
||||||
status={status}
|
status={status}
|
||||||
visible={showMedia}
|
visible={showMedia}
|
||||||
onToggleVisibility={handleToggleMediaVisibility}
|
onToggleVisibility={handleToggleMediaVisibility}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!group && actualStatus.group && (
|
|
||||||
<div className='status__meta'>
|
|
||||||
Posted in <NavLink to={`/groups/${actualStatus.getIn(['group', 'id'])}`}>{String(actualStatus.getIn(['group', 'title']))}</NavLink>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<StatusReplyMentions
|
|
||||||
status={actualStatus}
|
|
||||||
hoverable={hoverable}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{actualStatus.event ? <EventPreview className='shadow-xl' status={actualStatus} /> : (
|
{actualStatus.event ? <EventPreview className='shadow-xl' status={actualStatus} /> : (
|
||||||
<>
|
<>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
expanded={!status.hidden}
|
|
||||||
onExpandedToggle={handleExpandedToggle}
|
|
||||||
collapsable
|
collapsable
|
||||||
|
translatable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StatusMedia
|
<TranslateButton status={actualStatus} />
|
||||||
status={actualStatus}
|
|
||||||
muted={muted}
|
|
||||||
onClick={handleClick}
|
|
||||||
showMedia={showMedia}
|
|
||||||
onToggleVisibility={handleToggleMediaVisibility}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{quote}
|
{(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && (
|
||||||
|
<Stack space={4}>
|
||||||
|
<StatusMedia
|
||||||
|
status={actualStatus}
|
||||||
|
muted={muted}
|
||||||
|
onClick={handleClick}
|
||||||
|
showMedia={showMedia}
|
||||||
|
onToggleVisibility={handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{quote}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!hideActionBar && (
|
{(!hideActionBar && !isUnderReview) && (
|
||||||
<div className='pt-4'>
|
<div className='pt-4'>
|
||||||
<StatusActionBar status={actualStatus} withDismiss={withDismiss} />
|
<StatusActionBar status={actualStatus} withDismiss={withDismiss} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,49 +35,17 @@ const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ISpoilerButton {
|
|
||||||
onClick: React.MouseEventHandler,
|
|
||||||
hidden: boolean,
|
|
||||||
tabIndex?: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Button to expand status text behind a content warning */
|
|
||||||
const SpoilerButton: React.FC<ISpoilerButton> = ({ onClick, hidden, tabIndex }) => (
|
|
||||||
<button
|
|
||||||
tabIndex={tabIndex}
|
|
||||||
className={classNames(
|
|
||||||
'inline-block rounded-md px-1.5 py-0.5 ml-[0.5em]',
|
|
||||||
'text-gray-900 dark:text-gray-100',
|
|
||||||
'font-bold text-[11px] uppercase',
|
|
||||||
'bg-primary-100 dark:bg-primary-800',
|
|
||||||
'hover:bg-primary-300 dark:hover:bg-primary-600',
|
|
||||||
'focus:bg-primary-200 dark:focus:bg-primary-600',
|
|
||||||
'hover:no-underline',
|
|
||||||
'duration-100',
|
|
||||||
)}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{hidden ? (
|
|
||||||
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface IStatusContent {
|
interface IStatusContent {
|
||||||
status: Status,
|
status: Status,
|
||||||
expanded?: boolean,
|
|
||||||
onExpandedToggle?: () => void,
|
|
||||||
onClick?: () => void,
|
onClick?: () => void,
|
||||||
collapsable?: boolean,
|
collapsable?: boolean,
|
||||||
|
translatable?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renders the text content of a status */
|
/** Renders the text content of a status */
|
||||||
const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onExpandedToggle, onClick, collapsable = false }) => {
|
const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable = false, translatable }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const [hidden, setHidden] = useState(true);
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [onlyEmoji, setOnlyEmoji] = useState(false);
|
const [onlyEmoji, setOnlyEmoji] = useState(false);
|
||||||
|
|
||||||
|
@ -179,46 +147,32 @@ const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onE
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deltaX + deltaY < 5 && e.button === 0 && onClick) {
|
if (deltaX + deltaY < 5 && e.button === 0 && !(e.ctrlKey || e.metaKey) && onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
startXY.current = undefined;
|
startXY.current = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSpoilerClick: React.EventHandler<React.MouseEvent> = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (onExpandedToggle) {
|
|
||||||
// The parent manages the state
|
|
||||||
onExpandedToggle();
|
|
||||||
} else {
|
|
||||||
setHidden(!hidden);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsedHtml = useMemo((): string => {
|
const parsedHtml = useMemo((): string => {
|
||||||
const { contentHtml: html } = status;
|
const html = translatable && status.translation ? status.translation.get('content')! : status.contentHtml;
|
||||||
|
|
||||||
if (greentext) {
|
if (greentext) {
|
||||||
return addGreentext(html);
|
return addGreentext(html);
|
||||||
} else {
|
} else {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
}, [status.contentHtml]);
|
}, [status.contentHtml, status.translation]);
|
||||||
|
|
||||||
if (status.content.length === 0) {
|
if (status.content.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHidden = onExpandedToggle ? !expanded : hidden;
|
|
||||||
const withSpoiler = status.spoiler_text.length > 0;
|
const withSpoiler = status.spoiler_text.length > 0;
|
||||||
|
|
||||||
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
|
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
|
||||||
|
|
||||||
const content = { __html: parsedHtml };
|
const content = { __html: parsedHtml };
|
||||||
const spoilerContent = { __html: status.spoilerHtml };
|
|
||||||
const directionStyle: React.CSSProperties = { direction: 'ltr' };
|
const directionStyle: React.CSSProperties = { direction: 'ltr' };
|
||||||
const className = classNames(baseClassName, 'status-content', {
|
const className = classNames(baseClassName, 'status-content', {
|
||||||
'cursor-pointer': onClick,
|
'cursor-pointer': onClick,
|
||||||
|
@ -231,37 +185,7 @@ const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onE
|
||||||
directionStyle.direction = 'rtl';
|
directionStyle.direction = 'rtl';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.spoiler_text.length > 0) {
|
if (onClick) {
|
||||||
return (
|
|
||||||
<div className={className} ref={node} tabIndex={0} style={directionStyle} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}>
|
|
||||||
<p style={{ marginBottom: isHidden && status.mentions.isEmpty() ? 0 : undefined }}>
|
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} lang={status.language || undefined} />
|
|
||||||
|
|
||||||
<SpoilerButton
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={handleSpoilerClick}
|
|
||||||
hidden={isHidden}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
tabIndex={!isHidden ? 0 : undefined}
|
|
||||||
className={classNames({
|
|
||||||
'whitespace-pre-wrap': withSpoiler,
|
|
||||||
'hidden': isHidden,
|
|
||||||
'block': !isHidden,
|
|
||||||
})}
|
|
||||||
style={directionStyle}
|
|
||||||
dangerouslySetInnerHTML={content}
|
|
||||||
lang={status.language || undefined}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isHidden && status.poll && typeof status.poll === 'string' && (
|
|
||||||
<Poll id={status.poll} status={status.url} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (onClick) {
|
|
||||||
const output = [
|
const output = [
|
||||||
<div
|
<div
|
||||||
ref={node}
|
ref={node}
|
||||||
|
|
|
@ -81,6 +81,14 @@ const SensitiveContentOverlay = (props: ISensitiveContentOverlay) => {
|
||||||
<Text theme='white' size='sm' weight='medium'>
|
<Text theme='white' size='sm' weight='medium'>
|
||||||
{intl.formatMessage(isUnderReview ? messages.underReviewSubtitle : messages.sensitiveSubtitle)}
|
{intl.formatMessage(isUnderReview ? messages.underReviewSubtitle : messages.sensitiveSubtitle)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{status.spoiler_text && (
|
||||||
|
<div className='py-4 italic'>
|
||||||
|
<Text theme='white' size='md' weight='medium'>
|
||||||
|
“<span dangerouslySetInnerHTML={{ __html: status.spoilerHtml }} />”
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HStack alignItems='center' justifyContent='center' space={2}>
|
<HStack alignItems='center' justifyContent='center' space={2}>
|
||||||
|
|
|
@ -4,34 +4,22 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
// import { connect } from 'react-redux';
|
// import { connect } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
// import { openModal } from 'soapbox/actions/modals';
|
|
||||||
// import { useAppDispatch } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
import { CardHeader, CardTitle } from './ui';
|
import { CardHeader, CardTitle } from './ui';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||||
settings: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ISubNavigation {
|
interface ISubNavigation {
|
||||||
message: String,
|
message: React.ReactNode,
|
||||||
|
/** @deprecated Unused. */
|
||||||
settings?: React.ComponentType,
|
settings?: React.ComponentType,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubNavigation: React.FC<ISubNavigation> = ({ message }) => {
|
const SubNavigation: React.FC<ISubNavigation> = ({ message }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
// const dispatch = useAppDispatch();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
// const ref = useRef(null);
|
|
||||||
|
|
||||||
// const [scrolled, setScrolled] = useState(false);
|
|
||||||
|
|
||||||
// const onOpenSettings = () => {
|
|
||||||
// dispatch(openModal('COMPONENT', { component: Settings }));
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
if (window.history && window.history.length === 1) {
|
if (window.history && window.history.length === 1) {
|
||||||
history.push('/');
|
history.push('/');
|
||||||
|
@ -40,36 +28,6 @@ const SubNavigation: React.FC<ISubNavigation> = ({ message }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleBackKeyUp = (e) => {
|
|
||||||
// if (e.key === 'Enter') {
|
|
||||||
// handleClick();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const handleOpenSettings = () => {
|
|
||||||
// onOpenSettings();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const handleScroll = throttle(() => {
|
|
||||||
// if (this.node) {
|
|
||||||
// const { offsetTop } = this.node;
|
|
||||||
|
|
||||||
// if (offsetTop > 0) {
|
|
||||||
// setScrolled(true);
|
|
||||||
// } else {
|
|
||||||
// setScrolled(false);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, 150, { trailing: true });
|
|
||||||
|
|
||||||
// window.addEventListener('scroll', handleScroll);
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// window.removeEventListener('scroll', handleScroll);
|
|
||||||
// };
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardHeader
|
<CardHeader
|
||||||
aria-label={intl.formatMessage(messages.back)}
|
aria-label={intl.formatMessage(messages.back)}
|
||||||
|
|
|
@ -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 = me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||||
|
|
||||||
|
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (status.translation) {
|
||||||
|
dispatch(undoStatusTranslation(status.id));
|
||||||
|
} else {
|
||||||
|
dispatch(translateStatus(status.id, intl.locale));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!features.translations || !renderTranslate) return null;
|
||||||
|
|
||||||
|
if (status.translation) {
|
||||||
|
const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
|
||||||
|
const languageName = languageNames.of(status.language!);
|
||||||
|
const provider = status.translation.get('provider');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className='text-gray-700 dark:text-gray-600 text-sm' alignItems='start'>
|
||||||
|
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||||
|
|
||||||
|
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
|
||||||
|
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||||
|
</button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
|
||||||
|
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TranslateButton;
|
|
@ -40,6 +40,8 @@ interface IHStack {
|
||||||
space?: keyof typeof spaces
|
space?: keyof typeof spaces
|
||||||
/** Whether to let the flexbox grow. */
|
/** Whether to let the flexbox grow. */
|
||||||
grow?: boolean
|
grow?: boolean
|
||||||
|
/** HTML element to use for container. */
|
||||||
|
element?: keyof JSX.IntrinsicElements,
|
||||||
/** Extra CSS styles for the <div> */
|
/** Extra CSS styles for the <div> */
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
/** Whether to let the flexbox wrap onto multiple lines. */
|
/** Whether to let the flexbox wrap onto multiple lines. */
|
||||||
|
@ -48,10 +50,12 @@ interface IHStack {
|
||||||
|
|
||||||
/** Horizontal row of child elements. */
|
/** Horizontal row of child elements. */
|
||||||
const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
|
const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
|
||||||
const { space, alignItems, grow, justifyContent, wrap, className, ...filteredProps } = props;
|
const { space, alignItems, justifyContent, className, grow, element = 'div', wrap, ...filteredProps } = props;
|
||||||
|
|
||||||
|
const Elem = element as 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Elem
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames('flex', {
|
className={classNames('flex', {
|
||||||
|
|
|
@ -20,30 +20,36 @@ const justifyContentOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignItemsOptions = {
|
const alignItemsOptions = {
|
||||||
|
top: 'items-start',
|
||||||
|
bottom: 'items-end',
|
||||||
center: 'items-center',
|
center: 'items-center',
|
||||||
start: 'items-start',
|
start: 'items-start',
|
||||||
end: 'items-end',
|
end: 'items-end',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
/** Size of the gap between elements. */
|
|
||||||
space?: keyof typeof spaces
|
|
||||||
/** Horizontal alignment of children. */
|
/** Horizontal alignment of children. */
|
||||||
alignItems?: 'center' | 'start' | 'end',
|
alignItems?: keyof typeof alignItemsOptions
|
||||||
|
/** Extra class names on the element. */
|
||||||
|
className?: string
|
||||||
/** Vertical alignment of children. */
|
/** Vertical alignment of children. */
|
||||||
justifyContent?: keyof typeof justifyContentOptions
|
justifyContent?: keyof typeof justifyContentOptions
|
||||||
/** Extra class names on the <div> element. */
|
/** Size of the gap between elements. */
|
||||||
className?: string
|
space?: keyof typeof spaces
|
||||||
/** Whether to let the flexbox grow. */
|
/** Whether to let the flexbox grow. */
|
||||||
grow?: boolean
|
grow?: boolean
|
||||||
|
/** HTML element to use for container. */
|
||||||
|
element?: keyof JSX.IntrinsicElements,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vertical stack of child elements. */
|
/** Vertical stack of child elements. */
|
||||||
const Stack = React.forwardRef<HTMLDivElement, IStack>((props, ref: React.LegacyRef<HTMLDivElement> | undefined) => {
|
const Stack = React.forwardRef<HTMLDivElement, IStack>((props, ref: React.LegacyRef<HTMLDivElement> | undefined) => {
|
||||||
const { space, alignItems, justifyContent, className, grow, ...filteredProps } = props;
|
const { space, alignItems, justifyContent, className, grow, element = 'div', ...filteredProps } = props;
|
||||||
|
|
||||||
|
const Elem = element as 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Elem
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames('flex flex-col items', {
|
className={classNames('flex flex-col items', {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Portal from '@reach/portal';
|
import { Portal } from '@reach/portal';
|
||||||
import { TooltipPopup, useTooltip } from '@reach/tooltip';
|
import { TooltipPopup, useTooltip } from '@reach/tooltip';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,6 @@ const messages = defineMessages({
|
||||||
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
|
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect()
|
|
||||||
@injectIntl
|
|
||||||
class UserIndex extends ImmutablePureComponent {
|
class UserIndex extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -130,3 +128,5 @@ class UserIndex extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default injectIntl(connect()(UserIndex));
|
|
@ -1,26 +1,45 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { defineMessages, FormattedDate, useIntl } from 'react-intl';
|
import { defineMessages, FormattedDate, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';
|
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';
|
||||||
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack, Text } from 'soapbox/components/ui';
|
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { Token } from 'soapbox/reducers/security';
|
import { Token } from 'soapbox/reducers/security';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
|
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
|
||||||
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
|
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
|
||||||
|
revokeSessionHeading: { id: 'confirmations.revoke_session.heading', defaultMessage: 'Revoke current session' },
|
||||||
|
revokeSessionMessage: { id: 'confirmations.revoke_session.message', defaultMessage: 'You are about to revoke your current session. You will be signed out.' },
|
||||||
|
revokeSessionConfirm: { id: 'confirmations.revoke_session.confirm', defaultMessage: 'Revoke' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IAuthToken {
|
interface IAuthToken {
|
||||||
token: Token,
|
token: Token,
|
||||||
|
isCurrent: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthToken: React.FC<IAuthToken> = ({ token }) => {
|
const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleRevoke = () => {
|
const handleRevoke = () => {
|
||||||
dispatch(revokeOAuthTokenById(token.id));
|
if (isCurrent)
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
icon: require('@tabler/icons/alert-triangle.svg'),
|
||||||
|
heading: intl.formatMessage(messages.revokeSessionHeading),
|
||||||
|
message: intl.formatMessage(messages.revokeSessionMessage),
|
||||||
|
confirm: intl.formatMessage(messages.revokeSessionConfirm),
|
||||||
|
onConfirm: () => {
|
||||||
|
dispatch(revokeOAuthTokenById(token.id));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
else {
|
||||||
|
dispatch(revokeOAuthTokenById(token.id));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -42,7 +61,7 @@ const AuthToken: React.FC<IAuthToken> = ({ token }) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<div className='flex justify-end'>
|
<div className='flex justify-end'>
|
||||||
<Button theme='primary' onClick={handleRevoke}>
|
<Button theme={isCurrent ? 'danger' : 'primary'} onClick={handleRevoke}>
|
||||||
{intl.formatMessage(messages.revoke)}
|
{intl.formatMessage(messages.revoke)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,6 +74,11 @@ const AuthTokenList: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
|
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
|
||||||
|
const currentTokenId = useAppSelector(state => {
|
||||||
|
const currentToken = state.auth.get('tokens').valueSeq().find((token: ImmutableMap<string, any>) => token.get('me') === state.auth.get('me'));
|
||||||
|
|
||||||
|
return currentToken?.get('id');
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchOAuthTokens());
|
dispatch(fetchOAuthTokens());
|
||||||
|
@ -63,7 +87,7 @@ const AuthTokenList: React.FC = () => {
|
||||||
const body = tokens ? (
|
const body = tokens ? (
|
||||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||||
{tokens.map((token) => (
|
{tokens.map((token) => (
|
||||||
<AuthToken key={token.id} token={token} />
|
<AuthToken key={token.id} token={token} isCurrent={token.id === currentTokenId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : <Spinner />;
|
) : <Spinner />;
|
||||||
|
|
|
@ -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);
|
|
|
@ -10,8 +10,6 @@ import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
import Timeline from '../ui/components/timeline';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
import ColumnSettings from './containers/column_settings_container';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||||
});
|
});
|
||||||
|
@ -44,7 +42,10 @@ const CommunityTimeline = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
<div className='px-4 sm:p-0'>
|
||||||
|
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<PullToRefresh onRefresh={handleRefresh}>
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
<Timeline
|
<Timeline
|
||||||
scrollKey={`${timelineId}_timeline`}
|
scrollKey={`${timelineId}_timeline`}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
clearComposeSuggestions,
|
clearComposeSuggestions,
|
||||||
fetchComposeSuggestions,
|
fetchComposeSuggestions,
|
||||||
selectComposeSuggestion,
|
selectComposeSuggestion,
|
||||||
changeComposeSpoilerText,
|
|
||||||
insertEmojiCompose,
|
insertEmojiCompose,
|
||||||
uploadCompose,
|
uploadCompose,
|
||||||
} from 'soapbox/actions/compose';
|
} from 'soapbox/actions/compose';
|
||||||
|
@ -38,6 +37,7 @@ import UploadButtonContainer from '../containers/upload_button_container';
|
||||||
import WarningContainer from '../containers/warning_container';
|
import WarningContainer from '../containers/warning_container';
|
||||||
import { countableText } from '../util/counter';
|
import { countableText } from '../util/counter';
|
||||||
|
|
||||||
|
import SpoilerInput from './spoiler-input';
|
||||||
import TextCharacterCounter from './text_character_counter';
|
import TextCharacterCounter from './text_character_counter';
|
||||||
import VisualCharacterCounter from './visual_character_counter';
|
import VisualCharacterCounter from './visual_character_counter';
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ const messages = defineMessages({
|
||||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
|
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
|
||||||
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic…' },
|
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic…' },
|
||||||
eventPlaceholder: { id: 'compose_form.event_placeholder', defaultMessage: 'Post to this event' },
|
eventPlaceholder: { id: 'compose_form.event_placeholder', defaultMessage: 'Post to this event' },
|
||||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||||
message: { id: 'compose_form.message', defaultMessage: 'Message' },
|
message: { id: 'compose_form.message', defaultMessage: 'Message' },
|
||||||
|
@ -134,7 +134,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
setComposeFocused(true);
|
setComposeFocused(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = (e?: React.FormEvent<Element>) => {
|
||||||
if (text !== autosuggestTextareaRef.current?.textarea?.value) {
|
if (text !== autosuggestTextareaRef.current?.textarea?.value) {
|
||||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||||
// Update the state to match the current text
|
// Update the state to match the current text
|
||||||
|
@ -144,6 +144,10 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
// Submit disabled:
|
// Submit disabled:
|
||||||
const fulltext = [spoilerText, countableText(text)].join('');
|
const fulltext = [spoilerText, countableText(text)].join('');
|
||||||
|
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -167,10 +171,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text']));
|
dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text']));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeSpoilerText: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
dispatch(changeComposeSpoilerText(id, e.target.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCursor = (start: number, end: number = start) => {
|
const setCursor = (start: number, end: number = start) => {
|
||||||
if (!autosuggestTextareaRef.current?.textarea) return;
|
if (!autosuggestTextareaRef.current?.textarea) return;
|
||||||
autosuggestTextareaRef.current.textarea.setSelectionRange(start, end);
|
autosuggestTextareaRef.current.textarea.setSelectionRange(start, end);
|
||||||
|
@ -276,7 +276,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className='w-full' space={1} ref={formRef} onClick={handleClick}>
|
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||||
{scheduledStatusCount > 0 && !event && (
|
{scheduledStatusCount > 0 && !event && (
|
||||||
<Warning
|
<Warning
|
||||||
message={(
|
message={(
|
||||||
|
@ -302,30 +302,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
|
|
||||||
{!shouldCondense && !event && <ReplyMentions composeId={id} />}
|
{!shouldCondense && !event && <ReplyMentions composeId={id} />}
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames({
|
|
||||||
'relative transition-height': true,
|
|
||||||
'hidden': !spoiler,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<AutosuggestInput
|
|
||||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
|
||||||
value={spoilerText}
|
|
||||||
onChange={handleChangeSpoilerText}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
disabled={!spoiler}
|
|
||||||
ref={spoilerTextRef}
|
|
||||||
suggestions={suggestions}
|
|
||||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
|
||||||
onSuggestionSelected={onSpoilerSuggestionSelected}
|
|
||||||
searchTokens={[':']}
|
|
||||||
id='cw-spoiler-input'
|
|
||||||
className='border-none shadow-none px-0 py-2 text-base'
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AutosuggestTextarea
|
<AutosuggestTextarea
|
||||||
ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef}
|
ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef}
|
||||||
placeholder={intl.formatMessage(textareaPlaceholder)}
|
placeholder={intl.formatMessage(textareaPlaceholder)}
|
||||||
|
@ -345,11 +321,19 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
!condensed &&
|
!condensed &&
|
||||||
<div className='compose-form__modifiers'>
|
<Stack space={4} className='compose-form__modifiers'>
|
||||||
<UploadForm composeId={id} />
|
<UploadForm composeId={id} />
|
||||||
<PollForm composeId={id} />
|
<PollForm composeId={id} />
|
||||||
<ScheduleFormContainer composeId={id} />
|
<ScheduleFormContainer composeId={id} />
|
||||||
</div>
|
|
||||||
|
<SpoilerInput
|
||||||
|
composeId={id}
|
||||||
|
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||||
|
onSuggestionSelected={onSpoilerSuggestionSelected}
|
||||||
|
ref={spoilerTextRef}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
}
|
}
|
||||||
</AutosuggestTextarea>
|
</AutosuggestTextarea>
|
||||||
|
|
||||||
|
@ -370,7 +354,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button theme='primary' text={publishText} onClick={handleSubmit} disabled={disabledButton} />
|
<Button type='submit' theme='primary' text={publishText} disabled={disabledButton} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -168,7 +168,7 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<button onClick={handleToggleMultiple} className='text-left'>
|
<button type='button' onClick={handleToggleMultiple} className='text-left'>
|
||||||
<HStack alignItems='center' justifyContent='between'>
|
<HStack alignItems='center' justifyContent='between'>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text weight='medium'>
|
<Text weight='medium'>
|
||||||
|
@ -197,7 +197,7 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
|
||||||
|
|
||||||
{/* Remove Poll */}
|
{/* Remove Poll */}
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<button className='text-danger-500' onClick={onRemovePoll}>
|
<button type='button' className='text-danger-500' onClick={onRemovePoll}>
|
||||||
{intl.formatMessage(messages.removePoll)}
|
{intl.formatMessage(messages.removePoll)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { changeComposeSensitivity } from 'soapbox/actions/compose';
|
|
||||||
import { FormGroup, Checkbox } from 'soapbox/components/ui';
|
|
||||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
|
|
||||||
unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' },
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ISensitiveButton {
|
|
||||||
composeId: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Button to mark own media as sensitive. */
|
|
||||||
const SensitiveButton: React.FC<ISensitiveButton> = ({ composeId }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const compose = useCompose(composeId);
|
|
||||||
|
|
||||||
const active = compose.sensitive === true;
|
|
||||||
const disabled = compose.spoiler === true;
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
dispatch(changeComposeSensitivity(composeId));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='px-2.5 py-1'>
|
|
||||||
<FormGroup
|
|
||||||
labelText={<FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />}
|
|
||||||
labelTitle={intl.formatMessage(active ? messages.marked : messages.unmarked)}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
name='mark-sensitive'
|
|
||||||
checked={active}
|
|
||||||
onChange={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SensitiveButton;
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import classNames from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { changeComposeSpoilerness, changeComposeSpoilerText } from 'soapbox/actions/compose';
|
||||||
|
import AutosuggestInput, { IAutosuggestInput } from 'soapbox/components/autosuggest_input';
|
||||||
|
import { Divider, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'compose_form.spoiler_title', defaultMessage: 'Sensitive content' },
|
||||||
|
placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' },
|
||||||
|
remove: { id: 'compose_form.spoiler_remove', defaultMessage: 'Remove sensitive' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ISpoilerInput extends Pick<IAutosuggestInput, 'onSuggestionsFetchRequested' | 'onSuggestionsClearRequested' | 'onSuggestionSelected'> {
|
||||||
|
composeId: string extends 'default' ? never : string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Text input for content warning in composer. */
|
||||||
|
const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
||||||
|
composeId,
|
||||||
|
onSuggestionsFetchRequested,
|
||||||
|
onSuggestionsClearRequested,
|
||||||
|
onSuggestionSelected,
|
||||||
|
}, ref) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const compose = useCompose(composeId);
|
||||||
|
|
||||||
|
const handleChangeSpoilerText: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
dispatch(changeComposeSpoilerText(composeId, e.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
dispatch(changeComposeSpoilerness(composeId));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
space={4}
|
||||||
|
className={classNames({
|
||||||
|
'relative transition-height': true,
|
||||||
|
'hidden': !compose.spoiler,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text weight='medium'>
|
||||||
|
{intl.formatMessage(messages.title)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<AutosuggestInput
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
value={compose.spoiler_text}
|
||||||
|
onChange={handleChangeSpoilerText}
|
||||||
|
disabled={!compose.spoiler}
|
||||||
|
suggestions={compose.suggestions}
|
||||||
|
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||||
|
onSuggestionSelected={onSuggestionSelected}
|
||||||
|
searchTokens={[':']}
|
||||||
|
id='cw-spoiler-input'
|
||||||
|
className='rounded-md dark:!bg-transparent !bg-transparent'
|
||||||
|
ref={ref}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='text-center'>
|
||||||
|
<button type='button' className='text-danger-500' onClick={handleRemove}>
|
||||||
|
{intl.formatMessage(messages.remove)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SpoilerInput;
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
||||||
|
|
||||||
import { useCompose } from 'soapbox/hooks';
|
import { useCompose } from 'soapbox/hooks';
|
||||||
|
|
||||||
import SensitiveButton from './sensitive-button';
|
|
||||||
import Upload from './upload';
|
import Upload from './upload';
|
||||||
import UploadProgress from './upload-progress';
|
import UploadProgress from './upload-progress';
|
||||||
|
|
||||||
|
@ -28,8 +27,6 @@ const UploadForm: React.FC<IUploadForm> = ({ composeId }) => {
|
||||||
<Upload id={id} key={id} composeId={composeId} />
|
<Upload id={id} key={id} composeId={composeId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!mediaIds.isEmpty() && <SensitiveButton composeId={composeId} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import classNames from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface IIndicator {
|
||||||
|
state?: 'active' | 'pending' | 'error' | 'inactive',
|
||||||
|
size?: 'sm',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indicator dot component. */
|
||||||
|
const Indicator: React.FC<IIndicator> = ({ state = 'inactive', size = 'sm' }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('rounded-full outline-double', {
|
||||||
|
'w-1.5 h-1.5 shadow-sm': size === 'sm',
|
||||||
|
'bg-green-500 outline-green-400': state === 'active',
|
||||||
|
'bg-yellow-500 outline-yellow-400': state === 'pending',
|
||||||
|
'bg-red-500 outline-red-400': state === 'error',
|
||||||
|
'bg-neutral-500 outline-neutral-400': state === 'inactive',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Indicator;
|
|
@ -89,6 +89,14 @@ const Developers: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</DashWidget>
|
</DashWidget>
|
||||||
|
|
||||||
|
<DashWidget to='/developers/sw'>
|
||||||
|
<SvgIcon src={require('@tabler/icons/script.svg')} className='text-gray-700 dark:text-gray-600' />
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage id='developers.navigation.service_worker_label' defaultMessage='Service Worker' />
|
||||||
|
</Text>
|
||||||
|
</DashWidget>
|
||||||
|
|
||||||
<DashWidget onClick={leaveDevelopers}>
|
<DashWidget onClick={leaveDevelopers}>
|
||||||
<SvgIcon src={require('@tabler/icons/logout.svg')} className='text-gray-700 dark:text-gray-600' />
|
<SvgIcon src={require('@tabler/icons/logout.svg')} className='text-gray-700 dark:text-gray-600' />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
|
import { HStack, Text, Column, FormActions, Button, Stack, Icon } from 'soapbox/components/ui';
|
||||||
|
import { unregisterSw } from 'soapbox/utils/sw';
|
||||||
|
|
||||||
|
import Indicator from './components/indicator';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.developers.service_worker', defaultMessage: 'Service Worker' },
|
||||||
|
status: { id: 'sw.status', defaultMessage: 'Status' },
|
||||||
|
url: { id: 'sw.url', defaultMessage: 'Script URL' },
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Hook that returns the active ServiceWorker registration. */
|
||||||
|
const useRegistration = () => {
|
||||||
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [registration, setRegistration] = useState<ServiceWorkerRegistration>();
|
||||||
|
|
||||||
|
const isSupported = 'serviceWorker' in navigator;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSupported) {
|
||||||
|
navigator.serviceWorker.getRegistration()
|
||||||
|
.then(r => {
|
||||||
|
setRegistration(r);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => setLoading(false));
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
registration,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IServiceWorkerInfo {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mini ServiceWorker debugging component. */
|
||||||
|
const ServiceWorkerInfo: React.FC<IServiceWorkerInfo> = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { isLoading, registration } = useRegistration();
|
||||||
|
|
||||||
|
const url = registration?.active?.scriptURL;
|
||||||
|
|
||||||
|
const getState = () => {
|
||||||
|
if (registration?.waiting) {
|
||||||
|
return 'pending';
|
||||||
|
} else if (registration?.active) {
|
||||||
|
return 'active';
|
||||||
|
} else {
|
||||||
|
return 'inactive';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessage = () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='sw.state.loading'
|
||||||
|
defaultMessage='Loading…'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (!isLoading && !registration) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='sw.state.unavailable'
|
||||||
|
defaultMessage='Unavailable'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (registration?.waiting) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='sw.state.waiting'
|
||||||
|
defaultMessage='Waiting'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (registration?.active) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='sw.state.active'
|
||||||
|
defaultMessage='Active'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='sw.state.unknown'
|
||||||
|
defaultMessage='Unknown'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestart = async() => {
|
||||||
|
await unregisterSw();
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column label={intl.formatMessage(messages.heading)} backHref='/developers'>
|
||||||
|
<Stack space={4}>
|
||||||
|
<List>
|
||||||
|
<ListItem label={intl.formatMessage(messages.status)}>
|
||||||
|
<HStack alignItems='center' space={2}>
|
||||||
|
<Indicator state={getState()} />
|
||||||
|
<Text size='md' theme='muted'>{getMessage()}</Text>
|
||||||
|
</HStack>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{url && (
|
||||||
|
<ListItem label={intl.formatMessage(messages.url)}>
|
||||||
|
<a href={url} target='_blank' className='flex space-x-1 items-center truncate'>
|
||||||
|
<span className='truncate'>{url}</span>
|
||||||
|
<Icon
|
||||||
|
className='w-4 h-4'
|
||||||
|
src={require('@tabler/icons/external-link.svg')}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<FormActions>
|
||||||
|
<Button theme='tertiary' type='button' onClick={handleRestart}>
|
||||||
|
<FormattedMessage id='sw.restart' defaultMessage='Restart' />
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</Stack>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceWorkerInfo;
|
|
@ -1,39 +1,29 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import { Text } from 'soapbox/components/ui';
|
import { Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
const hasRestrictions = remoteInstance => {
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
const hasRestrictions = (remoteInstance: ImmutableMap<string, any>): boolean => {
|
||||||
return remoteInstance
|
return remoteInstance
|
||||||
.get('federation')
|
.get('federation')
|
||||||
.deleteAll(['accept', 'reject_deletes', 'report_removal'])
|
.deleteAll(['accept', 'reject_deletes', 'report_removal'])
|
||||||
.reduce((acc, value) => acc || value, false);
|
.reduce((acc: boolean, value: boolean) => acc || value, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
interface IInstanceRestrictions {
|
||||||
return {
|
remoteInstance: ImmutableMap<string, any>,
|
||||||
instance: state.get('instance'),
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance }) => {
|
||||||
class InstanceRestrictions extends ImmutablePureComponent {
|
const instance = useAppSelector(state => state.instance);
|
||||||
|
|
||||||
static propTypes = {
|
const renderRestrictions = () => {
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
remoteInstance: ImmutablePropTypes.map.isRequired,
|
|
||||||
instance: ImmutablePropTypes.map,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderRestrictions = () => {
|
|
||||||
const { remoteInstance } = this.props;
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -105,10 +95,9 @@ class InstanceRestrictions extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderContent = () => {
|
const renderContent = () => {
|
||||||
const { instance, remoteInstance } = this.props;
|
|
||||||
if (!instance || !remoteInstance) return null;
|
if (!instance || !remoteInstance) return null;
|
||||||
|
|
||||||
const host = remoteInstance.get('host');
|
const host = remoteInstance.get('host');
|
||||||
|
@ -136,7 +125,7 @@ class InstanceRestrictions extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
this.renderRestrictions(),
|
renderRestrictions(),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -150,14 +139,13 @@ class InstanceRestrictions extends ImmutablePureComponent {
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<div className='py-1 pl-4 mb-4 border-solid border-l-[3px] border-gray-300 dark:border-gray-500'>
|
||||||
<div className='py-1 pl-4 mb-4 border-solid border-l-[3px] border-gray-300 dark:border-gray-500'>
|
{renderContent()}
|
||||||
{this.renderContent()}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
}
|
export default InstanceRestrictions;
|
|
@ -3,10 +3,10 @@ import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { connectHashtagStream } from 'soapbox/actions/streaming';
|
import { connectHashtagStream } from 'soapbox/actions/streaming';
|
||||||
import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines';
|
import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines';
|
||||||
import ColumnHeader from 'soapbox/components/column_header';
|
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import Timeline from 'soapbox/features/ui/components/timeline';
|
import Timeline from 'soapbox/features/ui/components/timeline';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { Tag as TagEntity } from 'soapbox/types/entities';
|
import type { Tag as TagEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
const tags = params?.tags || { any: [], all: [], none: [] };
|
const tags = params?.tags || { any: [], all: [], none: [] };
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const hasUnread = useAppSelector<boolean>(state => (state.timelines.getIn([`hashtag:${id}`, 'unread']) as number) > 0);
|
|
||||||
const disconnects = useRef<(() => void)[]>([]);
|
const disconnects = useRef<(() => void)[]>([]);
|
||||||
|
|
||||||
// Mastodon supports displaying results from multiple hashtags.
|
// Mastodon supports displaying results from multiple hashtags.
|
||||||
|
@ -100,7 +99,10 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={`#${id}`} transparent withHeader={false}>
|
<Column label={`#${id}`} transparent withHeader={false}>
|
||||||
<ColumnHeader active={hasUnread} title={title()} />
|
<div className='px-4 pt-4 sm:p-0'>
|
||||||
|
<SubNavigation message={title()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Timeline
|
<Timeline
|
||||||
scrollKey='hashtag_timeline'
|
scrollKey='hashtag_timeline'
|
||||||
timelineId={`hashtag:${id}`}
|
timelineId={`hashtag:${id}`}
|
||||||
|
|
|
@ -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,6 +1,6 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import { defineMessages, useIntl, FormattedMessage, IntlShape, MessageDescriptor } from 'react-intl';
|
import { defineMessages, useIntl, FormattedMessage, IntlShape, MessageDescriptor, defineMessage } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { mentionCompose } from 'soapbox/actions/compose';
|
import { mentionCompose } from 'soapbox/actions/compose';
|
||||||
|
@ -58,6 +58,11 @@ const icons: Record<NotificationType, string> = {
|
||||||
'pleroma:participation_accepted': require('@tabler/icons/calendar-event.svg'),
|
'pleroma:participation_accepted': require('@tabler/icons/calendar-event.svg'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nameMessage = defineMessage({
|
||||||
|
id: 'notification.name',
|
||||||
|
defaultMessage: '{link}{others}',
|
||||||
|
});
|
||||||
|
|
||||||
const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
||||||
follow: {
|
follow: {
|
||||||
id: 'notification.follow',
|
id: 'notification.follow',
|
||||||
|
@ -130,10 +135,7 @@ const buildMessage = (
|
||||||
instanceTitle: string,
|
instanceTitle: string,
|
||||||
): React.ReactNode => {
|
): React.ReactNode => {
|
||||||
const link = buildLink(account);
|
const link = buildLink(account);
|
||||||
const name = intl.formatMessage({
|
const name = intl.formatMessage(nameMessage, {
|
||||||
id: 'notification.name',
|
|
||||||
defaultMessage: '{link}{others}',
|
|
||||||
}, {
|
|
||||||
link,
|
link,
|
||||||
others: totalCount && totalCount > 0 ? (
|
others: totalCount && totalCount > 0 ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -283,7 +285,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (type) {
|
switch (type as NotificationType) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
case 'user_approved':
|
case 'user_approved':
|
||||||
return account && typeof account === 'object' ? (
|
return account && typeof account === 'object' ? (
|
||||||
|
|
|
@ -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);
|
|
|
@ -77,9 +77,9 @@ const languages = {
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.preferences', defaultMessage: 'Preferences' },
|
heading: { id: 'column.preferences', defaultMessage: 'Preferences' },
|
||||||
display_media_default: { id: 'preferences.fields.display_media.default', defaultMessage: 'Hide media marked as sensitive' },
|
displayPostsDefault: { id: 'preferences.fields.display_media.default', defaultMessage: 'Hide posts marked as sensitive' },
|
||||||
display_media_hide_all: { id: 'preferences.fields.display_media.hide_all', defaultMessage: 'Always hide media' },
|
displayPostsHideAll: { id: 'preferences.fields.display_media.hide_all', defaultMessage: 'Always hide posts' },
|
||||||
display_media_show_all: { id: 'preferences.fields.display_media.show_all', defaultMessage: 'Always show media' },
|
displayPostsShowAll: { id: 'preferences.fields.display_media.show_all', defaultMessage: 'Always show posts' },
|
||||||
privacy_public: { id: 'preferences.options.privacy_public', defaultMessage: 'Public' },
|
privacy_public: { id: 'preferences.options.privacy_public', defaultMessage: 'Public' },
|
||||||
privacy_unlisted: { id: 'preferences.options.privacy_unlisted', defaultMessage: 'Unlisted' },
|
privacy_unlisted: { id: 'preferences.options.privacy_unlisted', defaultMessage: 'Unlisted' },
|
||||||
privacy_followers_only: { id: 'preferences.options.privacy_followers_only', defaultMessage: 'Followers-only' },
|
privacy_followers_only: { id: 'preferences.options.privacy_followers_only', defaultMessage: 'Followers-only' },
|
||||||
|
@ -102,9 +102,9 @@ const Preferences = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayMediaOptions = React.useMemo(() => ({
|
const displayMediaOptions = React.useMemo(() => ({
|
||||||
default: intl.formatMessage(messages.display_media_default),
|
default: intl.formatMessage(messages.displayPostsDefault),
|
||||||
hide_all: intl.formatMessage(messages.display_media_hide_all),
|
hide_all: intl.formatMessage(messages.displayPostsHideAll),
|
||||||
show_all: intl.formatMessage(messages.display_media_show_all),
|
show_all: intl.formatMessage(messages.displayPostsShowAll),
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
const defaultPrivacyOptions = React.useMemo(() => ({
|
const defaultPrivacyOptions = React.useMemo(() => ({
|
||||||
|
@ -149,7 +149,7 @@ const Preferences = () => {
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem label={<FormattedMessage id='preferences.fields.media_display_label' defaultMessage='Media display' />}>
|
<ListItem label={<FormattedMessage id='preferences.fields.media_display_label' defaultMessage='Sensitive content' />}>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
items={displayMediaOptions}
|
items={displayMediaOptions}
|
||||||
defaultValue={settings.get('displayMedia') as string | undefined}
|
defaultValue={settings.get('displayMedia') as string | undefined}
|
||||||
|
|
|
@ -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);
|
|
|
@ -14,8 +14,6 @@ import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
|
||||||
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
||||||
import Timeline from '../ui/components/timeline';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
import ColumnSettings from './containers/column_settings_container';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.public', defaultMessage: 'Fediverse timeline' },
|
title: { id: 'column.public', defaultMessage: 'Fediverse timeline' },
|
||||||
dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' },
|
dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' },
|
||||||
|
@ -65,8 +63,12 @@ const CommunityTimeline = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
<div className='px-4 sm:p-0'>
|
||||||
|
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<PinnedHostsPicker />
|
<PinnedHostsPicker />
|
||||||
|
|
||||||
{showExplanationBox && <div className='mb-4'>
|
{showExplanationBox && <div className='mb-4'>
|
||||||
<Accordion
|
<Accordion
|
||||||
headline={<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />}
|
headline={<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />}
|
||||||
|
|
|
@ -44,7 +44,6 @@ const ScheduledStatus: React.FC<IScheduledStatus> = ({ statusId, ...other }) =>
|
||||||
|
|
||||||
<StatusContent
|
<StatusContent
|
||||||
status={status}
|
status={status}
|
||||||
expanded
|
|
||||||
collapsable
|
collapsable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -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,6 +1,3 @@
|
||||||
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 PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
@ -8,153 +5,12 @@ import Overlay from 'react-overlays/lib/Overlay';
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
|
|
||||||
|
import IconPickerMenu from './icon-picker-menu';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
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'];
|
|
||||||
|
|
||||||
@injectIntl
|
|
||||||
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
|
|
||||||
class IconPickerDropdown extends React.PureComponent {
|
class IconPickerDropdown extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -243,3 +99,5 @@ class IconPickerDropdown extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default injectIntl(IconPickerDropdown);
|
|
@ -7,6 +7,7 @@ import StatusMedia from 'soapbox/components/status-media';
|
||||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||||
import StatusContent from 'soapbox/components/status_content';
|
import StatusContent from 'soapbox/components/status_content';
|
||||||
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
||||||
|
import TranslateButton from 'soapbox/components/translate-button';
|
||||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
||||||
|
@ -29,7 +30,6 @@ interface IDetailedStatus {
|
||||||
|
|
||||||
const DetailedStatus: React.FC<IDetailedStatus> = ({
|
const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
status,
|
status,
|
||||||
onToggleHidden,
|
|
||||||
onOpenCompareHistoryModal,
|
onOpenCompareHistoryModal,
|
||||||
onToggleMediaVisibility,
|
onToggleMediaVisibility,
|
||||||
showMedia,
|
showMedia,
|
||||||
|
@ -37,10 +37,6 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const node = useRef<HTMLDivElement>(null);
|
const node = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleExpandedToggle = () => {
|
|
||||||
onToggleHidden(status);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenCompareHistoryModal = () => {
|
const handleOpenCompareHistoryModal = () => {
|
||||||
onOpenCompareHistoryModal(status);
|
onOpenCompareHistoryModal(status);
|
||||||
};
|
};
|
||||||
|
@ -51,7 +47,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
if (!account || typeof account !== 'object') return null;
|
if (!account || typeof account !== 'object') return null;
|
||||||
|
|
||||||
const isUnderReview = actualStatus.visibility === 'self';
|
const isUnderReview = actualStatus.visibility === 'self';
|
||||||
const isSensitive = actualStatus.sensitive;
|
const isSensitive = actualStatus.hidden;
|
||||||
|
|
||||||
let statusTypeIcon = null;
|
let statusTypeIcon = null;
|
||||||
|
|
||||||
|
@ -97,27 +93,31 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(isUnderReview || isSensitive) ? (
|
{(isUnderReview || isSensitive) && (
|
||||||
<SensitiveContentOverlay
|
<SensitiveContentOverlay
|
||||||
status={status}
|
status={status}
|
||||||
visible={showMedia}
|
visible={showMedia}
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
<StatusContent
|
<Stack space={4}>
|
||||||
status={actualStatus}
|
<StatusContent status={actualStatus} translatable />
|
||||||
expanded={!actualStatus.hidden}
|
|
||||||
onExpandedToggle={handleExpandedToggle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StatusMedia
|
<TranslateButton status={actualStatus} />
|
||||||
status={actualStatus}
|
|
||||||
showMedia={showMedia}
|
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{quote}
|
{(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && (
|
||||||
|
<Stack space={4}>
|
||||||
|
<StatusMedia
|
||||||
|
status={actualStatus}
|
||||||
|
showMedia={showMedia}
|
||||||
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{quote}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<HStack justifyContent='between' alignItems='center' className='py-2' wrap>
|
<HStack justifyContent='between' alignItems='center' className='py-2' wrap>
|
||||||
|
|
|
@ -134,6 +134,7 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const status = useAppSelector(state => getStatus(state, { id: props.params.statusId }));
|
const status = useAppSelector(state => getStatus(state, { id: props.params.statusId }));
|
||||||
const displayMedia = settings.get('displayMedia') as DisplayMedia;
|
const displayMedia = settings.get('displayMedia') as DisplayMedia;
|
||||||
|
const isUnderReview = status?.visibility === 'self';
|
||||||
|
|
||||||
const { ancestorsIds, descendantsIds } = useAppSelector(state => {
|
const { ancestorsIds, descendantsIds } = useAppSelector(state => {
|
||||||
let ancestorsIds = ImmutableOrderedSet<string>();
|
let ancestorsIds = ImmutableOrderedSet<string>();
|
||||||
|
@ -412,7 +413,7 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
if (next && status) {
|
if (next && status) {
|
||||||
dispatch(fetchNext(status.id, next)).then(({ next }) => {
|
dispatch(fetchNext(status.id, next)).then(({ next }) => {
|
||||||
setNext(next);
|
setNext(next);
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
}
|
}
|
||||||
}, 300, { leading: true }), [next, status]);
|
}, 300, { leading: true }), [next, status]);
|
||||||
|
|
||||||
|
@ -481,14 +482,18 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
onOpenCompareHistoryModal={handleOpenCompareHistoryModal}
|
onOpenCompareHistoryModal={handleOpenCompareHistoryModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr className='mb-2 border-t-2 dark:border-primary-800' />
|
{!isUnderReview ? (
|
||||||
|
<>
|
||||||
|
<hr className='mb-2 border-t-2 dark:border-primary-800' />
|
||||||
|
|
||||||
<StatusActionBar
|
<StatusActionBar
|
||||||
status={status}
|
status={status}
|
||||||
expandable={false}
|
expandable={false}
|
||||||
space='expand'
|
space='expand'
|
||||||
withLabels
|
withLabels
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -37,7 +37,7 @@ const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClo
|
||||||
{...compProps}
|
{...compProps}
|
||||||
rel='noopener'
|
rel='noopener'
|
||||||
data-index={i}
|
data-index={i}
|
||||||
className={classNames({ active, destructive })}
|
className={classNames('w-full', { active, destructive })}
|
||||||
data-method={isLogout ? 'delete' : null}
|
data-method={isLogout ? 'delete' : null}
|
||||||
>
|
>
|
||||||
{icon && <Icon title={text} src={icon} role='presentation' tabIndex={-1} />}
|
{icon && <Icon title={text} src={icon} role='presentation' tabIndex={-1} />}
|
||||||
|
|
|
@ -202,6 +202,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
||||||
link={link}
|
link={link}
|
||||||
alt={attachment.description}
|
alt={attachment.description}
|
||||||
key={attachment.url}
|
key={attachment.url}
|
||||||
|
visible
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attachment.type === 'audio') {
|
} else if (attachment.type === 'audio') {
|
||||||
|
|
|
@ -56,7 +56,6 @@ const SelectedStatus = ({ statusId }: { statusId: string }) => {
|
||||||
|
|
||||||
<StatusContent
|
<StatusContent
|
||||||
status={status}
|
status={status}
|
||||||
expanded
|
|
||||||
collapsable
|
collapsable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import OtpInput from 'react-otp-input';
|
import OtpInput from 'react-otp-input';
|
||||||
|
|
||||||
import { verifyCredentials } from 'soapbox/actions/auth';
|
import { verifyCredentials } from 'soapbox/actions/auth';
|
||||||
|
@ -10,6 +10,37 @@ import { FormGroup, PhoneInput, Modal, Stack, Text } from 'soapbox/components/ui
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { getAccessToken } from 'soapbox/utils/auth';
|
import { getAccessToken } from 'soapbox/utils/auth';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
verificationInvalid: {
|
||||||
|
id: 'sms_verification.invalid',
|
||||||
|
defaultMessage: 'Please enter a valid phone number.',
|
||||||
|
},
|
||||||
|
verificationSuccess: {
|
||||||
|
id: 'sms_verification.success',
|
||||||
|
defaultMessage: 'A verification code has been sent to your phone number.',
|
||||||
|
},
|
||||||
|
verificationFail: {
|
||||||
|
id: 'sms_verification.fail',
|
||||||
|
defaultMessage: 'Failed to send SMS message to your phone number.',
|
||||||
|
},
|
||||||
|
verificationExpired: {
|
||||||
|
id: 'sms_verification.expired',
|
||||||
|
defaultMessage: 'Your SMS token has expired.',
|
||||||
|
},
|
||||||
|
verifySms: {
|
||||||
|
id: 'sms_verification.modal.verify_sms',
|
||||||
|
defaultMessage: 'Verify SMS',
|
||||||
|
},
|
||||||
|
verifyNumber: {
|
||||||
|
id: 'sms_verification.modal.verify_number',
|
||||||
|
defaultMessage: 'Verify phone number',
|
||||||
|
},
|
||||||
|
verifyCode: {
|
||||||
|
id: 'sms_verification.modal.verify_code',
|
||||||
|
defaultMessage: 'Verify code',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
interface IVerifySmsModal {
|
interface IVerifySmsModal {
|
||||||
onClose: (type: string) => void,
|
onClose: (type: string) => void,
|
||||||
}
|
}
|
||||||
|
@ -47,10 +78,7 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
setStatus(Statuses.IDLE);
|
setStatus(Statuses.IDLE);
|
||||||
dispatch(
|
dispatch(
|
||||||
snackbar.error(
|
snackbar.error(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationInvalid),
|
||||||
id: 'sms_verification.invalid',
|
|
||||||
defaultMessage: 'Please enter a valid phone number.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -59,10 +87,7 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
dispatch(reRequestPhoneVerification(phone!)).then(() => {
|
dispatch(reRequestPhoneVerification(phone!)).then(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
snackbar.success(
|
snackbar.success(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationSuccess),
|
||||||
id: 'sms_verification.success',
|
|
||||||
defaultMessage: 'A verification code has been sent to your phone number.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -70,10 +95,7 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
snackbar.error(
|
snackbar.error(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationFail),
|
||||||
id: 'sms_verification.fail',
|
|
||||||
defaultMessage: 'Failed to send SMS message to your phone number.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -102,20 +124,11 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
const confirmationText = useMemo(() => {
|
const confirmationText = useMemo(() => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case Statuses.IDLE:
|
case Statuses.IDLE:
|
||||||
return intl.formatMessage({
|
return intl.formatMessage(messages.verifySms);
|
||||||
id: 'sms_verification.modal.verify_sms',
|
|
||||||
defaultMessage: 'Verify SMS',
|
|
||||||
});
|
|
||||||
case Statuses.READY:
|
case Statuses.READY:
|
||||||
return intl.formatMessage({
|
return intl.formatMessage(messages.verifyNumber);
|
||||||
id: 'sms_verification.modal.verify_number',
|
|
||||||
defaultMessage: 'Verify phone number',
|
|
||||||
});
|
|
||||||
case Statuses.REQUESTED:
|
case Statuses.REQUESTED:
|
||||||
return intl.formatMessage({
|
return intl.formatMessage(messages.verifyCode);
|
||||||
id: 'sms_verification.modal.verify_code',
|
|
||||||
defaultMessage: 'Verify code',
|
|
||||||
});
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -126,12 +139,13 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
case Statuses.IDLE:
|
case Statuses.IDLE:
|
||||||
return (
|
return (
|
||||||
<Text theme='muted'>
|
<Text theme='muted'>
|
||||||
{intl.formatMessage({
|
<FormattedMessage
|
||||||
id: 'sms_verification.modal.verify_help_text',
|
id='sms_verification.modal.verify_help_text'
|
||||||
defaultMessage: 'Verify your phone number to start using {instance}.',
|
defaultMessage='Verify your phone number to start using {instance}.'
|
||||||
}, {
|
values={{
|
||||||
instance: title,
|
instance: title,
|
||||||
})}
|
}}
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
case Statuses.READY:
|
case Statuses.READY:
|
||||||
|
@ -149,10 +163,10 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text theme='muted' size='sm' align='center'>
|
<Text theme='muted' size='sm' align='center'>
|
||||||
{intl.formatMessage({
|
<FormattedMessage
|
||||||
id: 'sms_verification.modal.enter_code',
|
id='sms_verification.modal.enter_code'
|
||||||
defaultMessage: 'We sent you a 6-digit code via SMS. Enter it below.',
|
defaultMessage='We sent you a 6-digit code via SMS. Enter it below.'
|
||||||
})}
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<OtpInput
|
<OtpInput
|
||||||
|
@ -184,10 +198,7 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
})
|
})
|
||||||
.catch(() => dispatch(
|
.catch(() => dispatch(
|
||||||
snackbar.error(
|
snackbar.error(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationExpired),
|
||||||
id: 'sms_verification.invalid',
|
|
||||||
defaultMessage: 'Your SMS token has expired.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
@ -201,10 +212,10 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
intl.formatMessage({
|
<FormattedMessage
|
||||||
id: 'sms_verification.modal.verify_title',
|
id='sms_verification.modal.verify_title'
|
||||||
defaultMessage: 'Verify your phone number',
|
defaultMessage='Verify your phone number'
|
||||||
})
|
/>
|
||||||
}
|
}
|
||||||
onClose={() => onClose('VERIFY_SMS')}
|
onClose={() => onClose('VERIFY_SMS')}
|
||||||
cancelAction={status === Statuses.IDLE ? () => onClose('VERIFY_SMS') : undefined}
|
cancelAction={status === Statuses.IDLE ? () => onClose('VERIFY_SMS') : undefined}
|
||||||
|
@ -212,10 +223,12 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||||
confirmationAction={onConfirmationClick}
|
confirmationAction={onConfirmationClick}
|
||||||
confirmationText={confirmationText}
|
confirmationText={confirmationText}
|
||||||
secondaryAction={status === Statuses.REQUESTED ? resendVerificationCode : undefined}
|
secondaryAction={status === Statuses.REQUESTED ? resendVerificationCode : undefined}
|
||||||
secondaryText={status === Statuses.REQUESTED ? intl.formatMessage({
|
secondaryText={status === Statuses.REQUESTED ? (
|
||||||
id: 'sms_verification.modal.resend_code',
|
<FormattedMessage
|
||||||
defaultMessage: 'Resend verification code?',
|
id='sms_verification.modal.resend_code'
|
||||||
}) : undefined}
|
defaultMessage='Resend verification code?'
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
secondaryDisabled={requestedAnother}
|
secondaryDisabled={requestedAnother}
|
||||||
>
|
>
|
||||||
<Stack space={4}>
|
<Stack space={4}>
|
||||||
|
|
|
@ -4,9 +4,10 @@ import Toggle from 'react-toggle';
|
||||||
|
|
||||||
import { muteAccount } from 'soapbox/actions/accounts';
|
import { muteAccount } from 'soapbox/actions/accounts';
|
||||||
import { closeModal } from 'soapbox/actions/modals';
|
import { closeModal } from 'soapbox/actions/modals';
|
||||||
import { toggleHideNotifications } from 'soapbox/actions/mutes';
|
import { toggleHideNotifications, changeMuteDuration } from 'soapbox/actions/mutes';
|
||||||
import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
|
import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import DurationSelector from 'soapbox/features/compose/components/polls/duration-selector';
|
||||||
|
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
import { makeGetAccount } from 'soapbox/selectors';
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
@ -16,12 +17,14 @@ const MuteModal = () => {
|
||||||
|
|
||||||
const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!));
|
const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!));
|
||||||
const notifications = useAppSelector((state) => state.mutes.new.notifications);
|
const notifications = useAppSelector((state) => state.mutes.new.notifications);
|
||||||
|
const duration = useAppSelector((state) => state.mutes.new.duration);
|
||||||
|
const mutesDuration = useFeatures().mutesDuration;
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
dispatch(closeModal());
|
dispatch(closeModal());
|
||||||
dispatch(muteAccount(account.id, notifications));
|
dispatch(muteAccount(account.id, notifications, duration));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
@ -32,6 +35,12 @@ const MuteModal = () => {
|
||||||
dispatch(toggleHideNotifications());
|
dispatch(toggleHideNotifications());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeMuteDuration = (expiresIn: number): void => {
|
||||||
|
dispatch(changeMuteDuration(expiresIn));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAutoExpire = () => handleChangeMuteDuration(duration ? 0 : 2 * 60 * 60 * 24);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
|
@ -69,6 +78,32 @@ const MuteModal = () => {
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{mutesDuration && (
|
||||||
|
<>
|
||||||
|
<label>
|
||||||
|
<HStack alignItems='center' space={2}>
|
||||||
|
<Text tag='span'>
|
||||||
|
<FormattedMessage id='mute_modal.auto_expire' defaultMessage='Automatically expire mute?' />
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
checked={duration !== 0}
|
||||||
|
onChange={toggleAutoExpire}
|
||||||
|
icons={false}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{duration !== 0 && (
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text weight='medium'><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </Text>
|
||||||
|
|
||||||
|
<DurationSelector onDurationChange={handleChangeMuteDuration} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -79,7 +79,6 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
||||||
|
|
||||||
<StatusContent
|
<StatusContent
|
||||||
status={status}
|
status={status}
|
||||||
expanded
|
|
||||||
collapsable
|
collapsable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl, MessageDescriptor } from 'react-intl';
|
import { useIntl, MessageDescriptor } from 'react-intl';
|
||||||
|
import { NotificationStack, NotificationObject, StyleFactoryFn } from 'react-notification';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { dismissAlert } from 'soapbox/actions/alerts';
|
import { dismissAlert } from 'soapbox/actions/alerts';
|
||||||
import { Button } from 'soapbox/components/ui';
|
import { Button } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||||
import { NotificationStack, NotificationObject, StyleFactoryFn } from 'soapbox/react-notification';
|
|
||||||
|
|
||||||
import type { Alert } from 'soapbox/reducers/alerts';
|
import type { Alert } from 'soapbox/reducers/alerts';
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,7 @@ import {
|
||||||
AuthTokenList,
|
AuthTokenList,
|
||||||
EventInformation,
|
EventInformation,
|
||||||
EventDiscussion,
|
EventDiscussion,
|
||||||
|
ServiceWorkerInfo,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { WrappedRoute } from './util/react_router_helpers';
|
import { WrappedRoute } from './util/react_router_helpers';
|
||||||
|
|
||||||
|
@ -316,6 +317,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
||||||
<WrappedRoute path='/developers/apps/create' developerOnly page={DefaultPage} component={CreateApp} content={children} />
|
<WrappedRoute path='/developers/apps/create' developerOnly page={DefaultPage} component={CreateApp} content={children} />
|
||||||
<WrappedRoute path='/developers/settings_store' developerOnly page={DefaultPage} component={SettingsStore} content={children} />
|
<WrappedRoute path='/developers/settings_store' developerOnly page={DefaultPage} component={SettingsStore} content={children} />
|
||||||
<WrappedRoute path='/developers/timeline' developerOnly page={DefaultPage} component={TestTimeline} content={children} />
|
<WrappedRoute path='/developers/timeline' developerOnly page={DefaultPage} component={TestTimeline} content={children} />
|
||||||
|
<WrappedRoute path='/developers/sw' developerOnly page={DefaultPage} component={ServiceWorkerInfo} content={children} />
|
||||||
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
||||||
<WrappedRoute path='/error/network' developerOnly page={EmptyPage} component={() => new Promise((_resolve, reject) => reject())} content={children} />
|
<WrappedRoute path='/error/network' developerOnly page={EmptyPage} component={() => new Promise((_resolve, reject) => reject())} content={children} />
|
||||||
<WrappedRoute path='/error' developerOnly page={EmptyPage} component={IntentionalError} content={children} />
|
<WrappedRoute path='/error' developerOnly page={EmptyPage} component={IntentionalError} content={children} />
|
||||||
|
|
|
@ -470,6 +470,10 @@ export function TestTimeline() {
|
||||||
return import(/* webpackChunkName: "features/test_timeline" */'../../test_timeline');
|
return import(/* webpackChunkName: "features/test_timeline" */'../../test_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ServiceWorkerInfo() {
|
||||||
|
return import(/* webpackChunkName: "features/developers" */'../../developers/service-worker-info');
|
||||||
|
}
|
||||||
|
|
||||||
export function DatePicker() {
|
export function DatePicker() {
|
||||||
return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker');
|
return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker');
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,11 @@ const messages = defineMessages({
|
||||||
tokenNotFoundBody: { id: 'email_passthru.token_not_found.body', defaultMessage: 'Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
|
tokenNotFoundBody: { id: 'email_passthru.token_not_found.body', defaultMessage: 'Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
|
||||||
tokenExpiredHeading: { id: 'email_passthru.token_expired.heading', defaultMessage: 'Token Expired' },
|
tokenExpiredHeading: { id: 'email_passthru.token_expired.heading', defaultMessage: 'Token Expired' },
|
||||||
tokenExpiredBody: { id: 'email_passthru.token_expired.body', defaultMessage: 'Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
|
tokenExpiredBody: { id: 'email_passthru.token_expired.body', defaultMessage: 'Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
|
||||||
|
emailConfirmed: { id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' },
|
||||||
|
genericFail: { id: 'email_passthru.fail.generic', defaultMessage: 'Unable to confirm your email' },
|
||||||
|
tokenExpired: { id: 'email_passthru.fail.expired', defaultMessage: 'Your email token has expired' },
|
||||||
|
tokenNotFound: { id: 'email_passthru.fail.not_found', defaultMessage: 'Your email token is invalid.' },
|
||||||
|
invalidToken: { id: 'email_passthru.fail.invalid_token', defaultMessage: 'Your token is invalid' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const Success = () => {
|
const Success = () => {
|
||||||
|
@ -116,30 +121,21 @@ const EmailPassThru = () => {
|
||||||
dispatch(confirmEmailVerification(token))
|
dispatch(confirmEmailVerification(token))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setStatus(Statuses.SUCCESS);
|
setStatus(Statuses.SUCCESS);
|
||||||
dispatch(snackbar.success(intl.formatMessage({ id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' })));
|
dispatch(snackbar.success(intl.formatMessage(messages.emailConfirmed)));
|
||||||
})
|
})
|
||||||
.catch((error: AxiosError<any>) => {
|
.catch((error: AxiosError<any>) => {
|
||||||
const errorKey = error?.response?.data?.error;
|
const errorKey = error?.response?.data?.error;
|
||||||
let message = intl.formatMessage({
|
let message = intl.formatMessage(messages.genericFail);
|
||||||
id: 'email_passthru.fail.generic',
|
|
||||||
defaultMessage: 'Unable to confirm your email',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errorKey) {
|
if (errorKey) {
|
||||||
switch (errorKey) {
|
switch (errorKey) {
|
||||||
case 'token_expired':
|
case 'token_expired':
|
||||||
message = intl.formatMessage({
|
message = intl.formatMessage(messages.tokenExpired);
|
||||||
id: 'email_passthru.fail.expired',
|
|
||||||
defaultMessage: 'Your email token has expired.',
|
|
||||||
});
|
|
||||||
setStatus(Statuses.TOKEN_EXPIRED);
|
setStatus(Statuses.TOKEN_EXPIRED);
|
||||||
break;
|
break;
|
||||||
case 'token_not_found':
|
case 'token_not_found':
|
||||||
message = intl.formatMessage({
|
message = intl.formatMessage(messages.tokenNotFound);
|
||||||
id: 'email_passthru.fail.not_found',
|
message = intl.formatMessage(messages.invalidToken);
|
||||||
defaultMessage: 'Your email token is invalid.',
|
|
||||||
});
|
|
||||||
message = 'Your token is invalid';
|
|
||||||
setStatus(Statuses.TOKEN_NOT_FOUND);
|
setStatus(Statuses.TOKEN_NOT_FOUND);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
import { checkEmailVerification, postEmailVerification, requestEmailVerification } from 'soapbox/actions/verification';
|
import { checkEmailVerification, postEmailVerification, requestEmailVerification } from 'soapbox/actions/verification';
|
||||||
|
@ -8,6 +8,13 @@ import Icon from 'soapbox/components/icon';
|
||||||
import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
|
import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
verificationSuccess: { id: 'email_verification.success', defaultMessage: 'Verification email sent successfully.' },
|
||||||
|
verificationFail: { id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' },
|
||||||
|
verificationFailTakenAlert: { id: 'emai_verifilcation.exists', defaultMessage: 'This email has already been taken.' },
|
||||||
|
verificationFailTaken: { id: 'email_verification.taken', defaultMessage: 'is taken' },
|
||||||
|
});
|
||||||
|
|
||||||
const Statuses = {
|
const Statuses = {
|
||||||
IDLE: 'IDLE',
|
IDLE: 'IDLE',
|
||||||
REQUESTED: 'REQUESTED',
|
REQUESTED: 'REQUESTED',
|
||||||
|
@ -77,26 +84,23 @@ const EmailVerification = () => {
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
snackbar.success(
|
snackbar.success(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationSuccess),
|
||||||
id: 'email_verification.exists',
|
|
||||||
defaultMessage: 'Verification email sent successfully.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error: AxiosError) => {
|
.catch((error: AxiosError) => {
|
||||||
const errorMessage = (error.response?.data as any)?.error;
|
const errorMessage = (error.response?.data as any)?.error;
|
||||||
const isEmailTaken = errorMessage === 'email_taken';
|
const isEmailTaken = errorMessage === 'email_taken';
|
||||||
let message = intl.formatMessage({ id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' });
|
let message = intl.formatMessage(messages.verificationFail);
|
||||||
|
|
||||||
if (isEmailTaken) {
|
if (isEmailTaken) {
|
||||||
message = intl.formatMessage({ id: 'email_verification.exists', defaultMessage: 'This email has already been taken.' });
|
message = intl.formatMessage(messages.verificationFailTakenAlert);
|
||||||
} else if (errorMessage) {
|
} else if (errorMessage) {
|
||||||
message = errorMessage;
|
message = errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmailTaken) {
|
if (isEmailTaken) {
|
||||||
setErrors([intl.formatMessage({ id: 'email_verification.taken', defaultMessage: 'is taken' })]);
|
setErrors([intl.formatMessage(messages.verificationFailTaken)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(snackbar.error(message));
|
dispatch(snackbar.error(message));
|
||||||
|
@ -111,7 +115,9 @@ const EmailVerification = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-800 border-solid -mx-4 sm:-mx-10'>
|
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-800 border-solid -mx-4 sm:-mx-10'>
|
||||||
<h1 className='text-center font-bold text-2xl'>{intl.formatMessage({ id: 'email_verification.header', defaultMessage: 'Enter your email address' })}</h1>
|
<h1 className='text-center font-bold text-2xl'>
|
||||||
|
<FormattedMessage id='email_verification.header' defaultMessage='Enter your email address' />
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import OtpInput from 'react-otp-input';
|
import OtpInput from 'react-otp-input';
|
||||||
|
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
|
@ -8,6 +8,13 @@ import { confirmPhoneVerification, requestPhoneVerification } from 'soapbox/acti
|
||||||
import { Button, Form, FormGroup, PhoneInput, Text } from 'soapbox/components/ui';
|
import { Button, Form, FormGroup, PhoneInput, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
verificationInvalid: { id: 'sms_verification.invalid', defaultMessage: 'Please enter a valid phone number.' },
|
||||||
|
verificationSuccess: { id: 'sms_verification.success', defaultMessage: 'A verification code has been sent to your phone number.' },
|
||||||
|
verificationFail: { id: 'sms_verification.fail', defaultMessage: 'Failed to send SMS message to your phone number.' },
|
||||||
|
verificationExpired: { id: 'sms_verification.expired', defaultMessage: 'Your SMS token has expired.' },
|
||||||
|
});
|
||||||
|
|
||||||
const Statuses = {
|
const Statuses = {
|
||||||
IDLE: 'IDLE',
|
IDLE: 'IDLE',
|
||||||
REQUESTED: 'REQUESTED',
|
REQUESTED: 'REQUESTED',
|
||||||
|
@ -38,10 +45,7 @@ const SmsVerification = () => {
|
||||||
setStatus(Statuses.IDLE);
|
setStatus(Statuses.IDLE);
|
||||||
dispatch(
|
dispatch(
|
||||||
snackbar.error(
|
snackbar.error(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationInvalid),
|
||||||
id: 'sms_verification.invalid',
|
|
||||||
defaultMessage: 'Please enter a valid phone number.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -50,18 +54,12 @@ const SmsVerification = () => {
|
||||||
dispatch(requestPhoneVerification(phone!)).then(() => {
|
dispatch(requestPhoneVerification(phone!)).then(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
snackbar.success(
|
snackbar.success(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationSuccess),
|
||||||
id: 'sms_verification.success',
|
|
||||||
defaultMessage: 'A verification code has been sent to your phone number.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setStatus(Statuses.REQUESTED);
|
setStatus(Statuses.REQUESTED);
|
||||||
}).catch((error: AxiosError) => {
|
}).catch((error: AxiosError) => {
|
||||||
const message = (error.response?.data as any)?.message || intl.formatMessage({
|
const message = (error.response?.data as any)?.message || intl.formatMessage(messages.verificationFail);
|
||||||
id: 'sms_verification.fail',
|
|
||||||
defaultMessage: 'Failed to send SMS message to your phone number.',
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(snackbar.error(message));
|
dispatch(snackbar.error(message));
|
||||||
setStatus(Statuses.FAIL);
|
setStatus(Statuses.FAIL);
|
||||||
|
@ -78,10 +76,7 @@ const SmsVerification = () => {
|
||||||
dispatch(confirmPhoneVerification(verificationCode))
|
dispatch(confirmPhoneVerification(verificationCode))
|
||||||
.catch(() => dispatch(
|
.catch(() => dispatch(
|
||||||
snackbar.error(
|
snackbar.error(
|
||||||
intl.formatMessage({
|
intl.formatMessage(messages.verificationExpired),
|
||||||
id: 'sms_verification.invalid',
|
|
||||||
defaultMessage: 'Your SMS token has expired.',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
@ -97,7 +92,7 @@ const SmsVerification = () => {
|
||||||
<div>
|
<div>
|
||||||
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-800 border-solid -mx-4 sm:-mx-10'>
|
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-800 border-solid -mx-4 sm:-mx-10'>
|
||||||
<h1 className='text-center font-bold text-2xl'>
|
<h1 className='text-center font-bold text-2xl'>
|
||||||
{intl.formatMessage({ id: 'sms_verification.sent.header', defaultMessage: 'Verification code' })}
|
<FormattedMessage id='sms_verification.sent.header' defaultMessage='Verification code' />
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -136,7 +131,9 @@ const SmsVerification = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-800 border-solid -mx-4 sm:-mx-10'>
|
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-800 border-solid -mx-4 sm:-mx-10'>
|
||||||
<h1 className='text-center font-bold text-2xl'>{intl.formatMessage({ id: 'sms_verification.header', defaultMessage: 'Enter your phone number' })}</h1>
|
<h1 className='text-center font-bold text-2xl'>
|
||||||
|
<FormattedMessage id='sms_verification.header' defaultMessage='Enter your phone number' />
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -288,7 +288,7 @@
|
||||||
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
|
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
|
||||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
"compose_form.spoiler_placeholder": "Write your warning here (optional)",
|
||||||
"confirmation_modal.cancel": "Cancel",
|
"confirmation_modal.cancel": "Cancel",
|
||||||
"confirmations.admin.deactivate_user.confirm": "Deactivate @{name}",
|
"confirmations.admin.deactivate_user.confirm": "Deactivate @{name}",
|
||||||
"confirmations.admin.deactivate_user.heading": "Deactivate @{acct}",
|
"confirmations.admin.deactivate_user.heading": "Deactivate @{acct}",
|
||||||
|
@ -794,12 +794,12 @@
|
||||||
"preferences.fields.autoload_timelines_label": "Automatically load new posts when scrolled to the top of the page",
|
"preferences.fields.autoload_timelines_label": "Automatically load new posts when scrolled to the top of the page",
|
||||||
"preferences.fields.boost_modal_label": "Show confirmation dialog before reposting",
|
"preferences.fields.boost_modal_label": "Show confirmation dialog before reposting",
|
||||||
"preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post",
|
"preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post",
|
||||||
"preferences.fields.display_media.default": "Hide media marked as sensitive",
|
"preferences.fields.display_media.default": "Hide posts marked as sensitive",
|
||||||
"preferences.fields.display_media.hide_all": "Always hide media",
|
"preferences.fields.display_media.hide_all": "Always hide posts",
|
||||||
"preferences.fields.display_media.show_all": "Always show media",
|
"preferences.fields.display_media.show_all": "Always show posts",
|
||||||
"preferences.fields.expand_spoilers_label": "Always expand posts marked with content warnings",
|
"preferences.fields.expand_spoilers_label": "Always expand posts marked with content warnings",
|
||||||
"preferences.fields.language_label": "Display Language",
|
"preferences.fields.language_label": "Display Language",
|
||||||
"preferences.fields.media_display_label": "Media display",
|
"preferences.fields.media_display_label": "Sensitive content",
|
||||||
"preferences.hints.feed": "In your home feed",
|
"preferences.hints.feed": "In your home feed",
|
||||||
"privacy.change": "Adjust post privacy",
|
"privacy.change": "Adjust post privacy",
|
||||||
"privacy.direct.long": "Post to mentioned users only",
|
"privacy.direct.long": "Post to mentioned users only",
|
||||||
|
|
|
@ -915,7 +915,7 @@
|
||||||
"preferences.fields.expand_spoilers_label": "Zawsze rozwijaj wpisy z ostrzeżeniami o zawartości",
|
"preferences.fields.expand_spoilers_label": "Zawsze rozwijaj wpisy z ostrzeżeniami o zawartości",
|
||||||
"preferences.fields.language_label": "Język",
|
"preferences.fields.language_label": "Język",
|
||||||
"preferences.fields.media_display_label": "Wyświetlanie zawartości multimedialnej",
|
"preferences.fields.media_display_label": "Wyświetlanie zawartości multimedialnej",
|
||||||
"preferences.fields.missing_description_modal_label": "SPokazuj prośbę o potwierdzenie przed wysłaniem wpisu bez opisu multimediów",
|
"preferences.fields.missing_description_modal_label": "Pokazuj prośbę o potwierdzenie przed wysłaniem wpisu bez opisu multimediów",
|
||||||
"preferences.fields.privacy_label": "Prywatność wpisów",
|
"preferences.fields.privacy_label": "Prywatność wpisów",
|
||||||
"preferences.fields.reduce_motion_label": "Ogranicz ruch w animacjach",
|
"preferences.fields.reduce_motion_label": "Ogranicz ruch w animacjach",
|
||||||
"preferences.fields.system_font_label": "Używaj domyślnej czcionki systemu",
|
"preferences.fields.system_font_label": "Używaj domyślnej czcionki systemu",
|
||||||
|
@ -1211,8 +1211,11 @@
|
||||||
"status.show_less_all": "Zwiń wszystkie",
|
"status.show_less_all": "Zwiń wszystkie",
|
||||||
"status.show_more": "Rozwiń",
|
"status.show_more": "Rozwiń",
|
||||||
"status.show_more_all": "Rozwiń wszystkie",
|
"status.show_more_all": "Rozwiń wszystkie",
|
||||||
|
"status.show_original": "Pokaż oryginalny wpis",
|
||||||
"status.title": "Wpis",
|
"status.title": "Wpis",
|
||||||
"status.title_direct": "Wiadomość bezpośrednia",
|
"status.title_direct": "Wiadomość bezpośrednia",
|
||||||
|
"status.translated_from_with": "Przetłumaczono z {lang} z użyciem {provider}",
|
||||||
|
"status.translate": "Przetłumacz wpis",
|
||||||
"status.unbookmark": "Usuń z zakładek",
|
"status.unbookmark": "Usuń z zakładek",
|
||||||
"status.unbookmarked": "Usunięto z zakładek.",
|
"status.unbookmarked": "Usunięto z zakładek.",
|
||||||
"status.unmute_conversation": "Cofnij wyciszenie konwersacji",
|
"status.unmute_conversation": "Cofnij wyciszenie konwersacji",
|
||||||
|
@ -1255,6 +1258,7 @@
|
||||||
"time_remaining.seconds": "{number, plural, one {Pozostała # sekunda} few {Pozostały # sekundy} many {Pozostało # sekund} other {Pozostało # sekund}}",
|
"time_remaining.seconds": "{number, plural, one {Pozostała # sekunda} few {Pozostały # sekundy} many {Pozostało # sekund} other {Pozostało # sekund}}",
|
||||||
"trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym",
|
"trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym",
|
||||||
"trends.title": "Trendy",
|
"trends.title": "Trendy",
|
||||||
|
"trendsPanel.viewAll": "Pokaż wszystkie",
|
||||||
"ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Soapbox.",
|
"ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Soapbox.",
|
||||||
"unauthorized_modal.footer": "Masz już konto? {login}.",
|
"unauthorized_modal.footer": "Masz już konto? {login}.",
|
||||||
"unauthorized_modal.text": "Musisz się zalogować, aby to zrobić.",
|
"unauthorized_modal.text": "Musisz się zalogować, aby to zrobić.",
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
[
|
||||||
|
]
|
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
|
@ -192,4 +192,12 @@ describe('normalizeInstance()', () => {
|
||||||
const result = normalizeInstance(instance);
|
const result = normalizeInstance(instance);
|
||||||
expect(result.title).toBe('pixelfed');
|
expect(result.title).toBe('pixelfed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renames Akkoma to Pleroma', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/akkoma-instance.json');
|
||||||
|
const result = normalizeInstance(instance);
|
||||||
|
|
||||||
|
expect(result.version).toEqual('2.7.2 (compatible; Pleroma 2.4.5+akkoma)');
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,6 +42,7 @@ export const AccountRecord = ImmutableRecord({
|
||||||
location: '',
|
location: '',
|
||||||
locked: false,
|
locked: false,
|
||||||
moved: null as EmbeddedEntity<any>,
|
moved: null as EmbeddedEntity<any>,
|
||||||
|
mute_expires_at: null as string | null,
|
||||||
note: '',
|
note: '',
|
||||||
pleroma: ImmutableMap<string, any>(),
|
pleroma: ImmutableMap<string, any>(),
|
||||||
source: ImmutableMap<string, any>(),
|
source: ImmutableMap<string, any>(),
|
||||||
|
|
|
@ -98,6 +98,17 @@ const normalizeVersion = (instance: ImmutableMap<string, any>) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Rename Akkoma to Pleroma+akkoma */
|
||||||
|
const fixAkkoma = (instance: ImmutableMap<string, any>) => {
|
||||||
|
const version: string = instance.get('version', '');
|
||||||
|
|
||||||
|
if (version.includes('Akkoma')) {
|
||||||
|
return instance.set('version', '2.7.2 (compatible; Pleroma 2.4.5+akkoma)');
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
|
// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
|
||||||
export const normalizeInstance = (instance: Record<string, any>) => {
|
export const normalizeInstance = (instance: Record<string, any>) => {
|
||||||
return InstanceRecord(
|
return InstanceRecord(
|
||||||
|
@ -117,6 +128,7 @@ export const normalizeInstance = (instance: Record<string, any>) => {
|
||||||
|
|
||||||
// Normalize version
|
// Normalize version
|
||||||
normalizeVersion(instance);
|
normalizeVersion(instance);
|
||||||
|
fixAkkoma(instance);
|
||||||
|
|
||||||
// Merge defaults
|
// Merge defaults
|
||||||
instance.mergeDeepWith(mergeDefined, InstanceRecord());
|
instance.mergeDeepWith(mergeDefined, InstanceRecord());
|
||||||
|
|
|
@ -77,6 +77,7 @@ export const StatusRecord = ImmutableRecord({
|
||||||
hidden: false,
|
hidden: false,
|
||||||
search_index: '',
|
search_index: '',
|
||||||
spoilerHtml: '',
|
spoilerHtml: '',
|
||||||
|
translation: null as ImmutableMap<string, string> | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeAttachments = (status: ImmutableMap<string, any>) => {
|
const normalizeAttachments = (status: ImmutableMap<string, any>) => {
|
||||||
|
@ -163,6 +164,13 @@ const fixFiltered = (status: ImmutableMap<string, any>) => {
|
||||||
status.delete('filtered');
|
status.delete('filtered');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** If the status contains spoiler text, treat it as sensitive. */
|
||||||
|
const fixSensitivity = (status: ImmutableMap<string, any>) => {
|
||||||
|
if (status.get('spoiler_text')) {
|
||||||
|
status.set('sensitive', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Normalize event
|
// Normalize event
|
||||||
const normalizeEvent = (status: ImmutableMap<string, any>) => {
|
const normalizeEvent = (status: ImmutableMap<string, any>) => {
|
||||||
if (status.getIn(['pleroma', 'event'])) {
|
if (status.getIn(['pleroma', 'event'])) {
|
||||||
|
@ -184,6 +192,7 @@ export const normalizeStatus = (status: Record<string, any>) => {
|
||||||
addSelfMention(status);
|
addSelfMention(status);
|
||||||
fixQuote(status);
|
fixQuote(status);
|
||||||
fixFiltered(status);
|
fixFiltered(status);
|
||||||
|
fixSensitivity(status);
|
||||||
normalizeEvent(status);
|
normalizeEvent(status);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
* @module soapbox/precheck
|
* @module soapbox/precheck
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Whether pre-rendered data exists in Mastodon's format. */
|
/** Whether pre-rendered data exists in Pleroma's format. */
|
||||||
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
|
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
|
||||||
|
|
||||||
/** Whether pre-rendered data exists in Pleroma's format. */
|
/** Whether pre-rendered data exists in Mastodon's format. */
|
||||||
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
|
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
|
||||||
|
|
||||||
/** Whether initial data was loaded into the page by server-side-rendering (SSR). */
|
/** Whether initial data was loaded into the page by server-side-rendering (SSR). */
|
||||||
|
|
|
@ -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;
|
|
|
@ -187,30 +187,8 @@ describe('compose reducer', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => {
|
|
||||||
const state = initialState.set('home', ReducerCompose({ spoiler: true, sensitive: true, idempotencyKey: '' }));
|
|
||||||
const action = {
|
|
||||||
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
|
||||||
id: 'home',
|
|
||||||
};
|
|
||||||
expect(reducer(state, action).toJS().home).toMatchObject({
|
|
||||||
sensitive: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => {
|
|
||||||
const state = initialState.set('home', ReducerCompose({ spoiler: false, sensitive: true }));
|
|
||||||
const action = {
|
|
||||||
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
|
||||||
id: 'home',
|
|
||||||
};
|
|
||||||
expect(reducer(state, action).toJS().home).toMatchObject({
|
|
||||||
sensitive: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => {
|
it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => {
|
||||||
const state = initialState.set('home', ReducerCompose({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: ImmutableList() }));
|
const state = initialState.set('home', ReducerCompose({ spoiler_text: 'spoiler text', spoiler: true, sensitive: true, media_attachments: ImmutableList() }));
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.COMPOSE_SPOILERNESS_CHANGE,
|
type: actions.COMPOSE_SPOILERNESS_CHANGE,
|
||||||
id: 'home',
|
id: 'home',
|
||||||
|
@ -218,6 +196,7 @@ describe('compose reducer', () => {
|
||||||
expect(reducer(state, action).toJS().home).toMatchObject({
|
expect(reducer(state, action).toJS().home).toMatchObject({
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
spoiler_text: '',
|
spoiler_text: '',
|
||||||
|
sensitive: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue