sforkowany z mirror/soapbox
Merge remote-tracking branch 'origin/develop' into a11y
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>strip-front-mentions
commit
149f677ab4
13
.eslintrc.js
13
.eslintrc.js
|
@ -67,8 +67,12 @@ module.exports = {
|
|||
'consistent-return': 'error',
|
||||
'dot-notation': 'error',
|
||||
eqeqeq: 'error',
|
||||
indent: ['warn', 2],
|
||||
indent: ['error', 2],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'key-spacing': [
|
||||
'error',
|
||||
{ mode: 'minimum' },
|
||||
],
|
||||
'no-catch-shadow': 'error',
|
||||
'no-cond-assign': 'error',
|
||||
'no-console': [
|
||||
|
@ -111,6 +115,13 @@ module.exports = {
|
|||
'prefer-const': 'error',
|
||||
quotes: ['error', 'single'],
|
||||
semi: 'error',
|
||||
'space-unary-ops': [
|
||||
'error',
|
||||
{
|
||||
words: true,
|
||||
nonwords: false,
|
||||
},
|
||||
],
|
||||
strict: 'off',
|
||||
'valid-typeof': 'error',
|
||||
|
||||
|
|
|
@ -12,3 +12,12 @@ yarn-error.log*
|
|||
/static-test/
|
||||
/public/
|
||||
/dist/
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
# surge.sh
|
||||
CNAME
|
||||
AUTH
|
||||
CORS
|
||||
ROUTER
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link href="/manifest.json" rel="manifest">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
</head>
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
"confirmations.delete_list.confirm": "Delete",
|
||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||
"confirmations.domain_block.confirm": "Hide entire domain",
|
||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
|
||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.",
|
||||
"confirmations.mute.confirm": "Mute",
|
||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||
"confirmations.redraft.confirm": "Delete & redraft",
|
||||
|
@ -584,7 +584,7 @@
|
|||
"confirmations.delete_list.confirm": "Delete",
|
||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||
"confirmations.domain_block.confirm": "Hide entire domain",
|
||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
|
||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.",
|
||||
"confirmations.mute.confirm": "Mute",
|
||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||
"confirmations.redraft.confirm": "Delete & redraft",
|
||||
|
|
|
@ -59,6 +59,10 @@ export const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST';
|
|||
export const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS';
|
||||
export const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL';
|
||||
|
||||
export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
|
||||
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
|
||||
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
|
||||
|
||||
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
|
||||
export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
|
||||
export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
|
||||
|
@ -144,8 +148,16 @@ export function fetchAccountByUsername(username) {
|
|||
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
const me = state.get('me');
|
||||
|
||||
if (features.accountByUsername) {
|
||||
if (!me && features.accountLookup) {
|
||||
dispatch(accountLookup(username)).then(account => {
|
||||
dispatch(fetchAccountSuccess(account));
|
||||
}).catch(error => {
|
||||
dispatch(fetchAccountFail(null, error));
|
||||
dispatch(importErrorWhileFetchingAccountByUsername(username));
|
||||
});
|
||||
} else if (features.accountByUsername) {
|
||||
api(getState).get(`/api/v1/accounts/${username}`).then(response => {
|
||||
dispatch(fetchRelationships([response.data.id]));
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
|
@ -961,3 +973,17 @@ export function accountSearch(params, cancelToken) {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function accountLookup(acct, cancelToken) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct });
|
||||
return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => {
|
||||
if (account && account.id) dispatch(importFetchedAccount(account));
|
||||
dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account });
|
||||
return account;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ACCOUNT_LOOKUP_FAIL });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -62,6 +62,14 @@ export const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GR
|
|||
export const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
||||
export const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
||||
|
||||
export const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST';
|
||||
export const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS';
|
||||
export const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL';
|
||||
|
||||
export const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
||||
export const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||
export const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
||||
|
||||
const nicknamesFromIds = (getState, ids) => ids.map(id => getState().getIn(['accounts', id, 'acct']));
|
||||
|
||||
export function fetchConfig() {
|
||||
|
@ -319,3 +327,31 @@ export function demoteToUser(accountId) {
|
|||
]);
|
||||
};
|
||||
}
|
||||
|
||||
export function suggestUsers(accountIds) {
|
||||
return (dispatch, getState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds });
|
||||
return api(getState)
|
||||
.patch('/api/pleroma/admin/users/suggest', { nicknames })
|
||||
.then(({ data: { users } }) => {
|
||||
dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unsuggestUsers(accountIds) {
|
||||
return (dispatch, getState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds });
|
||||
return api(getState)
|
||||
.patch('/api/pleroma/admin/users/unsuggest', { nicknames })
|
||||
.then(({ data: { users } }) => {
|
||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -186,7 +186,7 @@ export function loadCredentials(token, accountUrl) {
|
|||
|
||||
export function logIn(intl, username, password) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch(createAppAndToken()).then(() => {
|
||||
return dispatch(createAuthApp()).then(() => {
|
||||
return dispatch(createUserToken(username, password));
|
||||
}).catch(error => {
|
||||
if (error.response.data.error === 'mfa_required') {
|
||||
|
|
|
@ -9,15 +9,17 @@ export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_RE
|
|||
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
|
||||
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
|
||||
|
||||
const noOp = () => new Promise(f => f());
|
||||
|
||||
export function fetchBookmarkedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||
return;
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(fetchBookmarkedStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/bookmarks').then(response => {
|
||||
return api(getState).get('/api/v1/bookmarks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
@ -53,12 +55,12 @@ export function expandBookmarkedStatuses() {
|
|||
const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);
|
||||
|
||||
if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||
return;
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(expandBookmarkedStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
|
@ -7,6 +8,10 @@ export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
|
|||
export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
|
||||
export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
|
||||
|
||||
export const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST';
|
||||
export const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS';
|
||||
export const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL';
|
||||
|
||||
export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
|
||||
export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
|
||||
export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
|
||||
|
@ -27,14 +32,61 @@ export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
|
|||
export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
|
||||
export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
|
||||
|
||||
export function fetchChats() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: CHATS_FETCH_REQUEST });
|
||||
return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => {
|
||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: data });
|
||||
export function fetchChatsV1() {
|
||||
return (dispatch, getState) =>
|
||||
api(getState).get('/api/v1/pleroma/chats').then((response) => {
|
||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchChatsV2() {
|
||||
return (dispatch, getState) =>
|
||||
api(getState).get('/api/v2/pleroma/chats').then((response) => {
|
||||
let next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
if (!next && response.data.length) {
|
||||
next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` };
|
||||
}
|
||||
|
||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data, next: next ? next.uri : null });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchChats() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
|
||||
dispatch({ type: CHATS_FETCH_REQUEST });
|
||||
if (features.chatsV2) {
|
||||
dispatch(fetchChatsV2());
|
||||
} else {
|
||||
dispatch(fetchChatsV1());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function expandChats() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['chats', 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: CHATS_EXPAND_REQUEST });
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch({ type: CHATS_EXPAND_SUCCESS, chats: response.data, next: next ? next.uri : null });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHATS_EXPAND_FAIL, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -140,7 +192,7 @@ export function startChat(accountId) {
|
|||
|
||||
export function markChatRead(chatId, lastReadId) {
|
||||
return (dispatch, getState) => {
|
||||
const chat = getState().getIn(['chats', chatId]);
|
||||
const chat = getState().getIn(['chats', 'items', chatId]);
|
||||
if (!lastReadId) lastReadId = chat.get('last_message');
|
||||
|
||||
if (chat.get('unread') < 1) return;
|
||||
|
|
|
@ -68,6 +68,9 @@ export const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
|
|||
export const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
|
||||
export const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
|
||||
|
||||
export const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
|
||||
export const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
|
||||
|
||||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
|
@ -93,10 +96,14 @@ export function changeCompose(text) {
|
|||
export function replyCompose(status, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const instance = state.get('instance');
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_REPLY,
|
||||
status: status,
|
||||
account: state.getIn(['accounts', state.get('me')]),
|
||||
explicitAddressing,
|
||||
});
|
||||
|
||||
dispatch(openModal('COMPOSE'));
|
||||
|
@ -183,6 +190,7 @@ export function submitCompose(routerHistory, force = false) {
|
|||
|
||||
const status = state.getIn(['compose', 'text'], '');
|
||||
const media = state.getIn(['compose', 'media_attachments']);
|
||||
let to = state.getIn(['compose', 'to'], null);
|
||||
|
||||
if (!validateSchedule(state)) {
|
||||
dispatch(snackbar.error(messages.scheduleError));
|
||||
|
@ -200,6 +208,13 @@ export function submitCompose(routerHistory, force = false) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (to && status) {
|
||||
const mentions = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/g); // not a perfect regex
|
||||
|
||||
if (mentions)
|
||||
to = to.union(mentions.map(mention => mention.trim().slice(1)));
|
||||
}
|
||||
|
||||
dispatch(submitComposeRequest());
|
||||
dispatch(closeModal());
|
||||
|
||||
|
@ -215,6 +230,7 @@ export function submitCompose(routerHistory, force = false) {
|
|||
content_type: state.getIn(['compose', 'content_type']),
|
||||
poll: state.getIn(['compose', 'poll'], null),
|
||||
scheduled_at: state.getIn(['compose', 'schedule'], null),
|
||||
to,
|
||||
};
|
||||
|
||||
dispatch(createStatus(params, idempotencyKey)).then(function(data) {
|
||||
|
@ -643,3 +659,27 @@ export function openComposeWithText(text = '') {
|
|||
dispatch(changeCompose(text));
|
||||
};
|
||||
}
|
||||
|
||||
export function addToMentions(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const acct = state.getIn(['accounts', accountId, 'acct']);
|
||||
|
||||
return dispatch({
|
||||
type: COMPOSE_ADD_TO_MENTIONS,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeFromMentions(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const acct = state.getIn(['accounts', accountId, 'acct']);
|
||||
|
||||
return dispatch({
|
||||
type: COMPOSE_REMOVE_FROM_MENTIONS,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import api from '../api';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
||||
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
|
||||
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
|
||||
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
|
||||
|
||||
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
|
||||
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
|
||||
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
|
||||
|
||||
export const fetchDirectory = params => (dispatch, getState) => {
|
||||
dispatch(fetchDirectoryRequest());
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||
}).catch(error => dispatch(fetchDirectoryFail(error)));
|
||||
};
|
||||
|
||||
export const fetchDirectoryRequest = () => ({
|
||||
type: DIRECTORY_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchDirectorySuccess = accounts => ({
|
||||
type: DIRECTORY_FETCH_SUCCESS,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const fetchDirectoryFail = error => ({
|
||||
type: DIRECTORY_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const expandDirectory = params => (dispatch, getState) => {
|
||||
dispatch(expandDirectoryRequest());
|
||||
|
||||
const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||
}).catch(error => dispatch(expandDirectoryFail(error)));
|
||||
};
|
||||
|
||||
export const expandDirectoryRequest = () => ({
|
||||
type: DIRECTORY_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
export const expandDirectorySuccess = accounts => ({
|
||||
type: DIRECTORY_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const expandDirectoryFail = error => ({
|
||||
type: DIRECTORY_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
|
@ -22,7 +22,7 @@ const getMeUrl = state => {
|
|||
};
|
||||
|
||||
// Figure out the appropriate instance to fetch depending on the state
|
||||
const getHost = state => {
|
||||
export const getHost = state => {
|
||||
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
|
||||
|
||||
try {
|
||||
|
|
|
@ -48,6 +48,10 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
|||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||
|
||||
export const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST';
|
||||
export const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS';
|
||||
export const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL';
|
||||
|
||||
const messages = defineMessages({
|
||||
bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
|
||||
bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
|
||||
|
@ -77,7 +81,6 @@ export function unreblog(status) {
|
|||
dispatch(unreblogRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unreblogSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(unreblogFail(status, error));
|
||||
|
@ -157,7 +160,6 @@ export function unfavourite(status) {
|
|||
dispatch(unfavouriteRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unfavouriteSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(unfavouriteFail(status, error));
|
||||
|
@ -477,3 +479,46 @@ export function unpinFail(status, error) {
|
|||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function remoteInteraction(ap_id, profile) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(remoteInteractionRequest(ap_id, profile));
|
||||
|
||||
return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => {
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
dispatch(remoteInteractionSuccess(ap_id, profile, data.url));
|
||||
|
||||
return data.url;
|
||||
}).catch(error => {
|
||||
dispatch(remoteInteractionFail(ap_id, profile, error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function remoteInteractionRequest(ap_id, profile) {
|
||||
return {
|
||||
type: REMOTE_INTERACTION_REQUEST,
|
||||
ap_id,
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
export function remoteInteractionSuccess(ap_id, profile, url) {
|
||||
return {
|
||||
type: REMOTE_INTERACTION_SUCCESS,
|
||||
ap_id,
|
||||
profile,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
export function remoteInteractionFail(ap_id, profile, error) {
|
||||
return {
|
||||
type: REMOTE_INTERACTION_FAIL,
|
||||
ap_id,
|
||||
profile,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -367,7 +367,7 @@ export const fetchAccountLists = accountId => (dispatch, getState) => {
|
|||
};
|
||||
|
||||
export const fetchAccountListsRequest = id => ({
|
||||
type:LIST_ADDER_LISTS_FETCH_REQUEST,
|
||||
type: LIST_ADDER_LISTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ const messages = defineMessages({
|
|||
userDeleted: { id: 'admin.users.user_deleted_message', defaultMessage: '@{acct} was deleted' },
|
||||
deleteStatusPrompt: { id: 'confirmations.admin.delete_status.message', defaultMessage: 'You are about to delete a post by @{acct}. This action cannot be undone.' },
|
||||
deleteStatusConfirm: { id: 'confirmations.admin.delete_status.confirm', defaultMessage: 'Delete post' },
|
||||
rejectUserPrompt: { id: 'confirmations.admin.reject_user.message', defaultMessage: 'You are about to reject @{acct} registration request. This action cannot be undone.' },
|
||||
rejectUserConfirm: { id: 'confirmations.admin.reject_user.confirm', defaultMessage: 'Reject @{name}' },
|
||||
statusDeleted: { id: 'admin.statuses.status_deleted_message', defaultMessage: 'Post by @{acct} was deleted' },
|
||||
markStatusSensitivePrompt: { id: 'confirmations.admin.mark_status_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} sensitive.' },
|
||||
markStatusNotSensitivePrompt: { id: 'confirmations.admin.mark_status_not_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} not sensitive.' },
|
||||
|
@ -85,6 +87,26 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
|
|||
};
|
||||
}
|
||||
|
||||
export function rejectUserModal(intl, accountId, afterConfirm = () => {}) {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
const acct = state.getIn(['accounts', accountId, 'acct']);
|
||||
const name = state.getIn(['accounts', accountId, 'username']);
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
|
||||
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteUsers([accountId]))
|
||||
.then(() => {
|
||||
afterConfirm();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterConfirm = () => {}) {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
|
|
|
@ -2,6 +2,7 @@ import { decode as decodeBase64 } from '../../utils/base64';
|
|||
import { pushNotificationsSetting } from '../../settings';
|
||||
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
||||
import { createPushSubsription, updatePushSubscription } from 'soapbox/actions/push_subscriptions';
|
||||
import { getVapidKey } from 'soapbox/utils/auth';
|
||||
|
||||
// Taken from https://www.npmjs.com/package/web-push
|
||||
const urlBase64ToUint8Array = (base64String) => {
|
||||
|
@ -13,11 +14,6 @@ const urlBase64ToUint8Array = (base64String) => {
|
|||
return decodeBase64(base64);
|
||||
};
|
||||
|
||||
const getVapidKey = getState => {
|
||||
const state = getState();
|
||||
return state.getIn(['auth', 'app', 'vapid_key']) || state.getIn(['instance', 'pleroma', 'vapid_public_key']);
|
||||
};
|
||||
|
||||
const getRegistration = () => navigator.serviceWorker.ready;
|
||||
|
||||
const getPushSubscription = (registration) =>
|
||||
|
@ -27,7 +23,7 @@ const getPushSubscription = (registration) =>
|
|||
const subscribe = (registration, getState) =>
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState)),
|
||||
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())),
|
||||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }) =>
|
||||
|
@ -35,7 +31,8 @@ const unsubscribe = ({ registration, subscription }) =>
|
|||
|
||||
const sendSubscriptionToBackend = (subscription, me) => {
|
||||
return (dispatch, getState) => {
|
||||
const params = { subscription };
|
||||
const alerts = getState().getIn(['push_notifications', 'alerts']).toJS();
|
||||
const params = { subscription, data: { alerts } };
|
||||
|
||||
if (me) {
|
||||
const data = pushNotificationsSetting.get(me);
|
||||
|
@ -54,7 +51,7 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager'
|
|||
export function register() {
|
||||
return (dispatch, getState) => {
|
||||
const me = getState().get('me');
|
||||
const vapidKey = getVapidKey(getState);
|
||||
const vapidKey = getVapidKey(getState());
|
||||
|
||||
dispatch(setBrowserSupport(supportsPushNotifications));
|
||||
|
||||
|
@ -105,6 +102,7 @@ export function register() {
|
|||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
if (error.code === 20 && error.name === 'AbortError') {
|
||||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
|
||||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
|
||||
|
|
|
@ -17,9 +17,16 @@ export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
|||
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
||||
|
||||
export function changeSearch(value) {
|
||||
return {
|
||||
type: SEARCH_CHANGE,
|
||||
value,
|
||||
return (dispatch, getState) => {
|
||||
// If backspaced all the way, clear the search
|
||||
if (value.length === 0) {
|
||||
return dispatch(clearSearch());
|
||||
} else {
|
||||
return dispatch({
|
||||
type: SEARCH_CHANGE,
|
||||
value,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -29,10 +36,12 @@ export function clearSearch() {
|
|||
};
|
||||
}
|
||||
|
||||
export function submitSearch() {
|
||||
export function submitSearch(filter) {
|
||||
return (dispatch, getState) => {
|
||||
const value = getState().getIn(['search', 'value']);
|
||||
const type = filter || getState().getIn(['search', 'filter'], 'accounts');
|
||||
|
||||
// An empty search doesn't return any results
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -44,6 +53,7 @@ export function submitSearch() {
|
|||
q: value,
|
||||
resolve: true,
|
||||
limit: 20,
|
||||
type,
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.data.accounts) {
|
||||
|
@ -54,7 +64,7 @@ export function submitSearch() {
|
|||
dispatch(importFetchedStatuses(response.data.statuses));
|
||||
}
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data));
|
||||
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchSearchFail(error));
|
||||
|
@ -69,10 +79,12 @@ export function fetchSearchRequest(value) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchSearchSuccess(results) {
|
||||
export function fetchSearchSuccess(results, searchTerm, searchType) {
|
||||
return {
|
||||
type: SEARCH_FETCH_SUCCESS,
|
||||
results,
|
||||
searchTerm,
|
||||
searchType,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -83,13 +95,17 @@ export function fetchSearchFail(error) {
|
|||
};
|
||||
}
|
||||
|
||||
export const setFilter = filterType => dispatch => {
|
||||
dispatch({
|
||||
type: SEARCH_FILTER_SET,
|
||||
path: ['search', 'filter'],
|
||||
value: filterType,
|
||||
});
|
||||
};
|
||||
export function setFilter(filterType) {
|
||||
return (dispatch) => {
|
||||
dispatch(submitSearch(filterType));
|
||||
|
||||
dispatch({
|
||||
type: SEARCH_FILTER_SET,
|
||||
path: ['search', 'filter'],
|
||||
value: filterType,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const expandSearch = type => (dispatch, getState) => {
|
||||
const value = getState().getIn(['search', 'value']);
|
||||
|
|
|
@ -38,6 +38,8 @@ export const defaultSettings = ImmutableMap({
|
|||
dyslexicFont: false,
|
||||
demetricator: false,
|
||||
|
||||
isDeveloper: false,
|
||||
|
||||
chats: ImmutableMap({
|
||||
panes: ImmutableList(),
|
||||
mainWindow: 'minimized',
|
||||
|
@ -101,6 +103,7 @@ export const defaultSettings = ImmutableMap({
|
|||
shows: ImmutableMap({
|
||||
reblog: false,
|
||||
reply: true,
|
||||
direct: false,
|
||||
}),
|
||||
other: ImmutableMap({
|
||||
onlyMedia: false,
|
||||
|
@ -114,6 +117,7 @@ export const defaultSettings = ImmutableMap({
|
|||
shows: ImmutableMap({
|
||||
reblog: true,
|
||||
reply: true,
|
||||
direct: false,
|
||||
}),
|
||||
other: ImmutableMap({
|
||||
onlyMedia: false,
|
||||
|
@ -133,6 +137,7 @@ export const defaultSettings = ImmutableMap({
|
|||
shows: ImmutableMap({
|
||||
reblog: true,
|
||||
pinned: true,
|
||||
direct: false,
|
||||
}),
|
||||
}),
|
||||
|
||||
|
|
|
@ -2,10 +2,16 @@ import api, { staticClient } from '../api';
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { createSelector } from 'reselect';
|
||||
import { getHost } from 'soapbox/actions/instance';
|
||||
import KVStore from 'soapbox/storage/kv_store';
|
||||
|
||||
export const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS';
|
||||
export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL';
|
||||
|
||||
export const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST';
|
||||
export const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS';
|
||||
export const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL';
|
||||
|
||||
const allowedEmoji = ImmutableList([
|
||||
'👍',
|
||||
'❤',
|
||||
|
@ -61,46 +67,71 @@ export const getSoapboxConfig = createSelector([
|
|||
return makeDefaultConfig(features).merge(soapbox);
|
||||
});
|
||||
|
||||
export function fetchSoapboxConfig() {
|
||||
export function rememberSoapboxConfig(host) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_REQUEST, host });
|
||||
return KVStore.getItemOrError(`soapbox_config:${host}`).then(soapboxConfig => {
|
||||
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_SUCCESS, host, soapboxConfig });
|
||||
return soapboxConfig;
|
||||
}).catch(error => {
|
||||
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_FAIL, host, error, skipAlert: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSoapboxConfig(host) {
|
||||
return (dispatch, getState) => {
|
||||
api(getState).get('/api/pleroma/frontend_configurations').then(response => {
|
||||
if (response.data.soapbox_fe) {
|
||||
dispatch(importSoapboxConfig(response.data.soapbox_fe));
|
||||
dispatch(importSoapboxConfig(response.data.soapbox_fe, host));
|
||||
} else {
|
||||
dispatch(fetchSoapboxJson());
|
||||
dispatch(fetchSoapboxJson(host));
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch(fetchSoapboxJson());
|
||||
dispatch(fetchSoapboxJson(host));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSoapboxJson() {
|
||||
// Tries to remember the config from browser storage before fetching it
|
||||
export function loadSoapboxConfig() {
|
||||
return (dispatch, getState) => {
|
||||
const host = getHost(getState());
|
||||
|
||||
return dispatch(rememberSoapboxConfig(host)).finally(() => {
|
||||
return dispatch(fetchSoapboxConfig(host));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSoapboxJson(host) {
|
||||
return (dispatch, getState) => {
|
||||
staticClient.get('/instance/soapbox.json').then(({ data }) => {
|
||||
if (!isObject(data)) throw 'soapbox.json failed';
|
||||
dispatch(importSoapboxConfig(data));
|
||||
dispatch(importSoapboxConfig(data, host));
|
||||
}).catch(error => {
|
||||
dispatch(soapboxConfigFail(error));
|
||||
dispatch(soapboxConfigFail(error, host));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function importSoapboxConfig(soapboxConfig) {
|
||||
export function importSoapboxConfig(soapboxConfig, host) {
|
||||
if (!soapboxConfig.brandColor) {
|
||||
soapboxConfig.brandColor = '#0482d8';
|
||||
}
|
||||
return {
|
||||
type: SOAPBOX_CONFIG_REQUEST_SUCCESS,
|
||||
soapboxConfig,
|
||||
host,
|
||||
};
|
||||
}
|
||||
|
||||
export function soapboxConfigFail(error) {
|
||||
export function soapboxConfigFail(error, host) {
|
||||
return {
|
||||
type: SOAPBOX_CONFIG_REQUEST_FAIL,
|
||||
error,
|
||||
skipAlert: true,
|
||||
host,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import { deleteFromTimelines } from './timelines';
|
|||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { openModal } from './modal';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { shouldHaveCard } from 'soapbox/utils/status';
|
||||
|
||||
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
|
||||
export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
|
||||
|
@ -33,13 +35,9 @@ export const STATUS_HIDE = 'STATUS_HIDE';
|
|||
|
||||
export const REDRAFT = 'REDRAFT';
|
||||
|
||||
export function fetchStatusRequest(id, skipLoading) {
|
||||
return {
|
||||
type: STATUS_FETCH_REQUEST,
|
||||
id,
|
||||
skipLoading,
|
||||
};
|
||||
}
|
||||
const statusExists = (getState, statusId) => {
|
||||
return getState().getIn(['statuses', statusId], null) !== null;
|
||||
};
|
||||
|
||||
export function createStatus(params, idempotencyKey) {
|
||||
return (dispatch, getState) => {
|
||||
|
@ -48,8 +46,31 @@ export function createStatus(params, idempotencyKey) {
|
|||
return api(getState).post('/api/v1/statuses', params, {
|
||||
headers: { 'Idempotency-Key': idempotencyKey },
|
||||
}).then(({ data: status }) => {
|
||||
// The backend might still be processing the rich media attachment
|
||||
if (!status.card && shouldHaveCard(status)) {
|
||||
status.expectsCard = true;
|
||||
}
|
||||
|
||||
dispatch(importFetchedStatus(status, idempotencyKey));
|
||||
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey });
|
||||
|
||||
// Poll the backend for the updated card
|
||||
if (status.expectsCard) {
|
||||
const delay = 1000;
|
||||
|
||||
const poll = (retries = 5) => {
|
||||
api(getState).get(`/api/v1/statuses/${status.id}`).then(response => {
|
||||
if (response.data && response.data.card) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
} else if (retries > 0 && response.status === 200) {
|
||||
setTimeout(() => poll(retries - 1), delay);
|
||||
}
|
||||
}).catch(console.error);
|
||||
};
|
||||
|
||||
setTimeout(() => poll(), delay);
|
||||
}
|
||||
|
||||
return status;
|
||||
}).catch(error => {
|
||||
dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey });
|
||||
|
@ -60,48 +81,32 @@ export function createStatus(params, idempotencyKey) {
|
|||
|
||||
export function fetchStatus(id) {
|
||||
return (dispatch, getState) => {
|
||||
const skipLoading = getState().getIn(['statuses', id], null) !== null;
|
||||
const skipLoading = statusExists(getState, id);
|
||||
|
||||
dispatch(fetchContext(id));
|
||||
dispatch({ type: STATUS_FETCH_REQUEST, id, skipLoading });
|
||||
|
||||
if (skipLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchStatusRequest(id, skipLoading));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(fetchStatusSuccess(response.data, skipLoading));
|
||||
return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
|
||||
dispatch(importFetchedStatus(status));
|
||||
dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
|
||||
return status;
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
||||
dispatch({ type: STATUS_FETCH_FAIL, id, error, skipLoading, skipAlert: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStatusSuccess(status, skipLoading) {
|
||||
return {
|
||||
type: STATUS_FETCH_SUCCESS,
|
||||
status,
|
||||
skipLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStatusFail(id, error, skipLoading) {
|
||||
return {
|
||||
type: STATUS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
skipLoading,
|
||||
skipAlert: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function redraft(status, raw_text) {
|
||||
return {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const instance = state.get('instance');
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
explicitAddressing,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -115,10 +120,10 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
|
||||
}
|
||||
|
||||
dispatch(deleteStatusRequest(id));
|
||||
dispatch({ type: STATUS_DELETE_REQUEST, id });
|
||||
|
||||
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
||||
dispatch(deleteStatusSuccess(id));
|
||||
dispatch({ type: STATUS_DELETE_SUCCESS, id });
|
||||
dispatch(deleteFromTimelines(id));
|
||||
|
||||
if (withRedraft) {
|
||||
|
@ -126,73 +131,37 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||
dispatch(openModal('COMPOSE'));
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch(deleteStatusFail(id, error));
|
||||
dispatch({ type: STATUS_DELETE_FAIL, id, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteStatusRequest(id) {
|
||||
return {
|
||||
type: STATUS_DELETE_REQUEST,
|
||||
id: id,
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteStatusSuccess(id) {
|
||||
return {
|
||||
type: STATUS_DELETE_SUCCESS,
|
||||
id: id,
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteStatusFail(id, error) {
|
||||
return {
|
||||
type: STATUS_DELETE_FAIL,
|
||||
id: id,
|
||||
error: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchContext(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchContextRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
|
||||
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
|
||||
dispatch({ type: CONTEXT_FETCH_REQUEST, id });
|
||||
|
||||
return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => {
|
||||
const { ancestors, descendants } = context;
|
||||
const statuses = ancestors.concat(descendants);
|
||||
dispatch(importFetchedStatuses(statuses));
|
||||
dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants });
|
||||
return context;
|
||||
}).catch(error => {
|
||||
if (error.response && error.response.status === 404) {
|
||||
dispatch(deleteFromTimelines(id));
|
||||
}
|
||||
|
||||
dispatch(fetchContextFail(id, error));
|
||||
dispatch({ type: CONTEXT_FETCH_FAIL, id, error, skipAlert: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchContextRequest(id) {
|
||||
return {
|
||||
type: CONTEXT_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchContextSuccess(id, ancestors, descendants) {
|
||||
return {
|
||||
type: CONTEXT_FETCH_SUCCESS,
|
||||
id,
|
||||
ancestors,
|
||||
descendants,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchContextFail(id, error) {
|
||||
return {
|
||||
type: CONTEXT_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
skipAlert: true,
|
||||
export function fetchStatusWithContext(id) {
|
||||
return (dispatch, getState) => {
|
||||
return Promise.all([
|
||||
dispatch(fetchContext(id)),
|
||||
dispatch(fetchStatus(id)),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -200,74 +169,28 @@ export function muteStatus(id) {
|
|||
return (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(muteStatusRequest(id));
|
||||
|
||||
dispatch({ type: STATUS_MUTE_REQUEST, id });
|
||||
api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
|
||||
dispatch(muteStatusSuccess(id));
|
||||
dispatch({ type: STATUS_MUTE_SUCCESS, id });
|
||||
}).catch(error => {
|
||||
dispatch(muteStatusFail(id, error));
|
||||
dispatch({ type: STATUS_MUTE_FAIL, id, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function muteStatusRequest(id) {
|
||||
return {
|
||||
type: STATUS_MUTE_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function muteStatusSuccess(id) {
|
||||
return {
|
||||
type: STATUS_MUTE_SUCCESS,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function muteStatusFail(id, error) {
|
||||
return {
|
||||
type: STATUS_MUTE_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function unmuteStatus(id) {
|
||||
return (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(unmuteStatusRequest(id));
|
||||
|
||||
dispatch({ type: STATUS_UNMUTE_REQUEST, id });
|
||||
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
|
||||
dispatch(unmuteStatusSuccess(id));
|
||||
dispatch({ type: STATUS_UNMUTE_SUCCESS, id });
|
||||
}).catch(error => {
|
||||
dispatch(unmuteStatusFail(id, error));
|
||||
dispatch({ type: STATUS_UNMUTE_FAIL, id, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unmuteStatusRequest(id) {
|
||||
return {
|
||||
type: STATUS_UNMUTE_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function unmuteStatusSuccess(id) {
|
||||
return {
|
||||
type: STATUS_UNMUTE_SUCCESS,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function unmuteStatusFail(id, error) {
|
||||
return {
|
||||
type: STATUS_UNMUTE_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function hideStatus(ids) {
|
||||
if (!Array.isArray(ids)) {
|
||||
ids = [ids];
|
||||
|
|
|
@ -7,6 +7,7 @@ import { isRtl } from '../rtl';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||
let word;
|
||||
|
@ -47,21 +48,28 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
autoFocus: PropTypes.bool,
|
||||
autoSelect: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
searchTokens: PropTypes.arrayOf(PropTypes.string),
|
||||
maxLength: PropTypes.number,
|
||||
menu: PropTypes.arrayOf(PropTypes.object),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
autoFocus: false,
|
||||
autoSelect: true,
|
||||
searchTokens: ImmutableList(['@', ':', '#']),
|
||||
};
|
||||
|
||||
getFirstIndex = () => {
|
||||
return this.props.autoSelect ? 0 : -1;
|
||||
}
|
||||
|
||||
state = {
|
||||
suggestionsHidden: true,
|
||||
focused: false,
|
||||
selectedSuggestion: 0,
|
||||
selectedSuggestion: this.getFirstIndex(),
|
||||
lastToken: null,
|
||||
tokenStart: 0,
|
||||
};
|
||||
|
@ -81,8 +89,10 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
onKeyDown = (e) => {
|
||||
const { suggestions, disabled } = this.props;
|
||||
const { suggestions, menu, disabled } = this.props;
|
||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
||||
const firstIndex = this.getFirstIndex();
|
||||
const lastIndex = suggestions.size + (menu || []).length - 1;
|
||||
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
|
@ -106,26 +116,33 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
|
||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, lastIndex) });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
|
||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, firstIndex) });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
// Select suggestion
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.size > 0 || menu)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
||||
this.setState({ selectedSuggestion: firstIndex });
|
||||
|
||||
if (selectedSuggestion < suggestions.size) {
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
||||
} else {
|
||||
const item = menu[selectedSuggestion - suggestions.size];
|
||||
this.handleMenuItemAction(item);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -186,11 +203,51 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
handleMenuItemAction = item => {
|
||||
this.onBlur();
|
||||
item.action();
|
||||
}
|
||||
|
||||
handleMenuItemClick = item => {
|
||||
return e => {
|
||||
e.preventDefault();
|
||||
this.handleMenuItemAction(item);
|
||||
};
|
||||
}
|
||||
|
||||
renderMenu = () => {
|
||||
const { menu, suggestions } = this.props;
|
||||
const { selectedSuggestion } = this.state;
|
||||
|
||||
if (!menu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return menu.map((item, i) => (
|
||||
<a
|
||||
className={classNames('autosuggest-input__action', { selected: suggestions.size - selectedSuggestion === i })}
|
||||
href='#'
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
onMouseDown={this.handleMenuItemClick(item)}
|
||||
key={i}
|
||||
>
|
||||
{item.icon && (
|
||||
<Icon src={item.icon} />
|
||||
)}
|
||||
|
||||
<span>{item.text}</span>
|
||||
</a>
|
||||
));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
const style = { direction: 'ltr' };
|
||||
|
||||
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
|
||||
|
||||
if (isRtl(value)) {
|
||||
style.direction = 'rtl';
|
||||
}
|
||||
|
@ -220,8 +277,9 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
/>
|
||||
</label>
|
||||
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
<div className={classNames('autosuggest-textarea__suggestions', { 'autosuggest-textarea__suggestions--visible': visible })}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
{this.renderMenu()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent {
|
|||
</span>
|
||||
|
||||
<div className='domain__buttons'>
|
||||
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
|
||||
<IconButton active src={require('@tabler/icons/icons/lock-open.svg')} title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import IconButton from './icon_button';
|
||||
|
@ -6,6 +7,7 @@ import Overlay from 'react-overlays/lib/Overlay';
|
|||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
let id = 0;
|
||||
|
@ -146,10 +148,10 @@ class DropdownMenu extends React.PureComponent {
|
|||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||
}
|
||||
|
||||
const { text, href, to, newTab, isLogout } = option;
|
||||
const { text, href, to, newTab, isLogout, icon, destructive } = option;
|
||||
|
||||
return (
|
||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
||||
<li className={classNames('dropdown-menu__item', { destructive })} key={`${text}-${i}`}>
|
||||
<a
|
||||
href={href || to || '#'}
|
||||
role='button'
|
||||
|
@ -162,6 +164,7 @@ class DropdownMenu extends React.PureComponent {
|
|||
target={newTab ? '_blank' : null}
|
||||
data-method={isLogout ? 'delete' : null}
|
||||
>
|
||||
{icon && <Icon src={icon} />}
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -1,30 +1,44 @@
|
|||
import React from 'react';
|
||||
// import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from './permalink';
|
||||
import { shortNumberFormat } from '../utils/numbers';
|
||||
|
||||
const Hashtag = ({ hashtag }) => (
|
||||
<div className='trends__item'>
|
||||
<div className='trends__item__name'>
|
||||
<Permalink href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`}>
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
</Permalink>
|
||||
const Hashtag = ({ hashtag }) => {
|
||||
const count = Number(hashtag.getIn(['history', 0, 'accounts']));
|
||||
|
||||
{hashtag.get('history') && <div className='trends__item__count'>
|
||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
|
||||
</div>}
|
||||
return (
|
||||
<div className='trends__item'>
|
||||
<div className='trends__item__name'>
|
||||
<Permalink href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`}>
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
</Permalink>
|
||||
|
||||
{hashtag.get('history') && (
|
||||
<div className='trends__item__count'>
|
||||
<FormattedMessage
|
||||
id='trends.count_by_accounts'
|
||||
defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking'
|
||||
values={{
|
||||
rawCount: count,
|
||||
count: <strong>{shortNumberFormat(count)}</strong>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hashtag.get('history') && (
|
||||
<div className='trends__item__sparkline'>
|
||||
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pleroma doesn't support tag history yet */}
|
||||
{/* hashtag.get('history') && <div className='trends__item__sparkline'>
|
||||
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
Hashtag.propTypes = {
|
||||
hashtag: ImmutablePropTypes.map.isRequired,
|
||||
|
|
|
@ -11,7 +11,7 @@ FaviconService.initFaviconService();
|
|||
|
||||
const getNotifTotals = state => {
|
||||
const notifications = state.getIn(['notifications', 'unread'], 0);
|
||||
const chats = state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
|
||||
const chats = state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
|
||||
const reports = state.getIn(['admin', 'openReports']).count();
|
||||
const approvals = state.getIn(['admin', 'awaitingApproval']).count();
|
||||
return notifications + chats + reports + approvals;
|
||||
|
|
|
@ -33,6 +33,7 @@ export default class IconButton extends React.PureComponent {
|
|||
tabIndex: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
emoji: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -47,6 +48,7 @@ export default class IconButton extends React.PureComponent {
|
|||
onClick: () => {},
|
||||
onMouseEnter: () => {},
|
||||
onMouseLeave: () => {},
|
||||
type: 'button',
|
||||
};
|
||||
|
||||
handleClick = (e) => {
|
||||
|
@ -106,6 +108,7 @@ export default class IconButton extends React.PureComponent {
|
|||
title,
|
||||
text,
|
||||
emoji,
|
||||
type,
|
||||
} = this.props;
|
||||
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
|
@ -134,6 +137,7 @@ export default class IconButton extends React.PureComponent {
|
|||
onMouseLeave={this.props.onMouseLeave}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
>
|
||||
<div style={src ? {} : style}>
|
||||
{emoji
|
||||
|
@ -163,6 +167,7 @@ export default class IconButton extends React.PureComponent {
|
|||
onMouseLeave={this.props.onMouseLeave}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
>
|
||||
<div style={src ? {} : style}>
|
||||
{emoji
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* iOS style loading spinner.
|
||||
* It's mostly CSS, adapted from: https://loading.io/css/
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const LoadingSpinner = ({ size = 30 }) => (
|
||||
<div class='lds-spinner' style={{ width: size, height: size }}>
|
||||
{Array(12).fill().map(i => (
|
||||
<div />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
LoadingSpinner.propTypes = {
|
||||
size: PropTypes.number,
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
|
@ -4,24 +4,24 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusContainer from 'soapbox/containers/status_container';
|
||||
|
||||
export default class MaterialStatus extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
hidden: PropTypes.bool,
|
||||
}
|
||||
|
||||
render() {
|
||||
// Performance: when hidden, don't render the wrapper divs
|
||||
if (this.props.hidden) {
|
||||
return this.props.children;
|
||||
return <StatusContainer {...this.props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='material-status' tabIndex={-1}>
|
||||
<div className='material-status__status focusable' tabIndex={0}>
|
||||
{this.props.children}
|
||||
<StatusContainer {...this.props} focusable={false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import escapeTextContentForBrowser from 'escape-html';
|
|||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
|
@ -33,6 +34,7 @@ class Poll extends ImmutablePureComponent {
|
|||
dispatch: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
me: SoapboxPropTypes.me,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -40,18 +42,22 @@ class Poll extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
_toggleOption = value => {
|
||||
if (this.props.poll.get('multiple')) {
|
||||
const tmp = { ...this.state.selected };
|
||||
if (tmp[value]) {
|
||||
delete tmp[value];
|
||||
if (this.props.me) {
|
||||
if (this.props.poll.get('multiple')) {
|
||||
const tmp = { ...this.state.selected };
|
||||
if (tmp[value]) {
|
||||
delete tmp[value];
|
||||
} else {
|
||||
tmp[value] = true;
|
||||
}
|
||||
this.setState({ selected: tmp });
|
||||
} else {
|
||||
const tmp = {};
|
||||
tmp[value] = true;
|
||||
this.setState({ selected: tmp });
|
||||
}
|
||||
this.setState({ selected: tmp });
|
||||
} else {
|
||||
const tmp = {};
|
||||
tmp[value] = true;
|
||||
this.setState({ selected: tmp });
|
||||
this.openUnauthorizedModal();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,6 +81,14 @@ class Poll extends ImmutablePureComponent {
|
|||
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
|
||||
};
|
||||
|
||||
openUnauthorizedModal = () => {
|
||||
const { dispatch, status } = this.props;
|
||||
dispatch(openModal('UNAUTHORIZED', {
|
||||
action: 'POLL_VOTE',
|
||||
ap_id: status,
|
||||
}));
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
if (this.props.disabled) {
|
||||
return;
|
||||
|
|
|
@ -9,22 +9,26 @@ import { NavLink, withRouter } from 'react-router-dom';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import IconWithCounter from 'soapbox/components/icon_with_counter';
|
||||
import classNames from 'classnames';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { isStaff } from 'soapbox/utils/accounts';
|
||||
import { isStaff, getBaseURL } from 'soapbox/utils/accounts';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
const reportsCount = state.getIn(['admin', 'openReports']).count();
|
||||
const approvalCount = state.getIn(['admin', 'awaitingApproval']).count();
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
account,
|
||||
logo: getSoapboxConfig(state).get('logo'),
|
||||
notificationCount: state.getIn(['notifications', 'unread']),
|
||||
chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||
chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||
dashboardCount: reportsCount + approvalCount,
|
||||
baseURL: getBaseURL(account),
|
||||
settings: getSettings(state),
|
||||
features: getFeatures(instance),
|
||||
instance,
|
||||
};
|
||||
|
@ -44,13 +48,15 @@ class PrimaryNavigation extends React.PureComponent {
|
|||
dashboardCount: PropTypes.number,
|
||||
notificationCount: PropTypes.number,
|
||||
chatsCount: PropTypes.number,
|
||||
baseURL: PropTypes.string,
|
||||
settings: PropTypes.object.isRequired,
|
||||
features: PropTypes.object.isRequired,
|
||||
location: PropTypes.object,
|
||||
instance: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { account, features, notificationCount, chatsCount, dashboardCount, location, instance } = this.props;
|
||||
const { account, settings, features, notificationCount, chatsCount, dashboardCount, location, instance, baseURL } = this.props;
|
||||
|
||||
return (
|
||||
<div className='column-header__wrapper primary-navigation__wrapper'>
|
||||
|
@ -118,12 +124,22 @@ class PrimaryNavigation extends React.PureComponent {
|
|||
)}
|
||||
|
||||
{(account && instance.get('invites_enabled')) && (
|
||||
<a href='/invites' className='btn grouped'>
|
||||
<a href={`${baseURL}/invites`} className='btn grouped'>
|
||||
<Icon src={require('@tabler/icons/icons/mailbox.svg')} className='primary-navigation__icon' />
|
||||
<FormattedMessage id='navigation.invites' defaultMessage='Invites' />
|
||||
</a>
|
||||
)}
|
||||
|
||||
{(settings.get('isDeveloper')) && (
|
||||
<NavLink key='developers' className='btn grouped' to='/developers'>
|
||||
<Icon
|
||||
src={require('@tabler/icons/icons/code.svg')}
|
||||
className={classNames('primary-navigation__icon', { 'svg-icon--active': location.pathname.startsWith('/developers') })}
|
||||
/>
|
||||
<FormattedMessage id='navigation.developers' defaultMessage='Developers' />
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
{features.federating ? (
|
||||
|
|
|
@ -52,6 +52,7 @@ export const ProfileHoverCard = ({ visible }) => {
|
|||
|
||||
const [popperElement, setPopperElement] = useState(null);
|
||||
|
||||
const me = useSelector(state => state.get('me'));
|
||||
const accountId = useSelector(state => state.getIn(['profile_hover_card', 'accountId']));
|
||||
const account = useSelector(state => accountId && getAccount(state, accountId));
|
||||
const targetRef = useSelector(state => state.getIn(['profile_hover_card', 'ref', 'current']));
|
||||
|
@ -65,7 +66,7 @@ export const ProfileHoverCard = ({ visible }) => {
|
|||
|
||||
if (!account) return null;
|
||||
const accountBio = { __html: account.get('note_emojified') };
|
||||
const followedBy = account.getIn(['relationship', 'followed_by']);
|
||||
const followedBy = me !== account.get('id') && account.getIn(['relationship', 'followed_by']);
|
||||
|
||||
return (
|
||||
<div className={classNames('profile-hover-card', { 'profile-hover-card--visible': visible })} ref={setPopperElement} style={styles.popper} {...attributes.popper} onMouseEnter={handleMouseEnter(dispatch)} onMouseLeave={handleMouseLeave(dispatch)}>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PTRComponent from 'react-simple-pull-to-refresh';
|
||||
|
||||
/**
|
||||
* PullToRefresh:
|
||||
* Wrapper around a third-party PTR component with Soapbox defaults.
|
||||
*/
|
||||
export default class PullToRefresh extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
onRefresh: PropTypes.func,
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
const { onRefresh } = this.props;
|
||||
|
||||
if (onRefresh) {
|
||||
return onRefresh();
|
||||
} else {
|
||||
// If not provided, do nothing
|
||||
return new Promise(resolve => resolve());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, onRefresh, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<PTRComponent
|
||||
onRefresh={this.handleRefresh}
|
||||
pullingContent={null}
|
||||
// `undefined` will fallback to the default, while `null` will render nothing
|
||||
refreshingContent={onRefresh ? undefined : null}
|
||||
pullDownThreshold={67}
|
||||
maxPullDownDistance={95}
|
||||
resistance={2}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</PTRComponent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PullToRefresh from './pull_to_refresh';
|
||||
|
||||
/**
|
||||
* Pullable:
|
||||
* Basic "pull to refresh" without the refresh.
|
||||
* Just visual feedback.
|
||||
*/
|
||||
export default class Pullable extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<PullToRefresh
|
||||
pullingContent={null}
|
||||
refreshingContent={null}
|
||||
>
|
||||
{children}
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class RadioButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, value, checked, onChange, label } = this.props;
|
||||
|
||||
return (
|
||||
<label className='radio-button'>
|
||||
<input
|
||||
name={name}
|
||||
type='radio'
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<span className={classNames('radio-button__input', { checked })} />
|
||||
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||
import LoadMore from './load_more';
|
||||
import MoreFollows from './more_follows';
|
||||
|
@ -9,6 +10,7 @@ import { throttle } from 'lodash';
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import LoadingIndicator from './loading_indicator';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import PullToRefresh from 'soapbox/components/pull_to_refresh';
|
||||
|
||||
const MOUSE_IDLE_DELAY = 300;
|
||||
|
||||
|
@ -43,6 +45,8 @@ class ScrollableList extends PureComponent {
|
|||
placeholderComponent: PropTypes.func,
|
||||
placeholderCount: PropTypes.number,
|
||||
autoload: PropTypes.bool,
|
||||
onRefresh: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -238,16 +242,22 @@ class ScrollableList extends PureComponent {
|
|||
}
|
||||
|
||||
renderLoading = () => {
|
||||
const { prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
|
||||
const { className, prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
|
||||
|
||||
if (Placeholder && placeholderCount > 0) {
|
||||
return Array(placeholderCount).fill().map((_, i) => (
|
||||
<Placeholder key={i} />
|
||||
));
|
||||
return (
|
||||
<div className={classNames('slist slist--flex', className)}>
|
||||
<div role='feed' className='item-list'>
|
||||
{Array(placeholderCount).fill().map((_, i) => (
|
||||
<Placeholder key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='slist slist--flex'>
|
||||
<div className={classNames('slist slist--flex', className)}>
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
</div>
|
||||
|
@ -260,10 +270,10 @@ class ScrollableList extends PureComponent {
|
|||
}
|
||||
|
||||
renderEmptyMessage = () => {
|
||||
const { prepend, alwaysPrepend, emptyMessage } = this.props;
|
||||
const { className, prepend, alwaysPrepend, emptyMessage } = this.props;
|
||||
|
||||
return (
|
||||
<div className='slist slist--flex' ref={this.setRef}>
|
||||
<div className={classNames('slist slist--flex', className)} ref={this.setRef}>
|
||||
{alwaysPrepend && prepend}
|
||||
|
||||
<div className='empty-column-indicator'>
|
||||
|
@ -274,13 +284,13 @@ class ScrollableList extends PureComponent {
|
|||
}
|
||||
|
||||
renderFeed = () => {
|
||||
const { children, scrollKey, isLoading, hasMore, prepend, onLoadMore, placeholderComponent: Placeholder } = this.props;
|
||||
const { className, children, scrollKey, isLoading, hasMore, prepend, onLoadMore, onRefresh, placeholderComponent: Placeholder } = this.props;
|
||||
const childrenCount = React.Children.count(children);
|
||||
const trackScroll = true; //placeholder
|
||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||
|
||||
return (
|
||||
<div className='slist' ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
const feed = (
|
||||
<div className={classNames('slist', className)} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
|
||||
|
@ -313,6 +323,16 @@ class ScrollableList extends PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (onRefresh) {
|
||||
return (
|
||||
<PullToRefresh onRefresh={onRefresh}>
|
||||
{feed}
|
||||
</PullToRefresh>
|
||||
);
|
||||
} else {
|
||||
return feed;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { FormPropTypes, InputContainer, LabelInputContainer } from 'soapbox/features/forms';
|
||||
|
||||
const messages = defineMessages({
|
||||
showPassword: { id: 'forms.show_password', defaultMessage: 'Show password' },
|
||||
hidePassword: { id: 'forms.hide_password', defaultMessage: 'Hide password' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class ShowablePassword extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
label: FormPropTypes.label,
|
||||
className: PropTypes.string,
|
||||
hint: PropTypes.node,
|
||||
error: PropTypes.bool,
|
||||
}
|
||||
|
||||
state = {
|
||||
revealed: false,
|
||||
}
|
||||
|
||||
toggleReveal = () => {
|
||||
if (this.props.onToggleVisibility) {
|
||||
this.props.onToggleVisibility();
|
||||
} else {
|
||||
this.setState({ revealed: !this.state.revealed });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, hint, error, label, className, ...props } = this.props;
|
||||
const { revealed } = this.state;
|
||||
|
||||
const revealButton = (
|
||||
<IconButton
|
||||
src={revealed ? require('@tabler/icons/icons/eye-off.svg') : require('@tabler/icons/icons/eye.svg')}
|
||||
onClick={this.toggleReveal}
|
||||
title={intl.formatMessage(revealed ? messages.hidePassword : messages.showPassword)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<InputContainer {...this.props} extraClass={classNames('showable-password', className)}>
|
||||
{label ? (
|
||||
<LabelInputContainer label={label}>
|
||||
<input {...props} type={revealed ? 'text' : 'password'} />
|
||||
{revealButton}
|
||||
</LabelInputContainer>
|
||||
) : (<>
|
||||
<input {...props} type={revealed ? 'text' : 'password'} />
|
||||
{revealButton}
|
||||
</>)}
|
||||
</InputContainer>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,7 @@ import { logOut, switchAccount } from 'soapbox/actions/auth';
|
|||
import ThemeToggle from '../features/ui/components/theme_toggle_container';
|
||||
import { fetchOwnAccounts } from 'soapbox/actions/auth';
|
||||
import { is as ImmutableIs } from 'immutable';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
|
@ -25,6 +26,7 @@ const messages = defineMessages({
|
|||
followers: { id: 'account.followers', defaultMessage: 'Followers' },
|
||||
follows: { id: 'account.follows', defaultMessage: 'Follows' },
|
||||
profile: { id: 'account.profile', defaultMessage: 'Profile' },
|
||||
invites: { id: 'navigation_bar.invites', defaultMessage: 'Invites' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
|
@ -39,12 +41,14 @@ const messages = defineMessages({
|
|||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
profileDirectory: { id: 'column.profile_directory', defaultMessage: 'Profile directory' },
|
||||
header: { id: 'tabs_bar.header', defaultMessage: 'Account Info' },
|
||||
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
||||
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
|
||||
donate: { id: 'donate', defaultMessage: 'Donate' },
|
||||
donate_crypto: { id: 'donate_crypto', defaultMessage: 'Donate cryptocurrency' },
|
||||
info: { id: 'column.info', defaultMessage: 'Server information' },
|
||||
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
||||
add_account: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
||||
});
|
||||
|
||||
|
@ -67,6 +71,8 @@ const makeMapStateToProps = () => {
|
|||
hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string',
|
||||
otherAccounts: getOtherAccounts(state),
|
||||
features,
|
||||
instance,
|
||||
settings: getSettings(state),
|
||||
siteTitle: instance.get('title'),
|
||||
baseURL: getBaseURL(account),
|
||||
};
|
||||
|
@ -101,7 +107,9 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
otherAccounts: ImmutablePropTypes.list,
|
||||
sidebarOpen: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
features: PropTypes.object.isRequired,
|
||||
instance: ImmutablePropTypes.map.isRequired,
|
||||
baseURL: PropTypes.string,
|
||||
};
|
||||
|
||||
|
@ -163,7 +171,7 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { sidebarOpen, intl, account, onClickLogOut, donateUrl, otherAccounts, hasCrypto, features, siteTitle, baseURL } = this.props;
|
||||
const { sidebarOpen, intl, account, onClickLogOut, donateUrl, otherAccounts, hasCrypto, settings, features, instance, siteTitle, baseURL } = this.props;
|
||||
const { switcher } = this.state;
|
||||
if (!account) return null;
|
||||
const acct = account.get('acct');
|
||||
|
@ -230,6 +238,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon src={require('@tabler/icons/icons/user.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.profile)}</span>
|
||||
</NavLink>
|
||||
{instance.get('invites_enabled') && <a className='sidebar-menu-item' href={`${baseURL}/invites`} onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/mailbox.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.invites)}</span>
|
||||
</a>}
|
||||
{donateUrl && <a className='sidebar-menu-item' href={donateUrl} onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/coin.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
|
||||
|
@ -246,6 +258,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon src={require('@tabler/icons/icons/bookmarks.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.bookmarks)}</span>
|
||||
</NavLink>}
|
||||
{features.profileDirectory && <NavLink className='sidebar-menu-item' to='/directory' onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/friends.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.profileDirectory)}</span>
|
||||
</NavLink>}
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__section'>
|
||||
|
@ -288,12 +304,19 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.preferences)}</span>
|
||||
</a>
|
||||
)}
|
||||
<NavLink className='sidebar-menu-item' to='/settings/import' onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/cloud-upload.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
||||
</NavLink>
|
||||
{features.importAPI ? (
|
||||
<NavLink className='sidebar-menu-item' to='/settings/import' onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/cloud-upload.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
||||
</NavLink>
|
||||
) : (
|
||||
<a className='sidebar-menu-item' href={`${baseURL}/settings/import`} onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/cloud-upload.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
||||
</a>
|
||||
)}
|
||||
{(features.federating && features.accountAliasesAPI) && <NavLink className='sidebar-menu-item' to='/settings/aliases' onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/briefcase.svg')} />
|
||||
<Icon src={require('feather-icons/dist/icons/briefcase.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.account_aliases)}</span>
|
||||
</NavLink>}
|
||||
{features.securityAPI ? (
|
||||
|
@ -316,6 +339,13 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
{(settings.get('isDeveloper')) && (
|
||||
<Link className='sidebar-menu-item' to='/developers' onClick={this.handleClose}>
|
||||
<Icon src={require('@tabler/icons/icons/code.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.developers)}</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div className='sidebar-menu__section'>
|
||||
<Link className='sidebar-menu-item' to='/auth/sign_out' onClick={onClickLogOut}>
|
||||
<Icon src={require('@tabler/icons/icons/logout.svg')} />
|
||||
|
|
|
@ -8,6 +8,7 @@ import RelativeTimestamp from './relative_timestamp';
|
|||
import DisplayName from './display_name';
|
||||
import StatusContent from './status_content';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusReplyMentions from './status_reply_mentions';
|
||||
import AttachmentThumbs from './attachment_thumbs';
|
||||
import Card from '../features/status/components/card';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
@ -19,6 +20,7 @@ import Icon from 'soapbox/components/icon';
|
|||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { getDomain } from 'soapbox/utils/accounts';
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
|
@ -86,7 +88,6 @@ class Status extends ImmutablePureComponent {
|
|||
unread: PropTypes.bool,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
showThread: PropTypes.bool,
|
||||
getScrollPosition: PropTypes.func,
|
||||
updateScrollBottom: PropTypes.func,
|
||||
cacheMediaWidth: PropTypes.func,
|
||||
|
@ -95,7 +96,6 @@ class Status extends ImmutablePureComponent {
|
|||
displayMedia: PropTypes.string,
|
||||
allowedEmoji: ImmutablePropTypes.list,
|
||||
focusable: PropTypes.bool,
|
||||
component: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -317,7 +317,7 @@ class Status extends ImmutablePureComponent {
|
|||
const poll = null;
|
||||
let statusAvatar, prepend, rebloggedByText, reblogContent;
|
||||
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread, group, wrapperComponent: WrapperComponent } = this.props;
|
||||
const { intl, hidden, featured, otherAccounts, unread, group } = this.props;
|
||||
|
||||
// FIXME: why does this need to reassign status and account??
|
||||
let { status, account, ...other } = this.props; // eslint-disable-line prefer-const
|
||||
|
@ -466,6 +466,10 @@ class Status extends ImmutablePureComponent {
|
|||
defaultWidth={this.props.cachedMediaWidth}
|
||||
/>
|
||||
);
|
||||
} else if (status.get('expectsCard', false)) {
|
||||
media = (
|
||||
<PlaceholderCard />
|
||||
);
|
||||
}
|
||||
|
||||
if (otherAccounts && otherAccounts.size > 1) {
|
||||
|
@ -495,76 +499,68 @@ class Status extends ImmutablePureComponent {
|
|||
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
||||
const domain = getDomain(status.get('account'));
|
||||
|
||||
const wrappedStatus = (
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: this.props.focusable && !this.props.muted })} tabIndex={this.props.focusable && !this.props.muted ? 0 : null} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
{prepend}
|
||||
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
<NavLink to={statusUrl} className='status__relative-time'>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||
</NavLink>
|
||||
|
||||
{favicon &&
|
||||
<div className='status__favicon'>
|
||||
<Link to={`/timeline/${domain}`}>
|
||||
<img src={favicon} alt='' title={domain} />
|
||||
</Link>
|
||||
</div>}
|
||||
|
||||
<div className='status__profile'>
|
||||
<div className='status__avatar'>
|
||||
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])}>
|
||||
{statusAvatar}
|
||||
</NavLink>
|
||||
</HoverRefWrapper>
|
||||
</div>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!group && status.get('group') && (
|
||||
<div className='status__meta'>
|
||||
Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StatusContent
|
||||
status={status}
|
||||
reblogContent={reblogContent}
|
||||
onClick={this.handleClick}
|
||||
expanded={!status.get('hidden')}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
collapsable
|
||||
/>
|
||||
|
||||
{media}
|
||||
{poll}
|
||||
|
||||
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
account={account}
|
||||
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||
{...other}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
{WrapperComponent ? <WrapperComponent>{wrappedStatus}</WrapperComponent> : wrappedStatus}
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: this.props.focusable && !this.props.muted })} tabIndex={this.props.focusable && !this.props.muted ? 0 : null} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
{prepend}
|
||||
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
<NavLink to={statusUrl} className='status__relative-time'>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||
</NavLink>
|
||||
|
||||
{favicon &&
|
||||
<div className='status__favicon'>
|
||||
<Link to={`/timeline/${domain}`}>
|
||||
<img src={favicon} alt='' title={domain} />
|
||||
</Link>
|
||||
</div>}
|
||||
|
||||
<div className='status__profile'>
|
||||
<div className='status__avatar'>
|
||||
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])}>
|
||||
{statusAvatar}
|
||||
</NavLink>
|
||||
</HoverRefWrapper>
|
||||
</div>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!group && status.get('group') && (
|
||||
<div className='status__meta'>
|
||||
Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StatusReplyMentions status={this._properStatus()} />
|
||||
|
||||
<StatusContent
|
||||
status={status}
|
||||
reblogContent={reblogContent}
|
||||
onClick={this.handleClick}
|
||||
expanded={!status.get('hidden')}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
collapsable
|
||||
/>
|
||||
|
||||
{media}
|
||||
{poll}
|
||||
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
account={account}
|
||||
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||
{...other}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -117,11 +117,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
]
|
||||
|
||||
handleReplyClick = () => {
|
||||
const { me } = this.props;
|
||||
const { me, onReply, onOpenUnauthorizedModal, status } = this.props;
|
||||
if (me) {
|
||||
this.props.onReply(this.props.status, this.context.router.history);
|
||||
onReply(status, this.context.router.history);
|
||||
} else {
|
||||
this.props.onOpenUnauthorizedModal();
|
||||
onOpenUnauthorizedModal('REPLY');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,22 +167,22 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
handleReactClick = emoji => {
|
||||
return e => {
|
||||
const { me, status } = this.props;
|
||||
const { me, dispatch, onOpenUnauthorizedModal, status } = this.props;
|
||||
if (me) {
|
||||
this.props.dispatch(simpleEmojiReact(status, emoji));
|
||||
dispatch(simpleEmojiReact(status, emoji));
|
||||
} else {
|
||||
this.props.onOpenUnauthorizedModal();
|
||||
onOpenUnauthorizedModal('FAVOURITE');
|
||||
}
|
||||
this.setState({ emojiSelectorVisible: false });
|
||||
};
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
const { me } = this.props;
|
||||
const { me, onFavourite, onOpenUnauthorizedModal, status } = this.props;
|
||||
if (me) {
|
||||
this.props.onFavourite(this.props.status);
|
||||
onFavourite(status);
|
||||
} else {
|
||||
this.props.onOpenUnauthorizedModal();
|
||||
onOpenUnauthorizedModal('FAVOURITE');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,11 +191,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
const { me } = this.props;
|
||||
const { me, onReblog, onOpenUnauthorizedModal, status } = this.props;
|
||||
if (me) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
onReblog(status, e);
|
||||
} else {
|
||||
this.props.onOpenUnauthorizedModal();
|
||||
onOpenUnauthorizedModal('REBLOG');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -301,15 +301,31 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
const menu = [];
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.open),
|
||||
action: this.handleOpen,
|
||||
icon: require('@tabler/icons/icons/arrows-vertical.svg'),
|
||||
});
|
||||
|
||||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||
// menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.copy),
|
||||
action: this.handleCopy,
|
||||
icon: require('@tabler/icons/icons/link.svg'),
|
||||
});
|
||||
// menu.push({
|
||||
// text: intl.formatMessage(messages.embed),
|
||||
// action: this.handleEmbed,
|
||||
// icon: require('feather-icons/dist/icons/link-2.svg'),
|
||||
// });
|
||||
}
|
||||
|
||||
if (features.bookmarks) {
|
||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark),
|
||||
action: this.handleBookmarkClick,
|
||||
icon: require(status.get('bookmarked') ? '@tabler/icons/icons/bookmark-off.svg' : '@tabler/icons/icons/bookmark.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (!me) {
|
||||
|
@ -319,57 +335,139 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
menu.push(null);
|
||||
|
||||
if (ownAccount || withDismiss) {
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
|
||||
action: this.handleConversationMuteClick,
|
||||
icon: require(mutingConversation ? '@tabler/icons/icons/bell.svg' : '@tabler/icons/icons/bell-off.svg'),
|
||||
});
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (ownAccount) {
|
||||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
|
||||
action: this.handlePinClick,
|
||||
icon: require(mutingConversation ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg'),
|
||||
});
|
||||
} else {
|
||||
if (status.get('visibility') === 'private') {
|
||||
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
|
||||
action: this.handleReblogClick,
|
||||
icon: require('@tabler/icons/icons/repeat.svg'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.delete),
|
||||
action: this.handleDeleteClick,
|
||||
icon: require('@tabler/icons/icons/trash.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.redraft),
|
||||
action: this.handleRedraftClick,
|
||||
icon: require('@tabler/icons/icons/edit.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleMentionClick,
|
||||
icon: require('feather-icons/dist/icons/at-sign.svg'),
|
||||
});
|
||||
|
||||
if (status.getIn(['account', 'pleroma', 'accepts_chat_messages'], false) === true) {
|
||||
menu.push({ text: intl.formatMessage(messages.chat, { name: status.getIn(['account', 'username']) }), action: this.handleChatClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.chat, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleChatClick,
|
||||
icon: require('@tabler/icons/icons/messages.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleDirectClick,
|
||||
icon: require('@tabler/icons/icons/mail.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleMuteClick,
|
||||
icon: require('@tabler/icons/icons/circle-x.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleBlockClick,
|
||||
icon: require('@tabler/icons/icons/ban.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleReport,
|
||||
icon: require('@tabler/icons/icons/flag.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (isStaff) {
|
||||
menu.push(null);
|
||||
|
||||
if (isAdmin) {
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/` });
|
||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/pleroma/admin/#/statuses/${status.get('id')}/` });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }),
|
||||
href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/`,
|
||||
icon: require('icons/gavel.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_status),
|
||||
href: `/pleroma/admin/#/statuses/${status.get('id')}/`,
|
||||
icon: require('@tabler/icons/icons/pencil.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(status.get('sensitive') === false ? messages.markStatusSensitive : messages.markStatusNotSensitive), action: this.handleToggleStatusSensitivity });
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.get('sensitive') === false ? messages.markStatusSensitive : messages.markStatusNotSensitive),
|
||||
action: this.handleToggleStatusSensitivity,
|
||||
icon: require('@tabler/icons/icons/alert-triangle.svg'),
|
||||
});
|
||||
|
||||
if (!ownAccount) {
|
||||
menu.push({ text: intl.formatMessage(messages.deactivateUser, { name: status.getIn(['account', 'username']) }), action: this.handleDeactivateUser });
|
||||
menu.push({ text: intl.formatMessage(messages.deleteUser, { name: status.getIn(['account', 'username']) }), action: this.handleDeleteUser });
|
||||
menu.push({ text: intl.formatMessage(messages.deleteStatus), action: this.handleDeleteStatus });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.deactivateUser, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleDeactivateUser,
|
||||
icon: require('@tabler/icons/icons/user-off.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.deleteUser, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleDeleteUser,
|
||||
icon: require('@tabler/icons/icons/user-minus.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.deleteStatus),
|
||||
action: this.handleDeleteStatus,
|
||||
icon: require('@tabler/icons/icons/trash.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!ownAccount && withGroupAdmin) {
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.group_remove_account), action: this.handleGroupRemoveAccount });
|
||||
menu.push({ text: intl.formatMessage(messages.group_remove_post), action: this.handleGroupRemovePost });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.group_remove_account),
|
||||
action: this.handleGroupRemoveAccount,
|
||||
icon: require('@tabler/icons/icons/user-x.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.group_remove_post),
|
||||
action: this.handleGroupRemovePost,
|
||||
icon: require('@tabler/icons/icons/trash.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
|
@ -501,10 +599,13 @@ const mapStateToProps = state => {
|
|||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = (dispatch, { status }) => ({
|
||||
dispatch,
|
||||
onOpenUnauthorizedModal() {
|
||||
dispatch(openModal('UNAUTHORIZED'));
|
||||
onOpenUnauthorizedModal(action) {
|
||||
dispatch(openModal('UNAUTHORIZED', {
|
||||
action,
|
||||
ap_id: status.get('url'),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -242,7 +242,7 @@ class StatusContent extends React.PureComponent {
|
|||
|
||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
|
||||
|
||||
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} status={status.get('url')} />}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.onClick) {
|
||||
|
@ -265,7 +265,7 @@ class StatusContent extends React.PureComponent {
|
|||
}
|
||||
|
||||
if (status.get('poll')) {
|
||||
output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
|
||||
output.push(<PollContainer pollId={status.get('poll')} key='poll' status={status.get('url')} />);
|
||||
}
|
||||
|
||||
return output;
|
||||
|
@ -285,7 +285,7 @@ class StatusContent extends React.PureComponent {
|
|||
];
|
||||
|
||||
if (status.get('poll')) {
|
||||
output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
|
||||
output.push(<PollContainer pollId={status.get('poll')} key='poll' status={status.get('url')} />);
|
||||
}
|
||||
|
||||
return output;
|
||||
|
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||
import { FormattedMessage, defineMessages } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusContainer from 'soapbox/containers/status_container';
|
||||
import MaterialStatus from 'soapbox/components/material_status';
|
||||
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -111,7 +110,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
const { timelineId, withGroupAdmin, group } = this.props;
|
||||
|
||||
return (
|
||||
<StatusContainer
|
||||
<MaterialStatus
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
|
@ -119,9 +118,6 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
contextType={timelineId}
|
||||
group={group}
|
||||
withGroupAdmin={withGroupAdmin}
|
||||
showThread
|
||||
wrapperComponent={MaterialStatus}
|
||||
focusable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -141,7 +137,6 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
contextType={timelineId}
|
||||
group={group}
|
||||
withGroupAdmin={withGroupAdmin}
|
||||
showThread
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -153,16 +148,13 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
if (!featuredStatusIds) return null;
|
||||
|
||||
return featuredStatusIds.map(statusId => (
|
||||
<StatusContainer
|
||||
<MaterialStatus
|
||||
key={`f-${statusId}`}
|
||||
id={statusId}
|
||||
featured
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
showThread
|
||||
wrapperComponent={MaterialStatus}
|
||||
focusable={false}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
|
||||
export default @injectIntl
|
||||
class StatusReplyMentions extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status } = this.props;
|
||||
|
||||
if (!status.get('in_reply_to_id')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const to = status.get('mentions', []);
|
||||
|
||||
// The post is a reply, but it has no mentions.
|
||||
if (to.size === 0) {
|
||||
// The author is replying to themself.
|
||||
if (status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
|
||||
return (
|
||||
<div className='reply-mentions'>
|
||||
<FormattedMessage
|
||||
id='reply_mentions.reply'
|
||||
defaultMessage='Replying to {accounts}{more}'
|
||||
values={{
|
||||
accounts: (<>
|
||||
<HoverRefWrapper accountId={status.getIn(['account', 'id'])} inline>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}`} className='reply-mentions__account'>@{status.getIn(['account', 'username'])}</Link>
|
||||
</HoverRefWrapper>
|
||||
</>),
|
||||
more: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// The reply-to is unknown. Rare, but it can happen.
|
||||
return (
|
||||
<div className='reply-mentions'>
|
||||
<FormattedMessage
|
||||
id='reply_mentions.reply_empty'
|
||||
defaultMessage='Replying to post'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The typical case with a reply-to and a list of mentions.
|
||||
return (
|
||||
<div className='reply-mentions'>
|
||||
<FormattedMessage
|
||||
id='reply_mentions.reply'
|
||||
defaultMessage='Replying to {accounts}{more}'
|
||||
values={{
|
||||
accounts: to.slice(0, 2).map(account => (<>
|
||||
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||
<Link to={`/@${account.get('acct')}`} className='reply-mentions__account'>@{account.get('username')}</Link>
|
||||
</HoverRefWrapper>
|
||||
{' '}
|
||||
</>)),
|
||||
more: to.size > 2 && (
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/mentions`}>
|
||||
<FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ const mapStateToProps = state => {
|
|||
account: state.getIn(['accounts', me]),
|
||||
logo: getSoapboxConfig(state).get('logo'),
|
||||
notificationCount: state.getIn(['notifications', 'unread']),
|
||||
chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||
chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||
dashboardCount: reportsCount + approvalCount,
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
|
|
|
@ -6,4 +6,5 @@ const mapStateToProps = (state, { pollId }) => ({
|
|||
me: state.get('me'),
|
||||
});
|
||||
|
||||
|
||||
export default connect(mapStateToProps)(Poll);
|
||||
|
|
|
@ -17,7 +17,7 @@ import { preload } from '../actions/preload';
|
|||
import { IntlProvider } from 'react-intl';
|
||||
import ErrorBoundary from '../components/error_boundary';
|
||||
import { loadInstance } from 'soapbox/actions/instance';
|
||||
import { fetchSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { loadSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { fetchMe } from 'soapbox/actions/me';
|
||||
import PublicLayout from 'soapbox/features/public_layout';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
|
@ -25,6 +25,7 @@ import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
|||
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||
import messages from 'soapbox/locales/messages';
|
||||
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
import { createGlobals } from 'soapbox/globals';
|
||||
|
||||
const validLocale = locale => Object.keys(messages).includes(locale);
|
||||
|
||||
|
@ -33,13 +34,16 @@ const previewVideoState = 'previewVideoModal';
|
|||
|
||||
export const store = configureStore();
|
||||
|
||||
// Configure global functions for developers
|
||||
createGlobals(store);
|
||||
|
||||
store.dispatch(preload());
|
||||
|
||||
store.dispatch(fetchMe())
|
||||
.then(() => {
|
||||
// Postpone for authenticated fetch
|
||||
store.dispatch(loadInstance());
|
||||
store.dispatch(fetchSoapboxConfig());
|
||||
store.dispatch(loadSoapboxConfig());
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
|
|
|
@ -66,174 +66,176 @@ const makeMapStateToProps = () => {
|
|||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onReply(status, router) {
|
||||
dispatch((_, getState) => {
|
||||
const state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(status, router)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(status, router));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onModalReblog(status) {
|
||||
const mapDispatchToProps = (dispatch, { intl }) => {
|
||||
function onModalReblog(status) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
dispatch(reblog(status));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onReblog(status, e) {
|
||||
dispatch((_, getState) => {
|
||||
const boostModal = getSettings(getState()).get('boostModal');
|
||||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
return {
|
||||
onReply(status, router) {
|
||||
dispatch((_, getState) => {
|
||||
const state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(status, router)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(status, router));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onModalReblog,
|
||||
|
||||
onReblog(status, e) {
|
||||
dispatch((_, getState) => {
|
||||
const boostModal = getSettings(getState()).get('boostModal');
|
||||
if (e.shiftKey || !boostModal) {
|
||||
onModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: onModalReblog }));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onFavourite(status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
onFavourite(status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
},
|
||||
|
||||
onBookmark(status) {
|
||||
if (status.get('bookmarked')) {
|
||||
dispatch(unbookmark(intl, status));
|
||||
} else {
|
||||
dispatch(bookmark(intl, status));
|
||||
}
|
||||
},
|
||||
|
||||
onPin(status) {
|
||||
if (status.get('pinned')) {
|
||||
dispatch(unpin(status));
|
||||
} else {
|
||||
dispatch(pin(status));
|
||||
}
|
||||
},
|
||||
|
||||
onEmbed(status) {
|
||||
dispatch(openModal('EMBED', {
|
||||
url: status.get('url'),
|
||||
onError: error => dispatch(showAlertForError(error)),
|
||||
}));
|
||||
},
|
||||
|
||||
onDelete(status, history, withRedraft = false) {
|
||||
dispatch((_, getState) => {
|
||||
const deleteModal = getSettings(getState()).get('deleteModal');
|
||||
if (!deleteModal) {
|
||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
||||
onBookmark(status) {
|
||||
if (status.get('bookmarked')) {
|
||||
dispatch(unbookmark(intl, status));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
||||
}));
|
||||
dispatch(bookmark(intl, status));
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
onDirect(account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
onPin(status) {
|
||||
if (status.get('pinned')) {
|
||||
dispatch(unpin(status));
|
||||
} else {
|
||||
dispatch(pin(status));
|
||||
}
|
||||
},
|
||||
|
||||
onChat(account, router) {
|
||||
dispatch(launchChat(account.get('id'), router));
|
||||
},
|
||||
onEmbed(status) {
|
||||
dispatch(openModal('EMBED', {
|
||||
url: status.get('url'),
|
||||
onError: error => dispatch(showAlertForError(error)),
|
||||
}));
|
||||
},
|
||||
|
||||
onMention(account, router) {
|
||||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
onDelete(status, history, withRedraft = false) {
|
||||
dispatch((_, getState) => {
|
||||
const deleteModal = getSettings(getState()).get('deleteModal');
|
||||
if (!deleteModal) {
|
||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onOpenMedia(media, index) {
|
||||
dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
onDirect(account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
||||
onOpenVideo(media, time) {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
},
|
||||
onChat(account, router) {
|
||||
dispatch(launchChat(account.get('id'), router));
|
||||
},
|
||||
|
||||
onOpenAudio(media, time) {
|
||||
dispatch(openModal('AUDIO', { media, time }));
|
||||
},
|
||||
onMention(account, router) {
|
||||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
|
||||
onBlock(status) {
|
||||
const account = status.get('account');
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account, status));
|
||||
},
|
||||
}));
|
||||
},
|
||||
onOpenMedia(media, index) {
|
||||
dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
|
||||
onReport(status) {
|
||||
dispatch(initReport(status.get('account'), status));
|
||||
},
|
||||
onOpenVideo(media, time) {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
},
|
||||
|
||||
onMute(account) {
|
||||
dispatch(initMuteModal(account));
|
||||
},
|
||||
onOpenAudio(media, time) {
|
||||
dispatch(openModal('AUDIO', { media, time }));
|
||||
},
|
||||
|
||||
onMuteConversation(status) {
|
||||
if (status.get('muted')) {
|
||||
dispatch(unmuteStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(muteStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
onBlock(status) {
|
||||
const account = status.get('account');
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account, status));
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
onToggleHidden(status) {
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
onReport(status) {
|
||||
dispatch(initReport(status.get('account'), status));
|
||||
},
|
||||
|
||||
onGroupRemoveAccount(groupId, accountId) {
|
||||
dispatch(createRemovedAccount(groupId, accountId));
|
||||
},
|
||||
onMute(account) {
|
||||
dispatch(initMuteModal(account));
|
||||
},
|
||||
|
||||
onGroupRemoveStatus(groupId, statusId) {
|
||||
dispatch(groupRemoveStatus(groupId, statusId));
|
||||
},
|
||||
onMuteConversation(status) {
|
||||
if (status.get('muted')) {
|
||||
dispatch(unmuteStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(muteStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onDeactivateUser(status) {
|
||||
dispatch(deactivateUserModal(intl, status.getIn(['account', 'id'])));
|
||||
},
|
||||
onToggleHidden(status) {
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onDeleteUser(status) {
|
||||
dispatch(deleteUserModal(intl, status.getIn(['account', 'id'])));
|
||||
},
|
||||
onGroupRemoveAccount(groupId, accountId) {
|
||||
dispatch(createRemovedAccount(groupId, accountId));
|
||||
},
|
||||
|
||||
onDeleteStatus(status) {
|
||||
dispatch(deleteStatusModal(intl, status.get('id')));
|
||||
},
|
||||
onGroupRemoveStatus(groupId, statusId) {
|
||||
dispatch(groupRemoveStatus(groupId, statusId));
|
||||
},
|
||||
|
||||
onToggleStatusSensitivity(status) {
|
||||
dispatch(toggleStatusSensitivityModal(intl, status.get('id'), status.get('sensitive')));
|
||||
},
|
||||
onDeactivateUser(status) {
|
||||
dispatch(deactivateUserModal(intl, status.getIn(['account', 'id'])));
|
||||
},
|
||||
|
||||
});
|
||||
onDeleteUser(status) {
|
||||
dispatch(deleteUserModal(intl, status.getIn(['account', 'id'])));
|
||||
},
|
||||
|
||||
onDeleteStatus(status) {
|
||||
dispatch(deleteStatusModal(intl, status.get('id')));
|
||||
},
|
||||
|
||||
onToggleStatusSensitivity(status) {
|
||||
dispatch(toggleStatusSensitivityModal(intl, status.get('id'), status.get('sensitive')));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||
|
|
|
@ -68,6 +68,8 @@ const messages = defineMessages({
|
|||
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
|
||||
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' },
|
||||
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
|
||||
suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
|
||||
unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
@ -177,66 +179,150 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if ('share' in navigator) {
|
||||
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.share, { name: account.get('username') }),
|
||||
action: this.handleShare,
|
||||
icon: require('feather-icons/dist/icons/share.svg'),
|
||||
});
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (account.get('id') === me) {
|
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), to: '/settings/profile' });
|
||||
menu.push({ text: intl.formatMessage(messages.preferences), to: '/settings/preferences' });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.edit_profile),
|
||||
to: '/settings/profile',
|
||||
icon: require('@tabler/icons/icons/user.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.preferences),
|
||||
to: '/settings/preferences',
|
||||
icon: require('@tabler/icons/icons/settings.svg'),
|
||||
});
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.follow_requests),
|
||||
to: '/follow_requests',
|
||||
icon: require('@tabler/icons/icons/user-plus.svg'),
|
||||
});
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mutes),
|
||||
to: '/mutes',
|
||||
icon: require('@tabler/icons/icons/circle-x.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.blocks),
|
||||
to: '/blocks',
|
||||
icon: require('@tabler/icons/icons/ban.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.domain_blocks),
|
||||
to: '/domain_blocks',
|
||||
icon: require('@tabler/icons/icons/ban.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mention, { name: account.get('username') }),
|
||||
action: this.props.onMention,
|
||||
icon: require('feather-icons/dist/icons/at-sign.svg'),
|
||||
});
|
||||
|
||||
if (account.getIn(['pleroma', 'accepts_chat_messages'], false) === true) {
|
||||
menu.push({ text: intl.formatMessage(messages.chat, { name: account.get('username') }), action: this.props.onChat });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.chat, { name: account.get('username') }),
|
||||
action: this.props.onChat,
|
||||
icon: require('@tabler/icons/icons/messages.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.direct, { name: account.get('username') }),
|
||||
action: this.props.onDirect,
|
||||
icon: require('@tabler/icons/icons/mail.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }),
|
||||
action: this.props.onReblogToggle,
|
||||
icon: require('@tabler/icons/icons/repeat.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }),
|
||||
action: this.props.onReblogToggle,
|
||||
icon: require('@tabler/icons/icons/repeat.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (features.accountSubscriptions) {
|
||||
if (account.getIn(['relationship', 'subscribing'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unsubscribe, { name: account.get('username') }), action: this.props.onSubscriptionToggle });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.unsubscribe, { name: account.get('username') }),
|
||||
action: this.props.onSubscriptionToggle,
|
||||
icon: require('@tabler/icons/icons/bell.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.subscribe, { name: account.get('username') }), action: this.props.onSubscriptionToggle });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.subscribe, { name: account.get('username') }),
|
||||
action: this.props.onSubscriptionToggle,
|
||||
icon: require('@tabler/icons/icons/bell-off.svg'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (features.lists) {
|
||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.add_or_remove_from_list),
|
||||
action: this.props.onAddToList,
|
||||
icon: require('@tabler/icons/icons/list.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
// menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
|
||||
menu.push(null);
|
||||
} else if (features.lists && features.unrestrictedLists) {
|
||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.add_or_remove_from_list),
|
||||
action: this.props.onAddToList,
|
||||
icon: require('@tabler/icons/icons/list.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.unmute, { name: account.get('username') }),
|
||||
action: this.props.onMute,
|
||||
icon: require('@tabler/icons/icons/circle-x.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mute, { name: account.get('username') }),
|
||||
action: this.props.onMute,
|
||||
icon: require('@tabler/icons/icons/circle-x.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.unblock, { name: account.get('username') }),
|
||||
action: this.props.onBlock,
|
||||
icon: require('@tabler/icons/icons/ban.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.block, { name: account.get('username') }),
|
||||
action: this.props.onBlock,
|
||||
icon: require('@tabler/icons/icons/ban.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.report, { name: account.get('username') }),
|
||||
action: this.props.onReport,
|
||||
icon: require('@tabler/icons/icons/flag.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (isRemote(account)) {
|
||||
|
@ -245,9 +331,17 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push(null);
|
||||
|
||||
if (account.getIn(['relationship', 'domain_blocking'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.unblockDomain, { domain }),
|
||||
action: this.props.onUnblockDomain,
|
||||
icon: require('@tabler/icons/icons/ban.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.blockDomain, { domain }),
|
||||
action: this.props.onBlockDomain,
|
||||
icon: require('@tabler/icons/icons/ban.svg'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,31 +349,90 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push(null);
|
||||
|
||||
if (isAdmin(meAccount)) {
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_account, { name: account.get('username') }),
|
||||
href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true,
|
||||
icon: require('icons/gavel.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (account.get('id') !== me && isLocal(account)) {
|
||||
if (account.get('id') !== me && isLocal(account) && isAdmin(meAccount)) {
|
||||
if (isAdmin(account)) {
|
||||
menu.push({ text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }), action: this.props.onPromoteToModerator });
|
||||
menu.push({ text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), action: this.props.onDemoteToUser });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }),
|
||||
action: this.props.onPromoteToModerator,
|
||||
icon: require('@tabler/icons/icons/arrow-up-circle.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }),
|
||||
action: this.props.onDemoteToUser,
|
||||
icon: require('@tabler/icons/icons/arrow-down-circle.svg'),
|
||||
});
|
||||
} else if (isModerator(account)) {
|
||||
menu.push({ text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), action: this.props.onPromoteToAdmin });
|
||||
menu.push({ text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), action: this.props.onDemoteToUser });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }),
|
||||
action: this.props.onPromoteToAdmin,
|
||||
icon: require('@tabler/icons/icons/arrow-up-circle.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }),
|
||||
action: this.props.onDemoteToUser,
|
||||
icon: require('@tabler/icons/icons/arrow-down-circle.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), action: this.props.onPromoteToAdmin });
|
||||
menu.push({ text: intl.formatMessage(messages.promoteToModerator, { name: account.get('username') }), action: this.props.onPromoteToModerator });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }),
|
||||
action: this.props.onPromoteToAdmin,
|
||||
icon: require('@tabler/icons/icons/arrow-up-circle.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.promoteToModerator, { name: account.get('username') }),
|
||||
action: this.props.onPromoteToModerator,
|
||||
icon: require('@tabler/icons/icons/arrow-up-circle.svg'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isVerified(account)) {
|
||||
menu.push({ text: intl.formatMessage(messages.unverifyUser, { name: account.get('username') }), action: this.props.onUnverifyUser });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.unverifyUser, { name: account.get('username') }),
|
||||
action: this.props.onUnverifyUser,
|
||||
icon: require('@tabler/icons/icons/check.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.verifyUser, { name: account.get('username') }), action: this.props.onVerifyUser });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.verifyUser, { name: account.get('username') }),
|
||||
action: this.props.onVerifyUser,
|
||||
icon: require('@tabler/icons/icons/check.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (features.suggestionsV2 && isAdmin(meAccount)) {
|
||||
if (account.getIn(['pleroma', 'is_suggested'])) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.unsuggestUser, { name: account.get('username') }),
|
||||
action: this.props.onUnsuggestUser,
|
||||
icon: require('@tabler/icons/icons/user-x.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.suggestUser, { name: account.get('username') }),
|
||||
action: this.props.onSuggestUser,
|
||||
icon: require('@tabler/icons/icons/user-check.svg'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (account.get('id') !== me) {
|
||||
menu.push({ text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }), action: this.props.onDeactivateUser });
|
||||
menu.push({ text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }), action: this.props.onDeleteUser });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }),
|
||||
action: this.props.onDeactivateUser,
|
||||
icon: require('@tabler/icons/icons/user-off.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }),
|
||||
icon: require('@tabler/icons/icons/user-minus.svg'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,6 +489,23 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
renderShareButton() {
|
||||
const { intl, account, me } = this.props;
|
||||
const canShare = 'share' in navigator;
|
||||
|
||||
if (!(account && me && account.get('id') === me && canShare)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
src={require('feather-icons/dist/icons/share.svg')}
|
||||
onClick={this.handleShare}
|
||||
title={intl.formatMessage(messages.share, { name: account.get('username') })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, intl, username, me, features } = this.props;
|
||||
const { isSmallScreen } = this.state;
|
||||
|
@ -436,6 +606,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__extra__buttons'>
|
||||
{me && <DropdownMenuContainer items={menu} src={require('@tabler/icons/icons/dots.svg')} direction='right' />}
|
||||
{this.renderShareButton()}
|
||||
{this.renderMessageButton()}
|
||||
<ActionButton account={account} />
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from 'soapbox/actions/accounts';
|
||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
||||
import LoadingIndicator from 'soapbox/components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import Column from 'soapbox/components/column';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
|
||||
import MediaItem from './components/media_item';
|
||||
|
@ -17,11 +17,12 @@ import MissingIndicator from 'soapbox/components/missing_indicator';
|
|||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
|
||||
const mapStateToProps = (state, { params, withReplies = false }) => {
|
||||
const username = params.username || '';
|
||||
const me = state.get('me');
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
|
||||
const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
|
||||
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
|
@ -186,6 +187,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<Column>
|
||||
<SubNavigation message={`@${accountUsername}`} />
|
||||
<div className='slist slist--flex' onScroll={this.handleScroll}>
|
||||
<div className='account__section-headline'>
|
||||
<div style={{ width: '100%', display: 'flex' }}>
|
||||
|
|
|
@ -117,6 +117,14 @@ export default class Header extends ImmutablePureComponent {
|
|||
this.props.onDemoteToUser(this.props.account);
|
||||
}
|
||||
|
||||
handleSuggestUser = () => {
|
||||
this.props.onSuggestUser(this.props.account);
|
||||
}
|
||||
|
||||
handleUnsuggestUser = () => {
|
||||
this.props.onUnsuggestUser(this.props.account);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, identity_proofs } = this.props;
|
||||
const moved = (account) ? account.get('moved') : false;
|
||||
|
@ -148,6 +156,8 @@ export default class Header extends ImmutablePureComponent {
|
|||
onPromoteToAdmin={this.handlePromoteToAdmin}
|
||||
onPromoteToModerator={this.handlePromoteToModerator}
|
||||
onDemoteToUser={this.handleDemoteToUser}
|
||||
onSuggestUser={this.handleSuggestUser}
|
||||
onUnsuggestUser={this.handleUnsuggestUser}
|
||||
username={this.props.username}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class MovedNote extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='account__moved-note'>
|
||||
<div className='account__moved-note__message'>
|
||||
<div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div>
|
||||
<div className='account__moved-note__icon-wrapper'><Icon src={require('feather-icons/dist/icons/briefcase.svg')} className='account__moved-note__icon' fixedWidth /></div>
|
||||
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ import {
|
|||
promoteToAdmin,
|
||||
promoteToModerator,
|
||||
demoteToUser,
|
||||
suggestUsers,
|
||||
unsuggestUsers,
|
||||
} from 'soapbox/actions/admin';
|
||||
import { isAdmin } from 'soapbox/utils/accounts';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
|
@ -47,7 +49,8 @@ const messages = defineMessages({
|
|||
promotedToModerator: { id: 'admin.users.actions.promote_to_moderator_message', defaultMessage: '@{acct} was promoted to a moderator' },
|
||||
demotedToModerator: { id: 'admin.users.actions.demote_to_moderator_message', defaultMessage: '@{acct} was demoted to a moderator' },
|
||||
demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
|
||||
|
||||
userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
|
||||
userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -145,7 +148,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
|
||||
onBlockDomain(domain) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockDomainConfirm),
|
||||
onConfirm: () => dispatch(blockDomain(domain)),
|
||||
}));
|
||||
|
@ -213,6 +216,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
onSuggestUser(account) {
|
||||
const message = intl.formatMessage(messages.userSuggested, { acct: account.get('acct') });
|
||||
|
||||
dispatch(suggestUsers([account.get('id')]))
|
||||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
onUnsuggestUser(account) {
|
||||
const message = intl.formatMessage(messages.userUnsuggested, { acct: account.get('acct') });
|
||||
|
||||
dispatch(unsuggestUsers([account.get('id')]))
|
||||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||
|
|
|
@ -7,7 +7,7 @@ import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../acti
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import StatusList from '../../components/status_list';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import Column from 'soapbox/components/column';
|
||||
// import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
|
@ -27,7 +27,7 @@ const makeMapStateToProps = () => {
|
|||
const mapStateToProps = (state, { params, withReplies = false }) => {
|
||||
const username = params.username || '';
|
||||
const me = state.get('me');
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
|
||||
const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
|
||||
let accountId = -1;
|
||||
|
|
|
@ -37,9 +37,11 @@ class Report extends ImmutablePureComponent {
|
|||
return [{
|
||||
text: intl.formatMessage(messages.deactivateUser, { name: report.getIn(['account', 'username']) }),
|
||||
action: this.handleDeactivateUser,
|
||||
icon: require('@tabler/icons/icons/user-off.svg'),
|
||||
}, {
|
||||
text: intl.formatMessage(messages.deleteUser, { name: report.getIn(['account', 'username']) }),
|
||||
action: this.handleDeleteUser,
|
||||
icon: require('@tabler/icons/icons/user-minus.svg'),
|
||||
}];
|
||||
}
|
||||
|
||||
|
@ -113,7 +115,7 @@ class Report extends ImmutablePureComponent {
|
|||
<Button className='button-alternative' size={30} onClick={this.handleCloseReport}>
|
||||
<FormattedMessage id='admin.reports.actions.close' defaultMessage='Close' />
|
||||
</Button>
|
||||
<DropdownMenu items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
<DropdownMenu className='admin-report__dropdown' items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -32,9 +32,12 @@ class ReportStatus extends ImmutablePureComponent {
|
|||
return [{
|
||||
text: intl.formatMessage(messages.viewStatus, { acct: `@${acct}` }),
|
||||
to: `/@${acct}/posts/${status.get('id')}`,
|
||||
icon: require('@tabler/icons/icons/pencil.svg'),
|
||||
}, {
|
||||
text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }),
|
||||
action: this.handleDeleteStatus,
|
||||
icon: require('@tabler/icons/icons/trash.svg'),
|
||||
destructive: true,
|
||||
}];
|
||||
}
|
||||
|
||||
|
@ -116,7 +119,7 @@ class ReportStatus extends ImmutablePureComponent {
|
|||
{media}
|
||||
</div>
|
||||
<div className='admin-report__status-actions'>
|
||||
<DropdownMenu items={menu} icon='ellipsis-v' size={18} direction='right' />
|
||||
<DropdownMenu items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,9 +5,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { deleteUsers, approveUsers } from 'soapbox/actions/admin';
|
||||
import { approveUsers } from 'soapbox/actions/admin';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { rejectUserModal } from '../../../actions/moderation';
|
||||
|
||||
const messages = defineMessages({
|
||||
approved: { id: 'admin.awaiting_approval.approved_message', defaultMessage: '{acct} was approved!' },
|
||||
|
@ -37,7 +38,6 @@ class UnapprovedAccount extends ImmutablePureComponent {
|
|||
|
||||
handleApprove = () => {
|
||||
const { dispatch, intl, account } = this.props;
|
||||
|
||||
dispatch(approveUsers([account.get('id')]))
|
||||
.then(() => {
|
||||
const message = intl.formatMessage(messages.approved, { acct: `@${account.get('acct')}` });
|
||||
|
@ -49,12 +49,10 @@ class UnapprovedAccount extends ImmutablePureComponent {
|
|||
handleReject = () => {
|
||||
const { dispatch, intl, account } = this.props;
|
||||
|
||||
dispatch(deleteUsers([account.get('id')]))
|
||||
.then(() => {
|
||||
const message = intl.formatMessage(messages.rejected, { acct: `@${account.get('acct')}` });
|
||||
dispatch(snackbar.info(message));
|
||||
})
|
||||
.catch(() => {});
|
||||
dispatch(rejectUserModal(intl, account.get('id'), () => {
|
||||
const message = intl.formatMessage(messages.rejected, { acct: `@${account.get('acct')}` });
|
||||
dispatch(snackbar.info(message));
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -12,6 +12,7 @@ import sourceCode from 'soapbox/utils/code';
|
|||
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { isAdmin } from 'soapbox/utils/accounts';
|
||||
import { isNumber } from 'soapbox/utils/numbers';
|
||||
|
||||
// https://stackoverflow.com/a/53230807
|
||||
const download = (response, filename) => {
|
||||
|
@ -102,16 +103,18 @@ class Dashboard extends ImmutablePureComponent {
|
|||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
{retention && <div className='dashcounter'>
|
||||
<div>
|
||||
<div className='dashcounter__num'>
|
||||
{retention}%
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />
|
||||
{isNumber(retention) && (
|
||||
<div className='dashcounter'>
|
||||
<div>
|
||||
<div className='dashcounter__num'>
|
||||
{retention}%
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
<div className='dashcounter'>
|
||||
<Link to='/timeline/local'>
|
||||
<div className='dashcounter__num'>
|
||||
|
|
|
@ -44,6 +44,7 @@ class Reports extends ImmutablePureComponent {
|
|||
return [{
|
||||
text: intl.formatMessage(messages.modlog),
|
||||
to: '/admin/log',
|
||||
icon: require('@tabler/icons/icons/list.svg'),
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { SimpleForm, TextInput } from 'soapbox/features/forms';
|
|||
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, is } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
||||
empty: { id: 'admin.user_index.empty', defaultMessage: 'No users found.' },
|
||||
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
|
||||
});
|
||||
|
@ -100,7 +101,7 @@ class UserIndex extends ImmutablePureComponent {
|
|||
const showLoading = isLoading && accountIds.isEmpty();
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Column heading={intl.formatMessage(messages.heading)}>
|
||||
<SimpleForm style={{ paddingBottom: 0 }}>
|
||||
<TextInput
|
||||
onChange={this.handleQueryChange}
|
||||
|
|
|
@ -65,7 +65,7 @@ class Account extends ImmutablePureComponent {
|
|||
if (!added && accountId !== me) {
|
||||
button = (
|
||||
<div className='account__relationship'>
|
||||
<IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={this.handleOnAdd} />
|
||||
<IconButton src={require('@tabler/icons/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={this.handleOnAdd} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -72,8 +72,7 @@ class Search extends React.PureComponent {
|
|||
</label>
|
||||
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
<Icon src={require('@tabler/icons/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} />
|
||||
</div>
|
||||
<Button onClick={this.handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,9 @@ exports[`<NativeCaptchaField /> renders correctly 1`] = `
|
|||
className="input required"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
onChange={[Function]}
|
||||
placeholder="Enter the pictured text"
|
||||
required={true}
|
||||
|
|
|
@ -14,7 +14,9 @@ exports[`<LoginForm /> renders for Mastodon 1`] = `
|
|||
>
|
||||
<input
|
||||
aria-label="Username"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="string email"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
|
@ -23,17 +25,49 @@ exports[`<LoginForm /> renders for Mastodon 1`] = `
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="input password user_password"
|
||||
className="input required showable-password password user_password"
|
||||
>
|
||||
<input
|
||||
aria-label="Password"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
className="password"
|
||||
autoCorrect="off"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required={true}
|
||||
type="password"
|
||||
/>
|
||||
<button
|
||||
aria-label="Show password"
|
||||
className="icon-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
tabIndex="0"
|
||||
title="Show password"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
style={Object {}}
|
||||
>
|
||||
<div
|
||||
className="svg-icon"
|
||||
>
|
||||
<svg
|
||||
id={
|
||||
Object {
|
||||
"process": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className="hint subtle-hint"
|
||||
|
@ -74,7 +108,9 @@ exports[`<LoginForm /> renders for Pleroma 1`] = `
|
|||
>
|
||||
<input
|
||||
aria-label="Username"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="string email"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
|
@ -83,17 +119,49 @@ exports[`<LoginForm /> renders for Pleroma 1`] = `
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="input password user_password"
|
||||
className="input required showable-password password user_password"
|
||||
>
|
||||
<input
|
||||
aria-label="Password"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
className="password"
|
||||
autoCorrect="off"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required={true}
|
||||
type="password"
|
||||
/>
|
||||
<button
|
||||
aria-label="Show password"
|
||||
className="icon-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
tabIndex="0"
|
||||
title="Show password"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
style={Object {}}
|
||||
>
|
||||
<div
|
||||
className="svg-icon"
|
||||
>
|
||||
<svg
|
||||
id={
|
||||
Object {
|
||||
"process": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className="hint subtle-hint"
|
||||
|
|
|
@ -17,7 +17,9 @@ exports[`<LoginPage /> renders correctly on load 1`] = `
|
|||
>
|
||||
<input
|
||||
aria-label="Username"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="string email"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
|
@ -26,17 +28,49 @@ exports[`<LoginPage /> renders correctly on load 1`] = `
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="input password user_password"
|
||||
className="input required showable-password password user_password"
|
||||
>
|
||||
<input
|
||||
aria-label="Password"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
className="password"
|
||||
autoCorrect="off"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required={true}
|
||||
type="password"
|
||||
/>
|
||||
<button
|
||||
aria-label="Show password"
|
||||
className="icon-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
tabIndex="0"
|
||||
title="Show password"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
style={Object {}}
|
||||
>
|
||||
<div
|
||||
className="svg-icon"
|
||||
>
|
||||
<svg
|
||||
id={
|
||||
Object {
|
||||
"process": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className="hint subtle-hint"
|
||||
|
|
|
@ -107,6 +107,8 @@ export const NativeCaptchaField = ({ captcha, onChange, onClick, name, value })
|
|||
name={name}
|
||||
value={value}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Link } from 'react-router-dom';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { getBaseURL } from 'soapbox/utils/state';
|
||||
import ShowablePassword from 'soapbox/components/showable_password';
|
||||
|
||||
const messages = defineMessages({
|
||||
username: { id: 'login.fields.username_placeholder', defaultMessage: 'Username' },
|
||||
|
@ -40,20 +41,25 @@ class LoginForm extends ImmutablePureComponent {
|
|||
type='text'
|
||||
name='username'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className='input password user_password'>
|
||||
<ShowablePassword
|
||||
aria-label={intl.formatMessage(messages.password)}
|
||||
className='password user_password'
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
name='password'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
required
|
||||
/>
|
||||
{/* <div className='input password user_password'>
|
||||
<input
|
||||
aria-label={intl.formatMessage(messages.password)}
|
||||
className='password'
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
type='password'
|
||||
name='password'
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
<p className='hint subtle-hint'>
|
||||
{hasResetPasswordAPI ? (
|
||||
<Link to='/auth/reset_password'>
|
||||
|
|
|
@ -5,6 +5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { connect } from 'react-redux';
|
||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { CancelToken } from 'axios';
|
||||
import { debounce } from 'lodash';
|
||||
import ShowablePassword from 'soapbox/components/showable_password';
|
||||
import {
|
||||
SimpleForm,
|
||||
SimpleInput,
|
||||
|
@ -19,6 +22,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { accountLookup } from 'soapbox/actions/accounts';
|
||||
|
||||
const messages = defineMessages({
|
||||
username: { id: 'registration.fields.username_placeholder', defaultMessage: 'Username' },
|
||||
|
@ -38,6 +42,7 @@ const mapStateToProps = (state, props) => ({
|
|||
needsConfirmation: state.getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']),
|
||||
needsApproval: state.getIn(['instance', 'approval_required']),
|
||||
supportsEmailList: getFeatures(state.get('instance')).emailList,
|
||||
supportsAccountLookup: getFeatures(state.get('instance')).accountLookup,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -51,6 +56,7 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
needsConfirmation: PropTypes.bool,
|
||||
needsApproval: PropTypes.bool,
|
||||
supportsEmailList: PropTypes.bool,
|
||||
supportsAccountLookup: PropTypes.bool,
|
||||
inviteToken: PropTypes.string,
|
||||
}
|
||||
|
||||
|
@ -63,6 +69,17 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
submissionLoading: false,
|
||||
params: ImmutableMap(),
|
||||
captchaIdempotencyKey: uuidv4(),
|
||||
usernameUnavailable: false,
|
||||
passwordConfirmation: '',
|
||||
passwordMismatch: false,
|
||||
}
|
||||
|
||||
source = CancelToken.source();
|
||||
|
||||
refreshCancelToken = () => {
|
||||
this.source.cancel();
|
||||
this.source = CancelToken.source();
|
||||
return this.source;
|
||||
}
|
||||
|
||||
setParams = map => {
|
||||
|
@ -73,10 +90,42 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
this.setParams({ [e.target.name]: e.target.value });
|
||||
}
|
||||
|
||||
onUsernameChange = e => {
|
||||
this.setParams({ username: e.target.value });
|
||||
this.setState({ usernameUnavailable: false });
|
||||
this.source.cancel();
|
||||
|
||||
this.usernameAvailable(e.target.value);
|
||||
}
|
||||
|
||||
onCheckboxChange = e => {
|
||||
this.setParams({ [e.target.name]: e.target.checked });
|
||||
}
|
||||
|
||||
onPasswordChange = e => {
|
||||
const password = e.target.value;
|
||||
const { passwordConfirmation } = this.state;
|
||||
this.onInputChange(e);
|
||||
|
||||
if (password === passwordConfirmation) {
|
||||
this.setState({ passwordMismatch: false });
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordConfirmChange = e => {
|
||||
const password = this.state.params.get('password', '');
|
||||
const passwordConfirmation = e.target.value;
|
||||
this.setState({ passwordConfirmation });
|
||||
|
||||
if (password === passwordConfirmation) {
|
||||
this.setState({ passwordMismatch: false });
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordConfirmBlur = e => {
|
||||
this.setState({ passwordMismatch: !this.passwordsMatch() });
|
||||
}
|
||||
|
||||
launchModal = () => {
|
||||
const { dispatch, intl, needsConfirmation, needsApproval } = this.props;
|
||||
|
||||
|
@ -113,9 +162,38 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
passwordsMatch = () => {
|
||||
const { params, passwordConfirmation } = this.state;
|
||||
return params.get('password', '') === passwordConfirmation;
|
||||
}
|
||||
|
||||
usernameAvailable = debounce(username => {
|
||||
const { dispatch, supportsAccountLookup } = this.props;
|
||||
|
||||
if (!supportsAccountLookup) return;
|
||||
|
||||
const source = this.refreshCancelToken();
|
||||
|
||||
dispatch(accountLookup(username, source.token))
|
||||
.then(account => {
|
||||
this.setState({ usernameUnavailable: !!account });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response && error.response.status === 404) {
|
||||
this.setState({ usernameUnavailable: false });
|
||||
}
|
||||
});
|
||||
|
||||
}, 1000, { trailing: true });
|
||||
|
||||
onSubmit = e => {
|
||||
const { dispatch, inviteToken } = this.props;
|
||||
|
||||
if (!this.passwordsMatch()) {
|
||||
this.setState({ passwordMismatch: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const params = this.state.params.withMutations(params => {
|
||||
// Locale for confirmation email
|
||||
params.set('locale', this.props.locale);
|
||||
|
@ -159,7 +237,7 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
|
||||
render() {
|
||||
const { instance, intl, supportsEmailList } = this.props;
|
||||
const { params } = this.state;
|
||||
const { params, usernameUnavailable, passwordConfirmation, passwordMismatch } = this.state;
|
||||
const isLoading = this.state.captchaLoading || this.state.submissionLoading;
|
||||
|
||||
return (
|
||||
|
@ -167,13 +245,22 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
<fieldset disabled={isLoading}>
|
||||
<div className='simple_form__overlay-area'>
|
||||
<div className='fields-group'>
|
||||
{usernameUnavailable && (
|
||||
<div className='error'>
|
||||
<FormattedMessage id='registration.username_unavailable' defaultMessage='Username is already taken.' />
|
||||
</div>
|
||||
)}
|
||||
<TextInput
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
name='username'
|
||||
hint={intl.formatMessage(messages.username_hint)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
pattern='^[a-zA-Z\d_-]+'
|
||||
onChange={this.onInputChange}
|
||||
onChange={this.onUsernameChange}
|
||||
value={params.get('username', '')}
|
||||
error={usernameUnavailable}
|
||||
required
|
||||
/>
|
||||
<SimpleInput
|
||||
|
@ -181,23 +268,38 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
name='email'
|
||||
type='email'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={this.onInputChange}
|
||||
value={params.get('email', '')}
|
||||
required
|
||||
/>
|
||||
<SimpleInput
|
||||
{passwordMismatch && (
|
||||
<div className='error'>
|
||||
<FormattedMessage id='registration.password_mismatch' defaultMessage="Passwords don't match." />
|
||||
</div>
|
||||
)}
|
||||
<ShowablePassword
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
name='password'
|
||||
type='password'
|
||||
autoComplete='off'
|
||||
onChange={this.onInputChange}
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={this.onPasswordChange}
|
||||
value={params.get('password', '')}
|
||||
error={passwordMismatch === true}
|
||||
required
|
||||
/>
|
||||
<SimpleInput
|
||||
<ShowablePassword
|
||||
placeholder={intl.formatMessage(messages.confirm)}
|
||||
name='confirm'
|
||||
type='password'
|
||||
name='password_confirmation'
|
||||
autoComplete='off'
|
||||
onChange={this.onInputChange}
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={this.onPasswordConfirmChange}
|
||||
onBlur={this.onPasswordConfirmBlur}
|
||||
value={passwordConfirmation}
|
||||
error={passwordMismatch === true}
|
||||
required
|
||||
/>
|
||||
{instance.get('approval_required') &&
|
||||
|
@ -206,8 +308,8 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
hint={<FormattedMessage id='registration.reason_hint' defaultMessage='This will help us review your application' />}
|
||||
name='reason'
|
||||
maxLength={500}
|
||||
autoComplete='off'
|
||||
onChange={this.onInputChange}
|
||||
value={params.get('reason', '')}
|
||||
required
|
||||
/>}
|
||||
</div>
|
||||
|
@ -225,12 +327,14 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
label={intl.formatMessage(messages.agreement, { tos: <Link to='/about/tos' target='_blank' key={0}>{intl.formatMessage(messages.tos)}</Link> })}
|
||||
name='agreement'
|
||||
onChange={this.onCheckboxChange}
|
||||
checked={params.get('agreement', false)}
|
||||
required
|
||||
/>
|
||||
{supportsEmailList && <Checkbox
|
||||
label={intl.formatMessage(messages.newsletter)}
|
||||
name='accepts_email_list'
|
||||
onChange={this.onCheckboxChange}
|
||||
checked={params.get('accepts_email_list', false)}
|
||||
/>}
|
||||
</div>
|
||||
<div className='actions'>
|
||||
|
|
|
@ -52,6 +52,7 @@ class Backups extends ImmutablePureComponent {
|
|||
return [{
|
||||
text: intl.formatMessage(messages.create),
|
||||
action: this.handleCreateBackup,
|
||||
icon: require('@tabler/icons/icons/plus.svg'),
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from '../ui/components/column';
|
||||
import Column from 'soapbox/components/column';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import StatusList from '../../components/status_list';
|
||||
|
@ -38,15 +39,22 @@ class Bookmarks extends ImmutablePureComponent {
|
|||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
fetchData = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchBookmarkedStatuses());
|
||||
return dispatch(fetchBookmarkedStatuses());
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandBookmarkedStatuses());
|
||||
}, 300, { leading: true })
|
||||
|
||||
handleRefresh = () => {
|
||||
return this.fetchData();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||
|
@ -55,7 +63,8 @@ class Bookmarks extends ImmutablePureComponent {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.heading)} transparent>
|
||||
<Column transparent>
|
||||
<SubNavigation message={intl.formatMessage(messages.heading)} />
|
||||
<StatusList
|
||||
trackScroll={!pinned}
|
||||
statusIds={statusIds}
|
||||
|
@ -63,6 +72,7 @@ class Bookmarks extends ImmutablePureComponent {
|
|||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onRefresh={this.handleRefresh}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { displayFqn } from 'soapbox/utils/state';
|
|||
|
||||
const mapStateToProps = (state, { params }) => {
|
||||
const getChat = makeGetChat();
|
||||
const chat = state.getIn(['chats', params.chatId], ImmutableMap()).toJS();
|
||||
const chat = state.getIn(['chats', 'items', params.chatId], ImmutableMap()).toJS();
|
||||
|
||||
return {
|
||||
me: state.get('me'),
|
||||
|
|
|
@ -2,19 +2,20 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { makeGetChat } from 'soapbox/selectors';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getChat = makeGetChat();
|
||||
|
||||
const mapStateToProps = (state, { chatId }) => {
|
||||
const chat = state.getIn(['chats', chatId]);
|
||||
const chat = state.getIn(['chats', 'items', chatId]);
|
||||
|
||||
return {
|
||||
chat: chat ? getChat(state, chat.toJS()) : undefined,
|
||||
|
@ -43,6 +44,8 @@ class Chat extends ImmutablePureComponent {
|
|||
const account = chat.get('account');
|
||||
const unreadCount = chat.get('unread');
|
||||
const content = chat.getIn(['last_message', 'content']);
|
||||
const attachment = chat.getIn(['last_message', 'attachment']);
|
||||
const image = attachment && attachment.getIn(['pleroma', 'mime_type'], '').startsWith('image/');
|
||||
const parsedContent = content ? emojify(content) : '';
|
||||
|
||||
return (
|
||||
|
@ -54,10 +57,24 @@ class Chat extends ImmutablePureComponent {
|
|||
<Avatar account={account} size={36} />
|
||||
</div>
|
||||
<DisplayName account={account} />
|
||||
<span
|
||||
className='chat__last-message'
|
||||
dangerouslySetInnerHTML={{ __html: parsedContent }}
|
||||
/>
|
||||
{attachment && (
|
||||
<Icon
|
||||
className='chat__attachment-icon'
|
||||
src={image ? require('@tabler/icons/icons/photo.svg') : require('@tabler/icons/icons/paperclip.svg')}
|
||||
/>
|
||||
)}
|
||||
{content ? (
|
||||
<span
|
||||
className='chat__last-message'
|
||||
dangerouslySetInnerHTML={{ __html: parsedContent }}
|
||||
/>
|
||||
) : attachment && (
|
||||
<span
|
||||
className='chat__last-message attachment'
|
||||
>
|
||||
{image ? <FormattedMessage id='chats.attachment_image' defaultMessage='Image' /> : <FormattedMessage id='chats.attachment' defaultMessage='Attachment' />}
|
||||
</span>
|
||||
)}
|
||||
{unreadCount > 0 && <i className='icon-with-badge__badge'>{shortNumberFormat(unreadCount)}</i>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,7 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = (state, { chatId }) => ({
|
||||
me: state.get('me'),
|
||||
chat: state.getIn(['chats', chatId]),
|
||||
chat: state.getIn(['chats', 'items', chatId]),
|
||||
chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()),
|
||||
});
|
||||
|
||||
|
@ -158,7 +158,10 @@ class ChatBox extends ImmutablePureComponent {
|
|||
{truncateFilename(attachment.preview_url, 20)}
|
||||
</div>
|
||||
<div class='chat-box__remove-attachment'>
|
||||
<IconButton icon='remove' onClick={this.handleRemoveFile} />
|
||||
<IconButton
|
||||
src={require('@tabler/icons/icons/x.svg')}
|
||||
onClick={this.handleRemoveFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,11 +2,19 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { debounce } from 'lodash';
|
||||
import { expandChats } from 'soapbox/actions/chats';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import PlaceholderChat from 'soapbox/features/placeholder/components/placeholder_chat';
|
||||
import Chat from './chat';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const messages = defineMessages({
|
||||
emptyMessage: { id: 'chat_panels.main_window.empty', defaultMessage: 'No chats found. To start a chat, visit a user\'s profile' },
|
||||
});
|
||||
|
||||
const getSortedChatIds = chats => (
|
||||
chats
|
||||
.toList()
|
||||
|
@ -32,7 +40,9 @@ const makeMapStateToProps = () => {
|
|||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
chatIds: sortedChatIdsSelector(state.get('chats')),
|
||||
chatIds: sortedChatIdsSelector(state.getIn(['chats', 'items'])),
|
||||
hasMore: !!state.getIn(['chats', 'next']),
|
||||
isLoading: state.getIn(['chats', 'loading']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
@ -47,28 +57,40 @@ class ChatList extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
chatIds: ImmutablePropTypes.list,
|
||||
onClickChat: PropTypes.func,
|
||||
emptyMessage: PropTypes.node,
|
||||
onRefresh: PropTypes.func,
|
||||
hasMore: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandChats());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render() {
|
||||
const { chatIds, emptyMessage } = this.props;
|
||||
const { intl, chatIds, hasMore, isLoading } = this.props;
|
||||
|
||||
return (
|
||||
<div className='chat-list'>
|
||||
<div className='chat-list__content'>
|
||||
{chatIds.count() === 0 &&
|
||||
<div className='empty-column-indicator'>{emptyMessage}</div>
|
||||
}
|
||||
{chatIds.map(chatId => (
|
||||
<div key={chatId} className='chat-list-item'>
|
||||
<Chat
|
||||
chatId={chatId}
|
||||
onClick={this.props.onClickChat}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ScrollableList
|
||||
className='chat-list'
|
||||
scrollKey='awaiting-approval'
|
||||
emptyMessage={intl.formatMessage(messages.emptyMessage)}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && chatIds.size === 0}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onRefresh={this.props.onRefresh}
|
||||
placeholderComponent={PlaceholderChat}
|
||||
placeholderCount={20}
|
||||
>
|
||||
{chatIds.map(chatId => (
|
||||
<div key={chatId} className='chat-list-item'>
|
||||
<Chat
|
||||
chatId={chatId}
|
||||
onClick={this.props.onClickChat}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -261,8 +261,17 @@ class ChatMessageList extends ImmutablePureComponent {
|
|||
renderMessage = (chatMessage) => {
|
||||
const { me, intl } = this.props;
|
||||
const menu = [
|
||||
{ text: intl.formatMessage(messages.delete), action: this.handleDeleteMessage(chatMessage.get('chat_id'), chatMessage.get('id')) },
|
||||
{ text: intl.formatMessage(messages.report), action: this.handleReportUser(chatMessage.get('account_id')) },
|
||||
{
|
||||
text: intl.formatMessage(messages.delete),
|
||||
action: this.handleDeleteMessage(chatMessage.get('chat_id'), chatMessage.get('id')),
|
||||
icon: require('@tabler/icons/icons/trash.svg'),
|
||||
destructive: true,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.report),
|
||||
action: this.handleReportUser(chatMessage.get('account_id')),
|
||||
icon: require('@tabler/icons/icons/flag.svg'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -287,8 +296,7 @@ class ChatMessageList extends ImmutablePureComponent {
|
|||
<div className='chat-message__menu'>
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
size={18}
|
||||
src={require('@tabler/icons/icons/dots.svg')}
|
||||
direction='top'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const getChatsUnreadCount = state => {
|
||||
const chats = state.get('chats');
|
||||
const chats = state.getIn(['chats', 'items']);
|
||||
return chats.reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
|
||||
};
|
||||
|
||||
|
@ -30,7 +30,7 @@ const normalizePanes = (chats, panes = ImmutableList()) => (
|
|||
);
|
||||
|
||||
const makeNormalizeChatPanes = () => createSelector([
|
||||
state => state.get('chats'),
|
||||
state => state.getIn(['chats', 'items']),
|
||||
state => getSettings(state).getIn(['chats', 'panes']),
|
||||
], normalizePanes);
|
||||
|
||||
|
@ -93,7 +93,6 @@ class ChatPanes extends ImmutablePureComponent {
|
|||
<>
|
||||
<ChatList
|
||||
onClickChat={this.handleClickChat}
|
||||
emptyMessage={<FormattedMessage id='chat_panels.main_window.empty' defaultMessage="No chats found. To start a chat, visit a user's profile." />}
|
||||
/>
|
||||
<AccountSearch
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
|
|
|
@ -22,7 +22,7 @@ const makeMapStateToProps = () => {
|
|||
const getChat = makeGetChat();
|
||||
|
||||
const mapStateToProps = (state, { chatId }) => {
|
||||
const chat = state.getIn(['chats', chatId]);
|
||||
const chat = state.getIn(['chats', 'items', chatId]);
|
||||
|
||||
return {
|
||||
me: state.get('me'),
|
||||
|
|
|
@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
|||
import { connect } from 'react-redux';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { launchChat } from 'soapbox/actions/chats';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { fetchChats, launchChat } from 'soapbox/actions/chats';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ChatList from './components/chat_list';
|
||||
import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
|
||||
import AccountSearch from 'soapbox/components/account_search';
|
||||
|
@ -35,6 +35,11 @@ class ChatIndex extends React.PureComponent {
|
|||
this.context.router.history.push(`/chats/${chat.get('id')}`);
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
const { dispatch } = this.props;
|
||||
return dispatch(fetchChats());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
|
@ -56,7 +61,7 @@ class ChatIndex extends React.PureComponent {
|
|||
|
||||
<ChatList
|
||||
onClickChat={this.handleClickChat}
|
||||
emptyMessage={<FormattedMessage id='chat_panels.main_window.empty' defaultMessage="No chats found. To start a chat, visit a user's profile." />}
|
||||
onRefresh={this.handleRefresh}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
@ -70,6 +70,12 @@ class CommunityTimeline extends React.PureComponent {
|
|||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
return dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { intl, onlyMedia, timelineId } = this.props;
|
||||
|
||||
|
@ -80,6 +86,7 @@ class CommunityTimeline extends React.PureComponent {
|
|||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onRefresh={this.handleRefresh}
|
||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||
/>
|
||||
</Column>
|
||||
|
|
|
@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import ReplyMentions from '../containers/reply_mentions_container';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
|
@ -308,7 +309,9 @@ export default class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
<WarningContainer />
|
||||
|
||||
{ !shouldCondense && <ReplyIndicatorContainer /> }
|
||||
{!shouldCondense && <ReplyIndicatorContainer />}
|
||||
|
||||
{!shouldCondense && <ReplyMentions />}
|
||||
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||
<AutosuggestInput
|
||||
|
|
|
@ -90,8 +90,8 @@ class Option extends React.PureComponent {
|
|||
onKeyPress={this.handleCheckboxKeypress}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
title={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
|
||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
|
||||
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
/>
|
||||
|
||||
<AutosuggestInput
|
||||
|
@ -109,7 +109,7 @@ class Option extends React.PureComponent {
|
|||
</label>
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
|
||||
<IconButton title={intl.formatMessage(messages.remove_option)} src={require('@tabler/icons/icons/x.svg')} onClick={this.handleOptionRemove} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
@ -178,7 +178,7 @@ class PollForm extends ImmutablePureComponent {
|
|||
|
||||
<div className='poll__footer'>
|
||||
{options.size < maxOptions && (
|
||||
<button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
|
||||
<button className='button button-secondary' onClick={this.handleAddOption}><Icon src={require('@tabler/icons/icons/plus.svg')} /> <FormattedMessage {...messages.add_option} /></button>
|
||||
)}
|
||||
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
|
|
|
@ -46,7 +46,9 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
||||
<div className='reply-indicator__cancel'>
|
||||
<IconButton title={intl.formatMessage(messages.cancel)} src={require('@tabler/icons/icons/x.svg')} onClick={this.handleClick} inverted />
|
||||
</div>
|
||||
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default @injectIntl
|
||||
class ReplyMentions extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onOpenMentionsModal: PropTypes.func.isRequired,
|
||||
explicitAddressing: PropTypes.bool,
|
||||
to: ImmutablePropTypes.orderedSet,
|
||||
isReply: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onOpenMentionsModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { explicitAddressing, to, isReply } = this.props;
|
||||
|
||||
if (!explicitAddressing || !isReply || !to || to.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href='#' className='reply-mentions' onClick={this.handleClick}>
|
||||
<FormattedMessage
|
||||
id='reply_mentions.reply'
|
||||
defaultMessage='Replying to {accounts}{more}'
|
||||
values={{
|
||||
accounts: to.slice(0, 2).map(acct => <><span className='reply-mentions__account'>@{acct.split('@')[0]}</span>{' '}</>),
|
||||
more: to.size > 2 && <FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -115,7 +115,7 @@ class ScheduleForm extends React.Component {
|
|||
ref={this.setRef}
|
||||
/>
|
||||
<div className='datepicker__cancel'>
|
||||
<IconButton size={20} title={intl.formatMessage(messages.remove)} icon='times' onClick={this.handleRemove} />
|
||||
<IconButton title={intl.formatMessage(messages.remove)} src={require('@tabler/icons/icons/x.svg')} onClick={this.handleRemove} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,47 +1,15 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
action: { id: 'search.action', defaultMessage: 'Search for “{query}”' },
|
||||
});
|
||||
|
||||
class SearchPopout extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { style } = this.props;
|
||||
const extraInformation = <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns posts you have written, favorited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' />;
|
||||
return (
|
||||
<div className='search-popout-container' style={{ ...style, position: 'absolute', zIndex: 1000 }}>
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
||||
<ul>
|
||||
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
|
||||
<li><em>@username</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='post' /></li>
|
||||
</ul>
|
||||
{extraInformation}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class Search extends React.PureComponent {
|
||||
|
||||
|
@ -56,13 +24,16 @@ class Search extends React.PureComponent {
|
|||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func.isRequired,
|
||||
onSelected: PropTypes.func,
|
||||
openInRoute: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
autosuggest: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
autoFocus: false,
|
||||
ausosuggest: false,
|
||||
}
|
||||
|
||||
state = {
|
||||
|
@ -81,15 +52,18 @@ class Search extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleKeyUp = (e) => {
|
||||
handleSubmit = () => {
|
||||
this.props.onSubmit();
|
||||
|
||||
if (this.props.openInRoute) {
|
||||
this.context.router.history.push('/search');
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onSubmit();
|
||||
|
||||
if (this.props.openInRoute) {
|
||||
this.context.router.history.push('/search');
|
||||
}
|
||||
this.handleSubmit();
|
||||
} else if (e.key === 'Escape') {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
|
@ -104,34 +78,51 @@ class Search extends React.PureComponent {
|
|||
this.setState({ expanded: false });
|
||||
}
|
||||
|
||||
handleSelected = accountId => {
|
||||
const { onSelected } = this.props;
|
||||
|
||||
if (onSelected) {
|
||||
onSelected(accountId, this.context.router.history);
|
||||
}
|
||||
}
|
||||
|
||||
makeMenu = () => {
|
||||
const { intl, value } = this.props;
|
||||
|
||||
return [
|
||||
{ text: intl.formatMessage(messages.action, { query: value }), icon: require('@tabler/icons/icons/search.svg'), action: this.handleSubmit },
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, value, autoFocus, submitted } = this.props;
|
||||
const { expanded } = this.state;
|
||||
const { intl, value, autoFocus, autosuggest, submitted } = this.props;
|
||||
const hasValue = value.length > 0 || submitted;
|
||||
|
||||
const Component = autosuggest ? AutosuggestAccountInput : 'input';
|
||||
|
||||
return (
|
||||
<div className='search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
|
||||
<input
|
||||
<Component
|
||||
className='search__input'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
onSelected={this.handleSelected}
|
||||
autoFocus={autoFocus}
|
||||
autoSelect={false}
|
||||
menu={this.makeMenu()}
|
||||
/>
|
||||
</label>
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<Icon src={require('@tabler/icons/icons/search.svg')} className={classNames('svg-icon--search', { active: !hasValue })} />
|
||||
<Icon src={require('@tabler/icons/icons/backspace.svg')} className={classNames('svg-icon--backspace', { active: hasValue })} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
</div>
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this}>
|
||||
<SearchPopout />
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ import StatusContainer from '../../../containers/status_container';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Hashtag from '../../../components/hashtag';
|
||||
import FilterBar from '../../search/components/filter_bar';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { WhoToFollowPanel } from 'soapbox/features/ui/util/async-components';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
|
||||
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
|
||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||
import Pullable from 'soapbox/components/pullable';
|
||||
|
||||
export default class SearchResults extends ImmutablePureComponent {
|
||||
|
||||
|
@ -24,6 +23,8 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
selectedFilter: PropTypes.string.isRequired,
|
||||
selectFilter: PropTypes.func.isRequired,
|
||||
features: PropTypes.object.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
trends: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
|
||||
|
@ -31,15 +32,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
|
||||
|
||||
render() {
|
||||
const { value, results, submitted, selectedFilter, features } = this.props;
|
||||
|
||||
if (!submitted && features.suggestions && results.isEmpty()) {
|
||||
return (
|
||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||
{Component => <Component limit={5} />}
|
||||
</BundleContainer>
|
||||
);
|
||||
}
|
||||
const { value, results, submitted, selectedFilter, suggestions, trends } = this.props;
|
||||
|
||||
let searchResults;
|
||||
let hasMore = false;
|
||||
|
@ -47,14 +40,16 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
let noResultsMessage;
|
||||
let placeholderComponent = PlaceholderStatus;
|
||||
|
||||
if (selectedFilter === 'accounts' && results.get('accounts')) {
|
||||
if (selectedFilter === 'accounts') {
|
||||
hasMore = results.get('accountsHasMore');
|
||||
loaded = results.get('accountsLoaded');
|
||||
placeholderComponent = PlaceholderAccount;
|
||||
|
||||
if (results.get('accounts').size > 0) {
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
searchResults = results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||
} else {
|
||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
||||
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.get('account')} id={suggestion.get('account')} />);
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
|
@ -86,14 +81,16 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'hashtags' && results.get('hashtags')) {
|
||||
if (selectedFilter === 'hashtags') {
|
||||
hasMore = results.get('hashtagsHasMore');
|
||||
loaded = results.get('hashtagsLoaded');
|
||||
placeholderComponent = PlaceholderHashtag;
|
||||
|
||||
if (results.get('hashtags').size > 0) {
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
searchResults = results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />);
|
||||
} else {
|
||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
||||
searchResults = trends.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />);
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
|
@ -108,21 +105,23 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<>
|
||||
{submitted && <FilterBar selectedFilter={submitted ? selectedFilter : null} selectFilter={this.handleSelectFilter} />}
|
||||
<FilterBar selectedFilter={selectedFilter} selectFilter={this.handleSelectFilter} />
|
||||
|
||||
{noResultsMessage || (
|
||||
<ScrollableList
|
||||
key={selectedFilter}
|
||||
scrollKey={`${selectedFilter}:${value}`}
|
||||
isLoading={submitted && !loaded}
|
||||
showLoading={submitted && !loaded && results.isEmpty()}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
>
|
||||
{searchResults}
|
||||
</ScrollableList>
|
||||
<Pullable>
|
||||
<ScrollableList
|
||||
key={selectedFilter}
|
||||
scrollKey={`${selectedFilter}:${value}`}
|
||||
isLoading={submitted && !loaded}
|
||||
showLoading={submitted && !loaded && results.isEmpty()}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
>
|
||||
{searchResults}
|
||||
</ScrollableList>
|
||||
</Pullable>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -10,9 +10,13 @@ const messages = defineMessages({
|
|||
upload: { id: 'upload_button.label', defaultMessage: 'Add media attachment' },
|
||||
});
|
||||
|
||||
const onlyImages = types => {
|
||||
return Boolean(types && types.every(type => type.startsWith('image/')));
|
||||
};
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = state => ({
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
|
||||
attachmentTypes: state.getIn(['instance', 'configuration', 'media_attachments', 'supported_mime_types']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
@ -28,7 +32,7 @@ class UploadButton extends ImmutablePureComponent {
|
|||
onSelectFile: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
resetFileKey: PropTypes.number,
|
||||
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
|
||||
attachmentTypes: ImmutablePropTypes.listOf(PropTypes.string),
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -47,17 +51,21 @@ class UploadButton extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { intl, resetFileKey, unavailable, disabled } = this.props;
|
||||
const { intl, resetFileKey, attachmentTypes, unavailable, disabled } = this.props;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const src = onlyImages(attachmentTypes)
|
||||
? require('@tabler/icons/icons/photo.svg')
|
||||
: require('@tabler/icons/icons/paperclip.svg');
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton
|
||||
className='compose-form__upload-button-icon'
|
||||
src={require('@tabler/icons/icons/paperclip.svg')}
|
||||
src={src}
|
||||
title={intl.formatMessage(messages.upload)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
|
@ -69,8 +77,7 @@ class UploadButton extends ImmutablePureComponent {
|
|||
ref={this.setRef}
|
||||
type='file'
|
||||
multiple
|
||||
// Accept all types for now.
|
||||
// accept={acceptContentTypes.toArray().join(',')}
|
||||
accept={attachmentTypes && attachmentTypes.toArray().join(',')}
|
||||
onChange={this.handleChange}
|
||||
disabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import ReplyMentions from '../components/reply_mentions';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
return state => {
|
||||
const instance = state.get('instance');
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
if (!explicitAddressing) {
|
||||
return {
|
||||
explicitAddressing: false,
|
||||
};
|
||||
}
|
||||
|
||||
const status = getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) });
|
||||
|
||||
if (!status) {
|
||||
return {
|
||||
isReply: false,
|
||||
};
|
||||
}
|
||||
const to = state.getIn(['compose', 'to']);
|
||||
|
||||
return {
|
||||
to,
|
||||
isReply: true,
|
||||
explicitAddressing: true,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onOpenMentionsModal() {
|
||||
dispatch(openModal('REPLY_MENTIONS', {
|
||||
onCancel: () => dispatch(openModal('COMPOSE')),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyMentions);
|
|
@ -13,6 +13,16 @@ const mapStateToProps = state => ({
|
|||
submitted: state.getIn(['search', 'submitted']),
|
||||
});
|
||||
|
||||
function redirectToAccount(accountId, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
const acct = getState().getIn(['accounts', accountId, 'acct']);
|
||||
|
||||
if (acct && routerHistory) {
|
||||
routerHistory.push(`/@${acct}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch, { autoSubmit }) => {
|
||||
|
||||
const debouncedSubmit = debounce(() => {
|
||||
|
@ -40,6 +50,11 @@ const mapDispatchToProps = (dispatch, { autoSubmit }) => {
|
|||
dispatch(showSearch());
|
||||
},
|
||||
|
||||
onSelected(accountId, routerHistory) {
|
||||
dispatch(clearSearch());
|
||||
dispatch(redirectToAccount(accountId, routerHistory));
|
||||
},
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ const mapStateToProps = state => {
|
|||
value: state.getIn(['search', 'submittedValue']),
|
||||
results: state.getIn(['search', 'results']),
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
trends: state.getIn(['trends', 'items']),
|
||||
submitted: state.getIn(['search', 'submitted']),
|
||||
selectedFilter: state.getIn(['search', 'filter']),
|
||||
features: getFeatures(instance),
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class Conversation extends ImmutablePureComponent {
|
|||
conversationId: PropTypes.string.isRequired,
|
||||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
lastStatusId: PropTypes.string,
|
||||
unread:PropTypes.bool.isRequired,
|
||||
unread: PropTypes.bool.isRequired,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
markRead: PropTypes.func.isRequired,
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import {
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
SimpleTextarea,
|
||||
FieldsGroup,
|
||||
} from 'soapbox/features/forms';
|
||||
import { createApp } from 'soapbox/actions/apps';
|
||||
import { obtainOAuthToken } from 'soapbox/actions/oauth';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { getBaseURL } from 'soapbox/utils/accounts';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import Accordion from 'soapbox/features/ui/components/accordion';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.app_create', defaultMessage: 'Create app' },
|
||||
namePlaceholder: { id: 'app_create.name_placeholder', defaultMessage: 'e.g. \'Soapbox\'' },
|
||||
scopesPlaceholder: { id: 'app_create.scopes_placeholder', defaultMessage: 'e.g. \'read write follow\'' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
|
||||
return {
|
||||
account,
|
||||
defaultScopes: features.scopes,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class CreateApp extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
defaultScopes: PropTypes.string,
|
||||
}
|
||||
|
||||
initialState = () => {
|
||||
return {
|
||||
params: ImmutableMap({
|
||||
client_name: '',
|
||||
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
scopes: '',
|
||||
website: '',
|
||||
}),
|
||||
app: null,
|
||||
token: null,
|
||||
isLoading: false,
|
||||
explanationExpanded: true,
|
||||
};
|
||||
}
|
||||
|
||||
state = this.initialState()
|
||||
|
||||
handleCreateApp = () => {
|
||||
const { dispatch, account } = this.props;
|
||||
const { params } = this.state;
|
||||
const baseURL = getBaseURL(account);
|
||||
|
||||
return dispatch(createApp(params.toJS(), baseURL))
|
||||
.then(app => this.setState({ app }));
|
||||
}
|
||||
|
||||
handleCreateToken = () => {
|
||||
const { dispatch, account } = this.props;
|
||||
const { app, params: appParams } = this.state;
|
||||
const baseURL = getBaseURL(account);
|
||||
|
||||
const tokenParams = {
|
||||
client_id: app.client_id,
|
||||
client_secret: app.client_secret,
|
||||
redirect_uri: appParams.get('redirect_uri'),
|
||||
grant_type: 'client_credentials',
|
||||
scope: appParams.get('scopes'),
|
||||
};
|
||||
|
||||
return dispatch(obtainOAuthToken(tokenParams, baseURL))
|
||||
.then(token => this.setState({ token }));
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
this.handleCreateApp()
|
||||
.then(this.handleCreateToken)
|
||||
.then(() => {
|
||||
this.scrollToTop();
|
||||
this.setState({ isLoading: false });
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
}
|
||||
|
||||
setParam = (key, value) => {
|
||||
const { params } = this.state;
|
||||
const newParams = params.set(key, value);
|
||||
|
||||
this.setState({ params: newParams });
|
||||
}
|
||||
|
||||
handleParamChange = key => {
|
||||
return e => {
|
||||
this.setParam(key, e.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
resetState = () => {
|
||||
this.setState(this.initialState());
|
||||
}
|
||||
|
||||
handleReset = e => {
|
||||
this.resetState();
|
||||
this.scrollToTop();
|
||||
}
|
||||
|
||||
toggleExplanation = expanded => {
|
||||
this.setState({ explanationExpanded: expanded });
|
||||
}
|
||||
|
||||
scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
renderResults = () => {
|
||||
const { intl } = this.props;
|
||||
const { app, token, explanationExpanded } = this.state;
|
||||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.heading)}>
|
||||
<SimpleForm>
|
||||
<FieldsGroup>
|
||||
<Accordion
|
||||
headline={<FormattedMessage id='app_create.results.explanation_title' defaultMessage='App created successfully' />}
|
||||
expanded={explanationExpanded}
|
||||
onToggle={this.toggleExplanation}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='app_create.results.explanation_text'
|
||||
defaultMessage='You created a new app and token! Please copy the credentials somewhere; you will not see them again after navigating away from this page.'
|
||||
/>
|
||||
</Accordion>
|
||||
</FieldsGroup>
|
||||
<FieldsGroup>
|
||||
<div className='code-editor'>
|
||||
<SimpleTextarea
|
||||
label={<FormattedMessage id='app_create.results.app_label' defaultMessage='App' />}
|
||||
value={JSON.stringify(app, null, 2)}
|
||||
rows={10}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</FieldsGroup>
|
||||
<FieldsGroup>
|
||||
<div className='code-editor'>
|
||||
<SimpleTextarea
|
||||
label={<FormattedMessage id='app_create.results.token_label' defaultMessage='OAuth token' />}
|
||||
value={JSON.stringify(token, null, 2)}
|
||||
rows={10}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</FieldsGroup>
|
||||
<div className='actions'>
|
||||
<button name='button' onClick={this.handleReset} className='btn button button-primary'>
|
||||
<FormattedMessage id='app_create.restart' defaultMessage='Create another' />
|
||||
</button>
|
||||
</div>
|
||||
</SimpleForm>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
const { params, app, token, isLoading } = this.state;
|
||||
|
||||
if (app && token) {
|
||||
return this.renderResults();
|
||||
}
|
||||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.heading)}>
|
||||
<SimpleForm onSubmit={this.handleSubmit}>
|
||||
<fieldset disabled={isLoading}>
|
||||
<TextInput
|
||||
label={<FormattedMessage id='app_create.name_label' defaultMessage='App name' />}
|
||||
placeholder={intl.formatMessage(messages.namePlaceholder)}
|
||||
onChange={this.handleParamChange('client_name')}
|
||||
value={params.get('client_name')}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label={<FormattedMessage id='app_create.website_label' defaultMessage='Website' />}
|
||||
placeholder='https://soapbox.pub'
|
||||
onChange={this.handleParamChange('website')}
|
||||
value={params.get('website')}
|
||||
/>
|
||||
<TextInput
|
||||
label={<FormattedMessage id='app_create.redirect_uri_label' defaultMessage='Redirect URIs' />}
|
||||
placeholder='https://example.com'
|
||||
onChange={this.handleParamChange('redirect_uris')}
|
||||
value={params.get('redirect_uris')}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label={<FormattedMessage id='app_create.scopes_label' defaultMessage='Scopes' />}
|
||||
placeholder={intl.formatMessage(messages.scopesPlaceholder)}
|
||||
onChange={this.handleParamChange('scopes')}
|
||||
value={params.get('scopes')}
|
||||
required
|
||||
/>
|
||||
<div className='actions'>
|
||||
<button name='button' type='submit' className='btn button button-primary'>
|
||||
<FormattedMessage id='app_create.submit' defaultMessage='Create app' />
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</SimpleForm>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Column from '../ui/components/column';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.developers', defaultMessage: 'Developers' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class Developers extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.heading)}>
|
||||
<div className='dashcounters'>
|
||||
<div className='dashcounter'>
|
||||
<Link to='/developers/apps/create'>
|
||||
<div className='dashcounter__icon'>
|
||||
<Icon src={require('@tabler/icons/icons/apps.svg')} />
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='developers.navigation.app_create_label' defaultMessage='Create an app' />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='dashcounter'>
|
||||
<Link to='/error'>
|
||||
<div className='dashcounter__icon'>
|
||||
<Icon src={require('@tabler/icons/icons/mood-sad.svg')} />
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='developers.navigation.intentional_error_label' defaultMessage='Trigger an error' />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
me: state.get('me'),
|
||||
account: getAccount(state, id),
|
||||
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default @injectIntl
|
||||
@connect(makeMapStateToProps)
|
||||
class AccountCard extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
me: SoapboxPropTypes.me,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
autoPlayGif: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { account, autoPlayGif, me } = this.props;
|
||||
|
||||
const followedBy = me !== account.get('id') && account.getIn(['relationship', 'followed_by']);
|
||||
|
||||
return (
|
||||
<div className='directory__card'>
|
||||
{followedBy &&
|
||||
<div className='directory__card__info'>
|
||||
<span className='relationship-tag'>
|
||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||
</span>
|
||||
</div>}
|
||||
<div className='directory__card__action-button'>
|
||||
<ActionButton account={account} small />
|
||||
</div>
|
||||
<div className='directory__card__img'>
|
||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||
</div>
|
||||
|
||||
<div className='directory__card__bar'>
|
||||
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||
<Avatar account={account} size={48} />
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<div
|
||||
className={classNames('account__header__content', (account.get('note').length === 0 || account.get('note') === '<p></p>') && 'empty')}
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
|
||||
<div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
|
||||
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import AccountCard from './components/account_card';
|
||||
import RadioButton from 'soapbox/components/radio_button';
|
||||
import classNames from 'classnames';
|
||||
import LoadMore from 'soapbox/components/load_more';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||
recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
|
||||
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
|
||||
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
|
||||
federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
|
||||
title: state.getIn(['instance', 'title']),
|
||||
features: getFeatures(state.get('instance')),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Directory extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
params: PropTypes.shape({
|
||||
order: PropTypes.string,
|
||||
local: PropTypes.bool,
|
||||
}),
|
||||
features: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
order: null,
|
||||
local: null,
|
||||
};
|
||||
|
||||
getParams = (props, state) => ({
|
||||
order: state.order === null ? (props.params.order || 'active') : state.order,
|
||||
local: state.local === null ? (props.params.local || false) : state.local,
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchDirectory(this.getParams(this.props, this.state)));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { dispatch } = this.props;
|
||||
const paramsOld = this.getParams(prevProps, prevState);
|
||||
const paramsNew = this.getParams(this.props, this.state);
|
||||
|
||||
if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
|
||||
dispatch(fetchDirectory(paramsNew));
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeOrder = e => {
|
||||
this.setState({ order: e.target.value });
|
||||
}
|
||||
|
||||
handleChangeLocal = e => {
|
||||
this.setState({ local: e.target.value === '1' });
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(expandDirectory(this.getParams(this.props, this.state)));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, accountIds, intl, title, features } = this.props;
|
||||
const { order, local } = this.getParams(this.props, this.state);
|
||||
|
||||
return (
|
||||
<Column icon='address-book-o' heading={intl.formatMessage(messages.title)}>
|
||||
<div className='directory__filter-form'>
|
||||
<div className='directory__filter-form__column' role='group'>
|
||||
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
|
||||
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
|
||||
</div>
|
||||
|
||||
{features.federating && (
|
||||
<div className='directory__filter-form__column' role='group'>
|
||||
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: title })} checked={local} onChange={this.handleChangeLocal} />
|
||||
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
|
||||
</div>
|
||||
|
||||
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -103,6 +103,7 @@ class EditProfile extends ImmutablePureComponent {
|
|||
|
||||
const strangerNotifications = account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']);
|
||||
const acceptsEmailList = account.getIn(['pleroma', 'accepts_email_list']);
|
||||
const discoverable = account.getIn(['source', 'pleroma', 'discoverable']);
|
||||
|
||||
const initialState = account.withMutations(map => {
|
||||
map.merge(map.get('source'));
|
||||
|
@ -111,6 +112,7 @@ class EditProfile extends ImmutablePureComponent {
|
|||
map.set('stranger_notifications', strangerNotifications);
|
||||
map.set('accepts_email_list', acceptsEmailList);
|
||||
map.set('hide_network', hidesNetwork(account));
|
||||
map.set('discoverable', discoverable);
|
||||
unescapeParams(map, ['display_name', 'bio']);
|
||||
});
|
||||
|
||||
|
@ -309,6 +311,13 @@ class EditProfile extends ImmutablePureComponent {
|
|||
checked={this.state.stranger_notifications}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<FormattedMessage id='edit_profile.fields.discoverable_label' defaultMessage='Allow account discovery' />}
|
||||
hint={<FormattedMessage id='edit_profile.hints.discoverable' defaultMessage='Display account in profile directory and allow indexing by external services' />}
|
||||
name='discoverable'
|
||||
checked={this.state.discoverable}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
{supportsEmailList && <Checkbox
|
||||
label={<FormattedMessage id='edit_profile.fields.accepts_email_list_label' defaultMessage='Subscribe to newsletter' />}
|
||||
hint={<FormattedMessage id='edit_profile.hints.accepts_email_list' defaultMessage='Opt-in to news and marketing updates.' />}
|
||||
|
@ -338,7 +347,7 @@ class EditProfile extends ImmutablePureComponent {
|
|||
onChange={this.handleFieldChange(i, 'value')}
|
||||
/>
|
||||
{
|
||||
this.state.fields.size > 4 && <Icon id='times-circle' onClick={this.handleDeleteField(i)} />
|
||||
this.state.fields.size > 4 && <Icon className='delete-field' src={require('@tabler/icons/icons/circle-x.svg')} onClick={this.handleDeleteField(i)} />
|
||||
}
|
||||
</div>
|
||||
))
|
||||
|
@ -347,7 +356,7 @@ class EditProfile extends ImmutablePureComponent {
|
|||
this.state.fields.size < maxFields && (
|
||||
<div className='actions add-row'>
|
||||
<div name='button' type='button' role='presentation' className='btn button button-secondary' onClick={this.handleAddField}>
|
||||
<Icon id='plus-circle' />
|
||||
<Icon src={require('@tabler/icons/icons/circle-plus.svg')} />
|
||||
<FormattedMessage id='edit_profile.meta_fields.add' defaultMessage='Add new item' />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -53,7 +53,7 @@ class ExternalLoginForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<SimpleForm onSubmit={this.handleSubmit}>
|
||||
<SimpleForm onSubmit={this.handleSubmit} className='external-login'>
|
||||
<fieldset disabled={isLoading}>
|
||||
<FieldsGroup>
|
||||
<TextInput
|
||||
|
@ -63,6 +63,8 @@ class ExternalLoginForm extends ImmutablePureComponent {
|
|||
value={this.state.host}
|
||||
onChange={this.handleHostChange}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
required
|
||||
/>
|
||||
</FieldsGroup>
|
||||
|
|
|
@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from '../../actions/favourites';
|
||||
import Column from '../ui/components/column';
|
||||
import StatusList from '../../components/status_list';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { debounce } from 'lodash';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
|
@ -13,6 +13,10 @@ import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
|
|||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { findAccountByUsername } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.favourited_statuses', defaultMessage: 'Liked posts' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { params }) => {
|
||||
const username = params.username || '';
|
||||
const me = state.get('me');
|
||||
|
@ -29,7 +33,7 @@ const mapStateToProps = (state, { params }) => {
|
|||
};
|
||||
}
|
||||
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
|
||||
const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
|
||||
|
||||
let accountId = -1;
|
||||
if (accountFetchError) {
|
||||
|
@ -102,7 +106,7 @@ class Favourites extends ImmutablePureComponent {
|
|||
}, 300, { leading: true })
|
||||
|
||||
render() {
|
||||
const { statusIds, isLoading, hasMore, isMyAccount, isAccount, accountId, unavailable } = this.props;
|
||||
const { intl, statusIds, isLoading, hasMore, isMyAccount, isAccount, accountId, unavailable } = this.props;
|
||||
|
||||
if (!isMyAccount && !isAccount && accountId !== -1) {
|
||||
return (
|
||||
|
@ -135,7 +139,7 @@ class Favourites extends ImmutablePureComponent {
|
|||
: <FormattedMessage id='empty_column.account_favourited_statuses' defaultMessage="This user doesn't have any liked posts yet." />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Column heading={intl.formatMessage(messages.heading)}>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='favourited_statuses'
|
||||
|
|
|
@ -50,7 +50,7 @@ class InstanceRestrictions extends ImmutablePureComponent {
|
|||
items.push((
|
||||
<div className='federation-restriction' key='followers_only'>
|
||||
<div className='federation-restriction__icon'>
|
||||
<Icon id='lock' />
|
||||
<Icon src={require('@tabler/icons/icons/lock.svg')} />
|
||||
</div>
|
||||
<div className='federation-restriction__message'>
|
||||
<FormattedMessage
|
||||
|
@ -64,7 +64,7 @@ class InstanceRestrictions extends ImmutablePureComponent {
|
|||
items.push((
|
||||
<div className='federation-restriction' key='federated_timeline_removal'>
|
||||
<div className='federation-restriction__icon'>
|
||||
<Icon id='unlock' />
|
||||
<Icon src={require('@tabler/icons/icons/lock-open.svg')} />
|
||||
</div>
|
||||
<div className='federation-restriction__message'>
|
||||
<FormattedMessage
|
||||
|
@ -80,7 +80,7 @@ class InstanceRestrictions extends ImmutablePureComponent {
|
|||
items.push((
|
||||
<div className='federation-restriction' key='full_media_removal'>
|
||||
<div className='federation-restriction__icon'>
|
||||
<Icon id='photo' />
|
||||
<Icon src={require('@tabler/icons/icons/photo-off.svg')} />
|
||||
</div>
|
||||
<div className='federation-restriction__message'>
|
||||
<FormattedMessage
|
||||
|
@ -94,7 +94,7 @@ class InstanceRestrictions extends ImmutablePureComponent {
|
|||
items.push((
|
||||
<div className='federation-restriction' key='partial_media_removal'>
|
||||
<div className='federation-restriction__icon'>
|
||||
<Icon id='photo' />
|
||||
<Icon src={require('@tabler/icons/icons/photo-off.svg')} />
|
||||
</div>
|
||||
<div className='federation-restriction__message'>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -43,7 +43,7 @@ class RestrictedInstance extends ImmutablePureComponent {
|
|||
>
|
||||
<a href='#' className='restricted-instance__header' onClick={this.toggleExpanded}>
|
||||
<div className='restricted-instance__icon'>
|
||||
<Icon id={expanded ? 'caret-down' : 'caret-right'} />
|
||||
<Icon src={expanded ? require('@tabler/icons/icons/caret-down.svg') : require('@tabler/icons/icons/caret-right.svg')} />
|
||||
</div>
|
||||
<div className='restricted-instance__host'>
|
||||
{remoteInstance.get('host')}
|
||||
|
|
|
@ -215,54 +215,50 @@ class Filters extends ImmutablePureComponent {
|
|||
|
||||
<Button className='button button-primary setup' text={intl.formatMessage(messages.add_new)} onClick={this.handleAddNew} />
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading_filters)} />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='filters'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{filters.map((filter, i) => (
|
||||
<div key={i} className='filter__container'>
|
||||
<div className='filter__details'>
|
||||
<div className='filter__phrase'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' /></span>
|
||||
<span className='filter__list-value'>{filter.get('phrase')}</span>
|
||||
</div>
|
||||
<div className='filter__contexts'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
{filter.get('context').map((context, i) => (
|
||||
<span key={i} className='context'>{context}</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
<div className='filter__details'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_details_label' defaultMessage='Filter settings:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
{filter.get('irreversible') ?
|
||||
<span><FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /></span> :
|
||||
<span><FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' /></span>
|
||||
}
|
||||
{filter.get('whole_word') &&
|
||||
<span><FormattedMessage id='filters.filters_list_whole-word' defaultMessage='Whole word' /></span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='filter__delete' role='button' tabIndex='0' onClick={this.handleFilterDelete} data-value={filter.get('id')} aria-label={intl.formatMessage(messages.delete)}>
|
||||
<Icon className='filter__delete-icon' id='times' size={40} />
|
||||
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
</div>
|
||||
</SimpleForm>
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading_filters)} />
|
||||
|
||||
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='filters'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{filters.map((filter, i) => (
|
||||
<div key={i} className='filter__container'>
|
||||
<div className='filter__details'>
|
||||
<div className='filter__phrase'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' /></span>
|
||||
<span className='filter__list-value'>{filter.get('phrase')}</span>
|
||||
</div>
|
||||
<div className='filter__contexts'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
{filter.get('context').map((context, i) => (
|
||||
<span key={i} className='context'>{context}</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
<div className='filter__details'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_details_label' defaultMessage='Filter settings:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
{filter.get('irreversible') ?
|
||||
<span><FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /></span> :
|
||||
<span><FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' /></span>
|
||||
}
|
||||
{filter.get('whole_word') &&
|
||||
<span><FormattedMessage id='filters.filters_list_whole-word' defaultMessage='Whole word' /></span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='filter__delete' role='button' tabIndex='0' onClick={this.handleFilterDelete} data-value={filter.get('id')} aria-label={intl.formatMessage(messages.delete)}>
|
||||
<Icon className='filter__delete-icon' id='times' size={40} />
|
||||
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,8 +39,8 @@ class AccountAuthorize extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='account--panel'>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} src={require('@tabler/icons/icons/check.svg')} onClick={onAuthorize} /></div>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} src={require('@tabler/icons/icons/x.svg')} onClick={onReject} /></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue