Merge remote-tracking branch 'origin/develop' into a11y

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
strip-front-mentions
marcin mikołajczak 2022-01-05 22:25:03 +01:00
commit 149f677ab4
253 zmienionych plików z 6177 dodań i 2107 usunięć

Wyświetl plik

@ -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',

9
.gitignore vendored
Wyświetl plik

@ -12,3 +12,12 @@ yarn-error.log*
/static-test/
/public/
/dist/
.idea
.DS_Store
# surge.sh
CNAME
AUTH
CORS
ROUTER

Wyświetl plik

@ -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>

Wyświetl plik

@ -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",

Wyświetl plik

@ -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;
});
};
}

Wyświetl plik

@ -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 });
});
};
}

Wyświetl plik

@ -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') {

Wyświetl plik

@ -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));

Wyświetl plik

@ -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;

Wyświetl plik

@ -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,
});
};
}

Wyświetl plik

@ -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,
});

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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,
};
}

Wyświetl plik

@ -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,
});

Wyświetl plik

@ -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();

Wyświetl plik

@ -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') {

Wyświetl plik

@ -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']);

Wyświetl plik

@ -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,
}),
}),

Wyświetl plik

@ -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,
};
}

Wyświetl plik

@ -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];

Wyświetl plik

@ -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>
);

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;

Wyświetl plik

@ -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

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>
);

Wyświetl plik

@ -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;

Wyświetl plik

@ -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 ? (

Wyświetl plik

@ -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)}>

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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')} />

Wyświetl plik

@ -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>
);
}

Wyświetl plik

@ -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'),
}));
},
});

Wyświetl plik

@ -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;

Wyświetl plik

@ -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}
/>
));
}

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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),
};

Wyświetl plik

@ -6,4 +6,5 @@ const mapStateToProps = (state, { pollId }) => ({
me: state.get('me'),
});
export default connect(mapStateToProps)(Poll);

Wyświetl plik

@ -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(() => {});

Wyświetl plik

@ -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));

Wyświetl plik

@ -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>

Wyświetl plik

@ -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' }}>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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));

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>
);

Wyświetl plik

@ -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>
);

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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'>

Wyświetl plik

@ -44,6 +44,7 @@ class Reports extends ImmutablePureComponent {
return [{
text: intl.formatMessage(messages.modlog),
to: '/admin/log',
icon: require('@tabler/icons/icons/list.svg'),
}];
}

Wyświetl plik

@ -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}

Wyświetl plik

@ -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>
);
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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}

Wyświetl plik

@ -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"

Wyświetl plik

@ -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"

Wyświetl plik

@ -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
/>

Wyświetl plik

@ -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'>

Wyświetl plik

@ -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'>

Wyświetl plik

@ -52,6 +52,7 @@ class Backups extends ImmutablePureComponent {
return [{
text: intl.formatMessage(messages.create),
action: this.handleCreateBackup,
icon: require('@tabler/icons/icons/plus.svg'),
}];
}

Wyświetl plik

@ -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}

Wyświetl plik

@ -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'),

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>
);

Wyświetl plik

@ -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>
);
}

Wyświetl plik

@ -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)}
/>

Wyświetl plik

@ -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)}

Wyświetl plik

@ -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'),

Wyświetl plik

@ -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>
);

Wyświetl plik

@ -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>

Wyświetl plik

@ -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

Wyświetl plik

@ -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}>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>
);
}

Wyświetl plik

@ -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>
)}
</>
);

Wyświetl plik

@ -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' }}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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));
},
};
};

Wyświetl plik

@ -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),

Wyświetl plik

@ -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,

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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'

Wyświetl plik

@ -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

Wyświetl plik

@ -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')}

Wyświetl plik

@ -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>
);
}

Wyświetl plik

@ -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