diff --git a/app/soapbox/actions/aliases.js b/app/soapbox/actions/aliases.js new file mode 100644 index 000000000..cc62f8fa6 --- /dev/null +++ b/app/soapbox/actions/aliases.js @@ -0,0 +1,128 @@ +import { defineMessages } from 'react-intl'; +import api from '../api'; +import { importFetchedAccount, importFetchedAccounts } from './importer'; +import { showAlertForError } from './alerts'; +import snackbar from './snackbar'; +import { isLoggedIn } from 'soapbox/utils/auth'; +import { ME_PATCH_SUCCESS } from './me'; + +export const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE'; +export const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY'; +export const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR'; + +export const ALIASES_ADD_REQUEST = 'ALIASES_ADD_REQUEST'; +export const ALIASES_ADD_SUCCESS = 'ALIASES_ADD_SUCCESS'; +export const ALIASES_ADD_FAIL = 'ALIASES_ADD_FAIL'; + +export const ALIASES_REMOVE_REQUEST = 'ALIASES_REMOVE_REQUEST'; +export const ALIASES_REMOVE_SUCCESS = 'ALIASES_REMOVE_SUCCESS'; +export const ALIASES_REMOVE_FAIL = 'ALIASES_REMOVE_FAIL'; + +const messages = defineMessages({ + createSuccess: { id: 'aliases.success.add', defaultMessage: 'Account alias created successfully' }, + removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' }, +}); + +export const fetchAliasesSuggestions = q => (dispatch, getState) => { + if (!isLoggedIn(getState)) return; + + const params = { + q, + resolve: true, + limit: 4, + }; + + api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchAliasesSuggestionsReady(q, data)); + }).catch(error => dispatch(showAlertForError(error))); +}; + +export const fetchAliasesSuggestionsReady = (query, accounts) => ({ + type: ALIASES_SUGGESTIONS_READY, + query, + accounts, +}); + +export const clearAliasesSuggestions = () => ({ + type: ALIASES_SUGGESTIONS_CLEAR, +}); + +export const changeAliasesSuggestions = value => ({ + type: ALIASES_SUGGESTIONS_CHANGE, + value, +}); + +export const addToAliases = (intl, apId) => (dispatch, getState) => { + if (!isLoggedIn(getState)) return; + + const alsoKnownAs = getState().getIn(['meta', 'pleroma', 'also_known_as']); + + dispatch(addToAliasesRequest(apId)); + + api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, apId] }) + .then((response => { + dispatch(snackbar.success(intl.formatMessage(messages.createSuccess))); + dispatch(addToAliasesSuccess(response.data)); + })) + .catch(err => dispatch(addToAliasesFail(err))); +}; + +export const addToAliasesRequest = (apId) => ({ + type: ALIASES_ADD_REQUEST, + apId, +}); + +export const addToAliasesSuccess = me => dispatch => { + dispatch(importFetchedAccount(me)); + dispatch({ + type: ME_PATCH_SUCCESS, + me, + }); + dispatch({ + type: ALIASES_ADD_SUCCESS, + }); +}; + +export const addToAliasesFail = (apId, error) => ({ + type: ALIASES_ADD_FAIL, + apId, + error, +}); + +export const removeFromAliases = (intl, apId) => (dispatch, getState) => { + if (!isLoggedIn(getState)) return; + + const alsoKnownAs = getState().getIn(['meta', 'pleroma', 'also_known_as']); + + dispatch(removeFromAliasesRequest(apId)); + + api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== apId) }) + .then(response => { + dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess))); + dispatch(removeFromAliasesSuccess(response.data)); + }) + .catch(err => dispatch(removeFromAliasesFail(apId, err))); +}; + +export const removeFromAliasesRequest = (apId) => ({ + type: ALIASES_REMOVE_REQUEST, + apId, +}); + +export const removeFromAliasesSuccess = me => dispatch => { + dispatch(importFetchedAccount(me)); + dispatch({ + type: ME_PATCH_SUCCESS, + me, + }); + dispatch({ + type: ALIASES_REMOVE_SUCCESS, + }); +}; + +export const removeFromAliasesFail = (apId, error) => ({ + type: ALIASES_REMOVE_FAIL, + apId, + error, +}); diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index b6881f355..82eaba8f5 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -33,6 +33,7 @@ const messages = defineMessages({ admin_settings: { id: 'navigation_bar.admin_settings', defaultMessage: 'Admin settings' }, soapbox_config: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' }, import_data: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' }, + account_aliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' }, security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, lists: { id: 'column.lists', defaultMessage: 'Lists' }, @@ -258,6 +259,10 @@ class SidebarMenu extends ImmutablePureComponent { {intl.formatMessage(messages.import_data)} + + + {intl.formatMessage(messages.account_aliases)} + {intl.formatMessage(messages.security)} diff --git a/app/soapbox/features/aliases/components/account.js b/app/soapbox/features/aliases/components/account.js new file mode 100644 index 000000000..dcdfe7814 --- /dev/null +++ b/app/soapbox/features/aliases/components/account.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { makeGetAccount } from '../../../selectors'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Avatar from '../../../components/avatar'; +import DisplayName from '../../../components/display_name'; +import IconButton from '../../../components/icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import { addToAliases } from '../../../actions/aliases'; + +const messages = defineMessages({ + add: { id: 'aliases.account.add', defaultMessage: 'Create alias' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { accountId, added }) => { + const account = getAccount(state, accountId); + const apId = account.getIn(['pleroma', 'ap_id']); + + return { + account, + apId, + added: typeof added === 'undefined' ? state.getIn(['meta', 'pleroma', 'also_known_as']).includes(apId) : added, + me: state.get('me'), + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch) => ({ + onAdd: (intl, apId) => dispatch(addToAliases(intl, apId)), +}); + +export default @connect(makeMapStateToProps, mapDispatchToProps) +@injectIntl +class Account extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + apId: PropTypes.string.isRequired, + intl: PropTypes.object.isRequired, + onAdd: PropTypes.func.isRequired, + added: PropTypes.bool, + }; + + static defaultProps = { + added: false, + }; + + handleOnAdd = () => this.props.onAdd(this.props.intl, this.props.apId); + + render() { + const { account, accountId, intl, added, me } = this.props; + + let button; + + if (!added && accountId !== me) { + button = ( +
+ +
+ ); + } + + return ( +
+
+
+
+ +
+ + {button} +
+
+ ); + } + +} diff --git a/app/soapbox/features/aliases/components/search.js b/app/soapbox/features/aliases/components/search.js new file mode 100644 index 000000000..1416b0390 --- /dev/null +++ b/app/soapbox/features/aliases/components/search.js @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; +import { fetchAliasesSuggestions, clearAliasesSuggestions, changeAliasesSuggestions } from '../../../actions/aliases'; +import classNames from 'classnames'; +import Icon from 'soapbox/components/icon'; +import Button from 'soapbox/components/button'; + +const messages = defineMessages({ + search: { id: 'aliases.search', defaultMessage: 'Search your old account' }, + searchTitle: { id: 'tabs_bar.search', defaultMessage: 'Search' }, +}); + +const mapStateToProps = state => ({ + value: state.getIn(['aliases', 'suggestions', 'value']), +}); + +const mapDispatchToProps = dispatch => ({ + onSubmit: value => dispatch(fetchAliasesSuggestions(value)), + onClear: () => dispatch(clearAliasesSuggestions()), + onChange: value => dispatch(changeAliasesSuggestions(value)), +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class Search extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + onClear: PropTypes.func.isRequired, + }; + + handleChange = e => { + this.props.onChange(e.target.value); + } + + handleKeyUp = e => { + if (e.keyCode === 13) { + this.props.onSubmit(this.props.value); + } + } + + handleSubmit = () => { + this.props.onSubmit(this.props.value); + } + + handleClear = () => { + this.props.onClear(); + } + + render() { + const { value, intl } = this.props; + const hasValue = value.length > 0; + + return ( +
+ + +
+ + +
+ +
+ ); + } + +} diff --git a/app/soapbox/features/aliases/index.js b/app/soapbox/features/aliases/index.js new file mode 100644 index 000000000..b2653ca6f --- /dev/null +++ b/app/soapbox/features/aliases/index.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Column from '../ui/components/column'; +import ColumnSubheading from '../ui/components/column_subheading'; +import ScrollableList from '../../components/scrollable_list'; +import Icon from 'soapbox/components/icon'; +import Search from './components/search'; +import Account from './components/account'; +import { removeFromAliases } from '../../actions/aliases'; + +const messages = defineMessages({ + heading: { id: 'column.aliases', defaultMessage: 'Account aliases' }, + subheading_add_new: { id: 'column.aliases.subheading_add_new', defaultMessage: 'Add New Alias' }, + create_error: { id: 'column.aliases.create_error', defaultMessage: 'Error creating alias' }, + delete_error: { id: 'column.aliases.delete_error', defaultMessage: 'Error deleting alias' }, + subheading_aliases: { id: 'column.aliases.subheading_aliases', defaultMessage: 'Current aliases' }, + delete: { id: 'column.aliases.delete', defaultMessage: 'Delete' }, +}); + +const mapStateToProps = state => ({ + aliases: state.getIn(['meta', 'pleroma', 'also_known_as']), + searchAccountIds: state.getIn(['aliases', 'suggestions', 'items']), + loaded: state.getIn(['aliases', 'suggestions', 'loaded']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Aliases extends ImmutablePureComponent { + + handleFilterDelete = e => { + const { dispatch, intl } = this.props; + dispatch(removeFromAliases(intl, e.currentTarget.dataset.value)); + } + + render() { + const { intl, aliases, searchAccountIds, loaded } = this.props; + + const emptyMessage = ; + + return ( + + + + { + loaded && searchAccountIds.size === 0 ? ( +
+ +
+ ) : ( +
+ {searchAccountIds.map(accountId => )} +
+ ) + } + +
+ + {aliases.map((alias, i) => ( +
+
+ + {alias} +
+
+ + +
+
+ ))} +
+
+
+ ); + } + +} \ No newline at end of file diff --git a/app/soapbox/features/ui/components/link_footer.js b/app/soapbox/features/ui/components/link_footer.js index 424f00356..6e2d864fa 100644 --- a/app/soapbox/features/ui/components/link_footer.js +++ b/app/soapbox/features/ui/components/link_footer.js @@ -38,6 +38,7 @@ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => ( {isAdmin(account) &&
  • } {isAdmin(account) &&
  • }
  • +
  • }
  • diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 43d4b7b7c..bc963e028 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -98,6 +98,7 @@ import { ScheduledStatuses, UserIndex, FederationRestrictions, + Aliases, } from './util/async-components'; // Dummy import, to make sure that ends up in the application bundle. @@ -261,6 +262,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index c1b34b608..1e5645869 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -249,3 +249,7 @@ export function UserIndex() { export function FederationRestrictions() { return import(/* webpackChunkName: "features/federation_restrictions" */'../../federation_restrictions'); } + +export function Aliases() { + return import(/* webpackChunkName: "features/aliases" */'../../aliases'); +} diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index e74f85e80..5ae873e7d 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -42,7 +42,8 @@ "account.share": "Udostępnij profil @{name}", "account.show_reblogs": "Pokazuj podbicia od @{name}", "account.subscribe": "Subskrybuj wpisy @{name}", - "account.unblock": "Odblokuj @{name}", + "account.subscribed": "Subscribed", + "account.unblock": "Zasubskrybowano", "account.unblock_domain": "Odblokuj domenę {domain}", "account.unendorse": "Przestań polecać", "account.unfollow": "Przestań śledzić", @@ -50,8 +51,6 @@ "account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}", "account.unsubscribe": "Przestań subskrybować wpisy @{name}", "account_gallery.none": "Brak zawartości multimedialnej do wyświetlenia.", - "auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło", - "auth.logged_out": "Wylogowano.", "admin.awaiting_approval.approved_message": "Przyjęto {acct}!", "admin.awaiting_approval.empty_message": "Nikt nie oczekuje przyjęcia. Gdy zarejestruje się nowy użytkownik, możesz zatwierdzić go tutaj.", "admin.awaiting_approval.rejected_message": "Odrzucono {acct}!", @@ -95,7 +94,7 @@ "admin.users.actions.promote_to_admin": "Mianuj @{name} administratorem", "admin.users.actions.promote_to_admin_message": "Mianowano @{acct} administratorem", "admin.users.actions.promote_to_moderator": "Mianuj @{name} moderatorem", - "admin.users.actions.promote_to_moderatorem_message": "Mianowano @{acct} moderatorem", + "admin.users.actions.promote_to_moderator_message": "Mianowano @{acct} moderatorem", "admin.users.actions.unverify_user": "Cofnij weryfikację @{name}", "admin.users.actions.verify_user": "Weryfikuj @{name}", "admin.users.user_deactivated_message": "Zdezaktywowano @{acct}", @@ -110,13 +109,14 @@ "alert.unexpected.message": "Wystąpił nieoczekiwany błąd.", "alert.unexpected.return_home": "Wróć na stronę główną", "alert.unexpected.title": "O nie!", - "audio.close": "Zamknij dźwięk", - "audio.expand": "Rozwiń dźwięk", - "audio.hide": "Ukryj dźwięk", - "audio.mute": "Wycisz", - "audio.pause": "Pauzuj", - "audio.play": "Odtwórz", - "audio.unmute": "Cofnij wyciszenie", + "aliases.account.add": "Utwórz alias", + "aliases.account_label": "Stare konto:", + "aliases.aliases_list_delete": "Odłącz alias", + "aliases.search": "Szukaj swojego starego konta", + "aliases.success.add": "Pomyślnie utworzono alias konta", + "aliases.success.remove": "Pomyślnie usunięto alias konta", + "auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło", + "auth.logged_out": "Wylogowano.", "backups.actions.create": "Utwórz kopię zapasową", "backups.empty_message": "Nie znaleziono kopii zapasaowych. {action}", "backups.empty_message.action": "Chcesz utworzyć?", @@ -143,14 +143,20 @@ "column.admin.moderation_log": "Dziennik moderacyjny", "column.admin.reports": "Zgłoszenia", "column.admin.reports.menu.moderation_log": "Dziennik moderacji", + "column.aliases": "Aliasy kont", + "column.aliases.create_error": "Błąd tworzenia aliasu", + "column.aliases.delete": "Usuń", + "column.aliases.delete_error": "Błąd usuwania aliasu", + "column.aliases.subheading_add_new": "Dodaj nowy alias", + "column.aliases.subheading_aliases": "Istniejące aliasy", "column.backups": "Kopie zapasowe", "column.blocks": "Zablokowani użytkownicy", "column.bookmarks": "Załadki", "column.chats": "Rozmowy", "column.community": "Lokalna oś czasu", + "column.crypto_donate": "Przekaż kryptowalutę", "column.direct": "Wiadomości bezpośrednie", "column.domain_blocks": "Ukryte domeny", - "column.crypto_donate": "Przekaż kryptowalutę", "column.edit_profile": "Edytuj profil", "column.federation_restrictions": "Ograniczenia federacji", "column.filters": "Wyciszone słowa", @@ -195,6 +201,7 @@ "column_header.hide_settings": "Ukryj ustawienia", "column_header.show_settings": "Pokaż ustawienia", "community.column_settings.media_only": "Tylko zawartość multimedialna", + "compose.invalid_schedule": "Musisz zaplanować wpis przynajmniej 5 minut wcześniej.", "compose_form.direct_message_warning": "Ten wpis będzie widoczny tylko dla wszystkich wspomnianych użytkowników.", "compose_form.direct_message_warning_learn_more": "Dowiedz się więcej", "compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.", @@ -250,10 +257,11 @@ "confirmations.reply.message": "W ten sposób utracisz wpis który obecnie tworzysz. Czy na pewno chcesz to zrobić?", "confirmations.unfollow.confirm": "Przestań śledzić", "confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?", - "crypto_donate_panel.actions.more": "Naciśnij, aby zobaczyć {count} more {count, plural, one {portfel} few {portfele} many {portfeli} other {portfeli}}", "crypto_donate.explanation_box.message": "{siteTitle} przyjmuje darowizny w kryptowalutach. Możesz wysłać darowiznę na jeden z poniższych adresów. Dziękujemy za Wasze wsparcie!", - "crypto_donate_panel.intro.message": "{siteTitle} przyjmuje darowizny w kryptowalutach, aby utrzymać naszą usługę. Dziękujemy za Wasze wsparcie!", + "crypto_donate.explanation_box.title": "Przeaż darowiznę w kryptowalutach", + "crypto_donate_panel.actions.more": "Naciśnij, aby zobaczyć {count} more {count, plural, one {portfel} few {portfele} many {portfeli} other {portfeli}}", "crypto_donate_panel.heading": "Przekaż kryptowalutę", + "crypto_donate_panel.intro.message": "{siteTitle} przyjmuje darowizny w kryptowalutach, aby utrzymać naszą usługę. Dziękujemy za Wasze wsparcie!", "datepicker.hint": "Zaplanowano publikację na…", "donate": "Przekaż darowiznę", "donate_crypto": "Przekaż kryptowalutę", @@ -277,7 +285,7 @@ "edit_profile.fields.meta_fields_label": "Metadane profilu", "edit_profile.fields.stranger_notifications_label": "Blokuj powiadomienia od nieznajomych", "edit_profile.fields.verified_display_name": "Zweryfikowani użytkownicy nie mogą zmieniać nazwy wyświetlanej", - "edit_profile.hints.accepts_email_list_label": "Otrzymuj wiadomości i nowości marketingowe", + "edit_profile.hints.accepts_email_list": "Otrzymuj wiadomości i nowości marketingowe.", "edit_profile.hints.avatar": "PNG, GIF lub JPG. Maksymalnie 2 MB. Zostanie zmniejszony do 400x400px", "edit_profile.hints.bot": "To konto podejmuje głównie zautomatyzowane działania i może nie być nadzorowane", "edit_profile.hints.header": "PNG, GIF lub JPG. Maksymalnie 2 MB. Zostanie zmniejszony do 1500x500px", @@ -306,6 +314,8 @@ "emoji_button.travel": "Podróże i miejsca", "empty_column.account_timeline": "Brak wpisów tutaj!", "empty_column.account_unavailable": "Profil niedostępny", + "empty_column.aliases": "Nie utworzyłeś(-aś) jeszcze żadnego aliasu konta.", + "empty_column.aliases.suggestions": "Brak propozycji kont dla podanej frazy.", "empty_column.blocks": "Nie zablokowałeś(-aś) jeszcze żadnego użytkownika.", "empty_column.bookmarks": "Nie masz jeszcze żadnej zakładki. Kiedy dodasz jakąś, pojawi się ona tutaj.", "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!", @@ -334,11 +344,14 @@ "federation_restriction.full_media_removal": "Pełne usunięcie mediów", "federation_restriction.media_nsfw": "Załączniki oznaczone jako NSFW", "federation_restriction.partial_media_removal": "Częściowe usunięcie mediów", + "federation_restrictions.empty_message": "{siteTitle} nie nakłada restrykcji na żadne instancje.", "federation_restrictions.explanation_box.message": "Zwykle serwery w Fediwersum mogą swobodnie porozumiewać się. {siteTitle} nakłada pewne ograniczenia na następujące serwery.", "federation_restrictions.explanation_box.title": "Zasady dla poszczególnych instancji", + "federation_restrictions.not_disclosed_message": "{siteTitle} nie udostępnia informacji o ograniczeniach federacji przez API.", "fediverse_tab.explanation_box.dismiss": "Nie pokazuj ponownie", "fediverse_tab.explanation_box.explanation": "{site_title} jest częścią Fediwersum, sieci społecznościowej na którą składają się tysiące niezależnie funkcjonujących stron (serwerów). Wpisy które tu widzisz pochodzą z serwerów podmiotów trzecich. Możesz do woli wchodzić z nimi w interakcje lub blokować serwery których nie lubisz. Zwracaj uwagę na pełną nazwę użytkownika po znaku @, aby wiedzieć z jakiego serwera pochodzi on. Aby widzieć tylko wpisy z {site_title}, odwiedź kartę {local}.", "fediverse_tab.explanation_box.title": "Czym jest Fediverse?", + "filters.added": "Dodano filtr.", "filters.context_header": "Konteksty filtru", "filters.context_hint": "Jedno lub więcej miejsc, gdzie filtr powinien zostać zaaplikowany", "filters.filters_list_context_label": "Konteksty filtru:", @@ -348,12 +361,12 @@ "filters.filters_list_hide": "Ukrywaj", "filters.filters_list_phrase_label": "Słowo kluczowe lub fraza:", "filters.filters_list_whole-word": "Całe słowo", - "filters.added": "Dodano filtr.", - "filters.deleted": "Usunięto filtr.", + "filters.removed": "Usunięto filtr.", "follow_request.authorize": "Autoryzuj", "follow_request.reject": "Odrzuć", "forms.copy": "Kopiuj", "getting_started.open_source_notice": "{code_name} jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitLabie tutaj: {code_link} (v{code_version}).", + "group.detail.archived_group": "Zarchiwizowana grupa", "group.members.empty": "Ta grupa nie ma żadnych członków.", "group.removed_accounts.empty": "Ta grupa nie ma żadnych usuniętych kont.", "groups.card.join": "Dołącz", @@ -362,13 +375,21 @@ "groups.card.roles.member": "Jesteś członkiem", "groups.card.view": "Zobacz", "groups.create": "Utwórz grupę", + "groups.detail.role_admin": "Jesteś administratorem", + "groups.edit": "Edytuj", "groups.form.coverImage": "Wyślij obraz baneru (nieobowiązkowe)", "groups.form.coverImageChange": "Wybrano obraz baneru", "groups.form.create": "Utwórz grupę", "groups.form.description": "Opis", "groups.form.title": "Tytuł", "groups.form.update": "Aktualizuj grupę", + "groups.join": "Dołącz do grupy", + "groups.leave": "Opuść grupę", "groups.removed_accounts": "Usunięte konta", + "groups.sidebar-panel.item.no_recent_activity": "Brak ostatniej aktywności", + "groups.sidebar-panel.item.view": "nowe wpisy", + "groups.sidebar-panel.show_all": "Pokaż wszystkie", + "groups.sidebar-panel.title": "Grupy do których należysz", "groups.tab_admin": "Zarządzaj", "groups.tab_featured": "Wyróżnione", "groups.tab_member": "Członek", @@ -407,9 +428,7 @@ "keyboard_shortcuts.back": "aby cofnąć się", "keyboard_shortcuts.blocked": "aby przejść do listy zablokowanych użytkowników", "keyboard_shortcuts.boost": "aby podbić wpis", - "keyboard_shortcuts.column": "aby przejść do wpisu z jednej z kolumn", "keyboard_shortcuts.compose": "aby przejść do pola tworzenia wpisu", - "keyboard_shortcuts.direct": "aby otworzyć kolumnę wiadomości bezpośrednich", "keyboard_shortcuts.down": "aby przejść na dół listy", "keyboard_shortcuts.enter": "aby otworzyć wpis", "keyboard_shortcuts.favourite": "aby dodać do ulubionych", @@ -428,7 +447,6 @@ "keyboard_shortcuts.reply": "aby odpowiedzieć", "keyboard_shortcuts.requests": "aby przejść do listy próśb o możliwość śledzenia", "keyboard_shortcuts.search": "aby przejść do pola wyszukiwania", - "keyboard_shortcuts.start": "aby otworzyć kolumnę „Rozpocznij”", "keyboard_shortcuts.toggle_hidden": "aby wyświetlić lub ukryć wpis spod CW", "keyboard_shortcuts.toggle_sensitivity": "by pokazać/ukryć multimedia", "keyboard_shortcuts.toot": "aby utworzyć nowy wpis", @@ -484,8 +502,10 @@ "morefollows.followers_label": "…i {count} więcej {count, plural, one {obserwujący(-a)} few {obserwujących} many {obserwujących} other {obserwujących}} na zdalnych stronach.", "morefollows.following_label": "…i {count} więcej {count, plural, one {obserwowany(-a)} few {obserwowanych} many {obserwowanych} other {obserwowanych}} na zdalnych stronach.", "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?", + "navigation_bar.account_aliases": "Aliasy kont", "navigation_bar.admin_settings": "Ustawienia administracyjne", "navigation_bar.blocks": "Zablokowani użytkownicy", + "navigation_bar.bookmarks": "Zakładki", "navigation_bar.compose": "Utwórz nowy wpis", "navigation_bar.domain_blocks": "Ukryte domeny", "navigation_bar.favourites": "Ulubione", @@ -494,7 +514,9 @@ "navigation_bar.import_data": "Importuj dane", "navigation_bar.info": "Szczegółowe informacje", "navigation_bar.keyboard_shortcuts": "Skróty klawiszowe", + "navigation_bar.lists": "Lists", "navigation_bar.logout": "Wyloguj", + "navigation_bar.messages": "Messages", "navigation_bar.mutes": "Wyciszeni użytkownicy", "navigation_bar.pins": "Przypięte wpisy", "navigation_bar.preferences": "Preferencje", @@ -613,6 +635,7 @@ "relative_time.minutes": "{number} min.", "relative_time.seconds": "{number} s.", "remote_instance.edit_federation": "Edytuj federację", + "remote_instance.federation_panel.heading": "Federation Restrictions", "remote_instance.federation_panel.no_restrictions_message": "{siteTitle} nie nakłada ograniczeń na {host}.", "remote_instance.federation_panel.restricted_message": "{siteTitle} blokuje wszystkie aktywności z {host}.", "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} nakłada pewne ograniczenia na {host}.", @@ -641,8 +664,6 @@ "search_results.hashtags": "Hashtagi", "search_results.statuses": "Wpisy", "search_results.top": "Góra", - "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", - "search_results.total.has_more": "{count, number} Ponad {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "security.codes.fail": "Nie udało się uzyskać zapasowych kodów", "security.confirm.fail": "Nieprawidłowy kod lub hasło. Spróbuj ponownie.", "security.delete_account.fail": "Nie udało się usunąć konta.", @@ -788,7 +809,6 @@ "upload_error.limit": "Przekroczono limit plików do wysłania.", "upload_error.poll": "Dołączanie plików nie dozwolone z głosowaniami.", "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących", - "upload_form.focus": "Zmień podgląd", "upload_form.preview": "Podgląd", "upload_form.undo": "Usuń", "upload_progress.label": "Wysyłanie…", diff --git a/app/soapbox/reducers/aliases.js b/app/soapbox/reducers/aliases.js new file mode 100644 index 000000000..a4d4ecec8 --- /dev/null +++ b/app/soapbox/reducers/aliases.js @@ -0,0 +1,35 @@ +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { + ALIASES_SUGGESTIONS_READY, + ALIASES_SUGGESTIONS_CLEAR, + ALIASES_SUGGESTIONS_CHANGE, +} from '../actions/aliases'; + +const initialState = ImmutableMap({ + suggestions: ImmutableMap({ + value: '', + loaded: false, + items: ImmutableList(), + }), +}); + +export default function aliasesReducer(state = initialState, action) { + switch(action.type) { + case ALIASES_SUGGESTIONS_CHANGE: + return state + .setIn(['suggestions', 'value'], action.value) + .setIn(['suggestions', 'loaded'], false); + case ALIASES_SUGGESTIONS_READY: + return state + .setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id))) + .setIn(['suggestions', 'loaded'], true); + case ALIASES_SUGGESTIONS_CLEAR: + return state.update('suggestions', suggestions => suggestions.withMutations(map => { + map.set('items', ImmutableList()); + map.set('value', ''); + map.set('loaded', false); + })); + default: + return state; + } +}; diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index 751a04afb..cf62b4fdf 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -53,6 +53,7 @@ import backups from './backups'; import admin_log from './admin_log'; import security from './security'; import scheduled_statuses from './scheduled_statuses'; +import aliases from './aliases'; const appReducer = combineReducers({ dropdown_menu, @@ -107,6 +108,7 @@ const appReducer = combineReducers({ admin_log, security, scheduled_statuses, + aliases, }); // Clear the state (mostly) when the user logs out diff --git a/app/styles/application.scss b/app/styles/application.scss index d247b3a7b..09b2aa355 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -84,6 +84,7 @@ @import 'components/datepicker'; @import 'components/remote-timeline'; @import 'components/federation-restrictions'; +@import 'components/aliases'; // Holiday @import 'holiday/halloween'; diff --git a/app/styles/components/aliases.scss b/app/styles/components/aliases.scss new file mode 100644 index 000000000..a9888a85b --- /dev/null +++ b/app/styles/components/aliases.scss @@ -0,0 +1,107 @@ +.aliases { + &__accounts { + overflow-y: auto; + + .account__display-name { + &:hover strong { + text-decoration: none; + } + } + + .account__avatar { + cursor: default; + } + + &.empty-column-indicator { + min-height: unset; + overflow-y: unset; + } + } + + &_search { + display: flex; + flex-direction: row; + margin: 10px; + + .search__input { + padding: 7px 30px 6px 10px; + } + + > label { + flex: 1 1; + } + + > .search__icon .fa { + top: 8px; + right: 102px !important; + } + + > .button { + width: 80px; + margin-left: 10px; + } + } +} + +.aliases-settings-panel { + flex: 1; + + .item-list article { + border-bottom: 1px solid var(--primary-text-color--faint); + + &:last-child { + border-bottom: 0; + } + } + + .alias__container { + padding: 20px; + display: flex; + justify-content: space-between; + font-size: 14px; + + span.alias__list-label { + padding-right: 5px; + color: var(--primary-text-color--faint); + } + + span.alias__list-value span { + padding-right: 5px; + text-transform: capitalize; + + &::after { + content: ','; + } + + &:last-of-type { + &::after { + content: ''; + } + } + } + + .alias__delete { + display: flex; + align-items: baseline; + cursor: pointer; + + span.alias__delete-label { + color: var(--primary-text-color--faint); + font-size: 14px; + font-weight: 800; + } + + .alias__delete-icon { + background: none; + color: var(--primary-text-color--faint); + padding: 0 5px; + margin: 0 auto; + font-size: 16px; + } + } + } + + .slist--flex { + height: 100%; + } +}