WIP: Account aliases

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
groups^2
marcin mikołajczak 2021-08-05 15:31:29 +02:00
rodzic 29cdc4867b
commit f203a4d389
13 zmienionych plików z 527 dodań i 0 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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 {
<Icon id='cloud-upload' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
</NavLink>
<NavLink className='sidebar-menu-item' to='/settings/aliases' onClick={this.handleClose}>
<Icon id='suitcase' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.account_aliases)}</span>
</NavLink>
<NavLink className='sidebar-menu-item' to='/auth/edit' onClick={this.handleClose}>
<Icon id='lock' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.security)}</span>

Wyświetl plik

@ -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 = (
<div className='account__relationship'>
<IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={this.handleOnAdd} />
</div>
);
}
return (
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</div>
{button}
</div>
</div>
);
}
}

Wyświetl plik

@ -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 (
<div className='aliases_search search'>
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
<input
className='search__input'
type='text'
value={value}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
placeholder={intl.formatMessage(messages.search)}
/>
</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 })} />
</div>
<Button onClick={this.handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
</div>
);
}
}

Wyświetl plik

@ -0,0 +1,72 @@
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']),
});
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 } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.aliases' defaultMessage="You haven't created any account alias yet." />;
return (
<Column className='aliases-settings-panel' icon='suitcase' heading={intl.formatMessage(messages.heading)} backBtnSlim>
<ColumnSubheading text={intl.formatMessage(messages.subheading_add_new)} />
<Search />
<div className='aliases__accounts'>
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
</div>
<ColumnSubheading text={intl.formatMessage(messages.subheading_aliases)} />
<div className='aliases-settings-panel'>
<ScrollableList
scrollKey='aliases'
emptyMessage={emptyMessage}
>
{aliases.map((alias, i) => (
<div key={i} className='alias__container'>
<div className='alias__details'>
<span className='alias__list-label'><FormattedMessage id='aliases.account_label' defaultMessage='Old account:' /></span>
<span className='alias__list-value'>{alias}</span>
</div>
<div className='alias__delete' role='button' tabIndex='0' onClick={this.handleFilterDelete} data-value={alias} aria-label={intl.formatMessage(messages.delete)}>
<Icon className='alias__delete-icon' id='times' size={40} />
<span className='alias__delete-label'><FormattedMessage id='aliases.aliases_list_delete' defaultMessage='Unlink alias' /></span>
</div>
</div>
))}
</ScrollableList>
</div>
</Column>
);
}
}

Wyświetl plik

@ -38,6 +38,7 @@ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => (
{isAdmin(account) && <li><a href='/pleroma/admin'><FormattedMessage id='navigation_bar.admin_settings' defaultMessage='AdminFE' /></a></li>}
{isAdmin(account) && <li><Link to='/soapbox/config'><FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' /></Link></li>}
<li><Link to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></Link></li>
<li><Link to='/settings/aliases'><FormattedMessage id='navigation_bar.account_aliases' defaultMessage='Account aliases' /></Link></li>
<li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li>
</>}
<li><Link to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></Link></li>

Wyświetl plik

@ -98,6 +98,7 @@ import {
ScheduledStatuses,
UserIndex,
FederationRestrictions,
Aliases,
} from './util/async-components';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
@ -261,6 +262,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/settings/preferences' page={DefaultPage} component={Preferences} content={children} />
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
<WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />
<WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />
<WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} />
<WrappedRoute path='/soapbox/config' page={DefaultPage} component={SoapboxConfig} content={children} />

Wyświetl plik

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

Wyświetl plik

@ -110,6 +110,12 @@
"alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
"alert.unexpected.return_home": "Wróć na stronę główną",
"alert.unexpected.title": "O nie!",
"aliases.account_label": "Stare konto:",
"aliases.account.add": "Utwórz alias",
"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",
"audio.close": "Zamknij dźwięk",
"audio.expand": "Rozwiń dźwięk",
"audio.hide": "Ukryj dźwięk",
@ -143,6 +149,12 @@
"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",
@ -305,6 +317,7 @@
"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.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ć!",
@ -480,6 +493,7 @@
"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.compose": "Utwórz nowy wpis",

Wyświetl plik

@ -0,0 +1,29 @@
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: '',
items: ImmutableList(),
}),
});
export default function aliasesReducer(state = initialState, action) {
switch(action.type) {
case ALIASES_SUGGESTIONS_CHANGE:
return state.setIn(['suggestions', 'value'], action.value);
case ALIASES_SUGGESTIONS_READY:
return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
case ALIASES_SUGGESTIONS_CLEAR:
return state.update('suggestions', suggestions => suggestions.withMutations(map => {
map.set('items', ImmutableList());
map.set('value', '');
}));
default:
return state;
}
};

Wyświetl plik

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

Wyświetl plik

@ -84,6 +84,7 @@
@import 'components/datepicker';
@import 'components/remote-timeline';
@import 'components/federation-restrictions';
@import 'components/aliases';
// Holiday
@import 'holiday/halloween';

Wyświetl plik

@ -0,0 +1,102 @@
.aliases {
&__accounts {
overflow-y: auto;
.account__display-name {
&:hover strong {
text-decoration: none;
}
}
.account__avatar {
cursor: default;
}
}
&_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%;
}
}