mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-02-03 13:33:32 -05:00
[Glitch] Change design of lists in web UI
Port 62603508c7343f0b9ef880bcbed67c70a9e8375d to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
2259949e9b
commit
217e0f87fd
@ -1,8 +1,5 @@
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { showAlertForError } from './alerts';
|
|
||||||
import { importFetchedAccounts } from './importer';
|
|
||||||
|
|
||||||
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||||
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
||||||
@ -11,45 +8,10 @@ export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
|
|||||||
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
|
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
|
||||||
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
|
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
|
||||||
|
|
||||||
export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
|
|
||||||
export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
|
|
||||||
export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
|
|
||||||
|
|
||||||
export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
|
|
||||||
export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
|
|
||||||
export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
|
|
||||||
|
|
||||||
export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
|
|
||||||
export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
|
|
||||||
export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
|
|
||||||
|
|
||||||
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
||||||
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
||||||
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
||||||
|
|
||||||
export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
|
|
||||||
export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
|
|
||||||
export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
|
|
||||||
export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
|
|
||||||
export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
|
|
||||||
|
|
||||||
export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
|
|
||||||
export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
|
|
||||||
export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
|
|
||||||
|
|
||||||
export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
|
|
||||||
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
|
|
||||||
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
|
|
||||||
|
|
||||||
export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
|
|
||||||
export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
|
|
||||||
|
|
||||||
export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
|
|
||||||
export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
|
|
||||||
export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const fetchList = id => (dispatch, getState) => {
|
export const fetchList = id => (dispatch, getState) => {
|
||||||
if (getState().getIn(['lists', id])) {
|
if (getState().getIn(['lists', id])) {
|
||||||
return;
|
return;
|
||||||
@ -100,89 +62,6 @@ export const fetchListsFail = error => ({
|
|||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const submitListEditor = shouldReset => (dispatch, getState) => {
|
|
||||||
const listId = getState().getIn(['listEditor', 'listId']);
|
|
||||||
const title = getState().getIn(['listEditor', 'title']);
|
|
||||||
|
|
||||||
if (listId === null) {
|
|
||||||
dispatch(createList(title, shouldReset));
|
|
||||||
} else {
|
|
||||||
dispatch(updateList(listId, title, shouldReset));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupListEditor = listId => (dispatch, getState) => {
|
|
||||||
dispatch({
|
|
||||||
type: LIST_EDITOR_SETUP,
|
|
||||||
list: getState().getIn(['lists', listId]),
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(fetchListAccounts(listId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const changeListEditorTitle = value => ({
|
|
||||||
type: LIST_EDITOR_TITLE_CHANGE,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createList = (title, shouldReset) => (dispatch) => {
|
|
||||||
dispatch(createListRequest());
|
|
||||||
|
|
||||||
api().post('/api/v1/lists', { title }).then(({ data }) => {
|
|
||||||
dispatch(createListSuccess(data));
|
|
||||||
|
|
||||||
if (shouldReset) {
|
|
||||||
dispatch(resetListEditor());
|
|
||||||
}
|
|
||||||
}).catch(err => dispatch(createListFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createListRequest = () => ({
|
|
||||||
type: LIST_CREATE_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createListSuccess = list => ({
|
|
||||||
type: LIST_CREATE_SUCCESS,
|
|
||||||
list,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createListFail = error => ({
|
|
||||||
type: LIST_CREATE_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch) => {
|
|
||||||
dispatch(updateListRequest(id));
|
|
||||||
|
|
||||||
api().put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
|
||||||
dispatch(updateListSuccess(data));
|
|
||||||
|
|
||||||
if (shouldReset) {
|
|
||||||
dispatch(resetListEditor());
|
|
||||||
}
|
|
||||||
}).catch(err => dispatch(updateListFail(id, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateListRequest = id => ({
|
|
||||||
type: LIST_UPDATE_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateListSuccess = list => ({
|
|
||||||
type: LIST_UPDATE_SUCCESS,
|
|
||||||
list,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateListFail = (id, error) => ({
|
|
||||||
type: LIST_UPDATE_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const resetListEditor = () => ({
|
|
||||||
type: LIST_EDITOR_RESET,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteList = id => (dispatch) => {
|
export const deleteList = id => (dispatch) => {
|
||||||
dispatch(deleteListRequest(id));
|
dispatch(deleteListRequest(id));
|
||||||
|
|
||||||
@ -206,167 +85,3 @@ export const deleteListFail = (id, error) => ({
|
|||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchListAccounts = listId => (dispatch) => {
|
|
||||||
dispatch(fetchListAccountsRequest(listId));
|
|
||||||
|
|
||||||
api().get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
|
||||||
dispatch(importFetchedAccounts(data));
|
|
||||||
dispatch(fetchListAccountsSuccess(listId, data));
|
|
||||||
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchListAccountsRequest = id => ({
|
|
||||||
type: LIST_ACCOUNTS_FETCH_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchListAccountsSuccess = (id, accounts, next) => ({
|
|
||||||
type: LIST_ACCOUNTS_FETCH_SUCCESS,
|
|
||||||
id,
|
|
||||||
accounts,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchListAccountsFail = (id, error) => ({
|
|
||||||
type: LIST_ACCOUNTS_FETCH_FAIL,
|
|
||||||
id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchListSuggestions = q => (dispatch) => {
|
|
||||||
const params = {
|
|
||||||
q,
|
|
||||||
resolve: false,
|
|
||||||
limit: 4,
|
|
||||||
following: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
api().get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
|
||||||
dispatch(importFetchedAccounts(data));
|
|
||||||
dispatch(fetchListSuggestionsReady(q, data));
|
|
||||||
}).catch(error => dispatch(showAlertForError(error)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
|
||||||
type: LIST_EDITOR_SUGGESTIONS_READY,
|
|
||||||
query,
|
|
||||||
accounts,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clearListSuggestions = () => ({
|
|
||||||
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const changeListSuggestions = value => ({
|
|
||||||
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addToListEditor = accountId => (dispatch, getState) => {
|
|
||||||
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addToList = (listId, accountId) => (dispatch) => {
|
|
||||||
dispatch(addToListRequest(listId, accountId));
|
|
||||||
|
|
||||||
api().post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
|
|
||||||
.then(() => dispatch(addToListSuccess(listId, accountId)))
|
|
||||||
.catch(err => dispatch(addToListFail(listId, accountId, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addToListRequest = (listId, accountId) => ({
|
|
||||||
type: LIST_EDITOR_ADD_REQUEST,
|
|
||||||
listId,
|
|
||||||
accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addToListSuccess = (listId, accountId) => ({
|
|
||||||
type: LIST_EDITOR_ADD_SUCCESS,
|
|
||||||
listId,
|
|
||||||
accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addToListFail = (listId, accountId, error) => ({
|
|
||||||
type: LIST_EDITOR_ADD_FAIL,
|
|
||||||
listId,
|
|
||||||
accountId,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeFromListEditor = accountId => (dispatch, getState) => {
|
|
||||||
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeFromList = (listId, accountId) => (dispatch) => {
|
|
||||||
dispatch(removeFromListRequest(listId, accountId));
|
|
||||||
|
|
||||||
api().delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
|
|
||||||
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
|
|
||||||
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeFromListRequest = (listId, accountId) => ({
|
|
||||||
type: LIST_EDITOR_REMOVE_REQUEST,
|
|
||||||
listId,
|
|
||||||
accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeFromListSuccess = (listId, accountId) => ({
|
|
||||||
type: LIST_EDITOR_REMOVE_SUCCESS,
|
|
||||||
listId,
|
|
||||||
accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeFromListFail = (listId, accountId, error) => ({
|
|
||||||
type: LIST_EDITOR_REMOVE_FAIL,
|
|
||||||
listId,
|
|
||||||
accountId,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const resetListAdder = () => ({
|
|
||||||
type: LIST_ADDER_RESET,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setupListAdder = accountId => (dispatch, getState) => {
|
|
||||||
dispatch({
|
|
||||||
type: LIST_ADDER_SETUP,
|
|
||||||
account: getState().getIn(['accounts', accountId]),
|
|
||||||
});
|
|
||||||
dispatch(fetchLists());
|
|
||||||
dispatch(fetchAccountLists(accountId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAccountLists = accountId => (dispatch) => {
|
|
||||||
dispatch(fetchAccountListsRequest(accountId));
|
|
||||||
|
|
||||||
api().get(`/api/v1/accounts/${accountId}/lists`)
|
|
||||||
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
|
|
||||||
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAccountListsRequest = id => ({
|
|
||||||
type:LIST_ADDER_LISTS_FETCH_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAccountListsSuccess = (id, lists) => ({
|
|
||||||
type: LIST_ADDER_LISTS_FETCH_SUCCESS,
|
|
||||||
id,
|
|
||||||
lists,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAccountListsFail = (id, err) => ({
|
|
||||||
type: LIST_ADDER_LISTS_FETCH_FAIL,
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addToListAdder = listId => (dispatch, getState) => {
|
|
||||||
dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeFromListAdder = listId => (dispatch, getState) => {
|
|
||||||
dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
|
|
||||||
};
|
|
||||||
|
13
app/javascript/flavours/glitch/actions/lists_typed.ts
Normal file
13
app/javascript/flavours/glitch/actions/lists_typed.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { apiCreate, apiUpdate } from 'flavours/glitch/api/lists';
|
||||||
|
import type { List } from 'flavours/glitch/models/list';
|
||||||
|
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
|
||||||
|
|
||||||
|
export const createList = createDataLoadingThunk(
|
||||||
|
'list/create',
|
||||||
|
(list: Partial<List>) => apiCreate(list),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateList = createDataLoadingThunk(
|
||||||
|
'list/update',
|
||||||
|
(list: Partial<List>) => apiUpdate(list),
|
||||||
|
);
|
@ -68,6 +68,7 @@ export async function apiRequest<ApiResponse = unknown>(
|
|||||||
method: Method,
|
method: Method,
|
||||||
url: string,
|
url: string,
|
||||||
args: {
|
args: {
|
||||||
|
signal?: AbortSignal;
|
||||||
params?: RequestParamsOrData;
|
params?: RequestParamsOrData;
|
||||||
data?: RequestParamsOrData;
|
data?: RequestParamsOrData;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
32
app/javascript/flavours/glitch/api/lists.ts
Normal file
32
app/javascript/flavours/glitch/api/lists.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
apiRequestPost,
|
||||||
|
apiRequestPut,
|
||||||
|
apiRequestGet,
|
||||||
|
apiRequestDelete,
|
||||||
|
} from 'flavours/glitch/api';
|
||||||
|
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
|
||||||
|
import type { ApiListJSON } from 'flavours/glitch/api_types/lists';
|
||||||
|
|
||||||
|
export const apiCreate = (list: Partial<ApiListJSON>) =>
|
||||||
|
apiRequestPost<ApiListJSON>('v1/lists', list);
|
||||||
|
|
||||||
|
export const apiUpdate = (list: Partial<ApiListJSON>) =>
|
||||||
|
apiRequestPut<ApiListJSON>(`v1/lists/${list.id}`, list);
|
||||||
|
|
||||||
|
export const apiGetAccounts = (listId: string) =>
|
||||||
|
apiRequestGet<ApiAccountJSON[]>(`v1/lists/${listId}/accounts`, {
|
||||||
|
limit: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiGetAccountLists = (accountId: string) =>
|
||||||
|
apiRequestGet<ApiListJSON[]>(`v1/accounts/${accountId}/lists`);
|
||||||
|
|
||||||
|
export const apiAddAccountToList = (listId: string, accountId: string) =>
|
||||||
|
apiRequestPost(`v1/lists/${listId}/accounts`, {
|
||||||
|
account_ids: [accountId],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiRemoveAccountFromList = (listId: string, accountId: string) =>
|
||||||
|
apiRequestDelete(`v1/lists/${listId}/accounts`, {
|
||||||
|
account_ids: [accountId],
|
||||||
|
});
|
10
app/javascript/flavours/glitch/api_types/lists.ts
Normal file
10
app/javascript/flavours/glitch/api_types/lists.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// See app/serializers/rest/list_serializer.rb
|
||||||
|
|
||||||
|
export type RepliesPolicyType = 'list' | 'followed' | 'none';
|
||||||
|
|
||||||
|
export interface ApiListJSON {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
exclusive: boolean;
|
||||||
|
replies_policy: RepliesPolicyType;
|
||||||
|
}
|
@ -7,11 +7,11 @@ import { Icon } from './icon';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
checked: boolean;
|
checked?: boolean;
|
||||||
indeterminate: boolean;
|
indeterminate?: boolean;
|
||||||
name: string;
|
name?: string;
|
||||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
label: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CheckBox: React.FC<Props> = ({
|
export const CheckBox: React.FC<Props> = ({
|
||||||
@ -30,6 +30,7 @@ export const CheckBox: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
readOnly={!onChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
@ -42,7 +43,7 @@ export const CheckBox: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>{label}</span>
|
{label && <span>{label}</span>}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -80,6 +80,7 @@ class ScrollableList extends PureComponent {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
bindToDocument: PropTypes.bool,
|
bindToDocument: PropTypes.bool,
|
||||||
preventScroll: PropTypes.bool,
|
preventScroll: PropTypes.bool,
|
||||||
|
footer: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -324,7 +325,7 @@ class ScrollableList extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
|
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
const childrenCount = Children.count(children);
|
const childrenCount = Children.count(children);
|
||||||
|
|
||||||
@ -342,11 +343,13 @@ class ScrollableList extends PureComponent {
|
|||||||
<div className='scrollable__append'>
|
<div className='scrollable__append'>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||||
<div role='feed' className='item-list'>
|
<div role='feed' className='item-list'>
|
||||||
{prepend}
|
{prepend}
|
||||||
|
|
||||||
@ -375,6 +378,8 @@ class ScrollableList extends PureComponent {
|
|||||||
|
|
||||||
{!hasMore && append}
|
{!hasMore && append}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -385,6 +390,8 @@ class ScrollableList extends PureComponent {
|
|||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,13 @@ const mapStateToProps = state => ({
|
|||||||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} dispatch
|
||||||
|
* @param {Object} root0
|
||||||
|
* @param {any} [root0.status]
|
||||||
|
* @param {any} root0.items
|
||||||
|
* @param {any} [root0.scrollKey]
|
||||||
|
*/
|
||||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||||
onOpen(id, onItemClick, keyboard) {
|
onOpen(id, onItemClick, keyboard) {
|
||||||
dispatch(isUserTouching() ? openModal({
|
dispatch(isUserTouching() ? openModal({
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import { injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Avatar } from '../../../components/avatar';
|
|
||||||
import { DisplayName } from '../../../components/display_name';
|
|
||||||
import { makeGetAccount } from '../../../selectors';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { accountId }) => ({
|
|
||||||
account: getAccount(state, accountId),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Account extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account: ImmutablePropTypes.record.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account } = this.props;
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
|
@ -1,75 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
|
|
||||||
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
|
|
||||||
import { IconButton } from '../../../components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
|
||||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const MapStateToProps = (state, { listId, added }) => ({
|
|
||||||
list: state.get('lists').get(listId),
|
|
||||||
added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { listId }) => ({
|
|
||||||
onRemove: () => dispatch(removeFromListAdder(listId)),
|
|
||||||
onAdd: () => dispatch(addToListAdder(listId)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class List extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
list: ImmutablePropTypes.map.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onRemove: PropTypes.func.isRequired,
|
|
||||||
onAdd: PropTypes.func.isRequired,
|
|
||||||
added: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
added: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { list, intl, onRemove, onAdd, added } = this.props;
|
|
||||||
|
|
||||||
let button;
|
|
||||||
|
|
||||||
if (added) {
|
|
||||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
|
||||||
} else {
|
|
||||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='list'>
|
|
||||||
<div className='list__wrapper'>
|
|
||||||
<div className='list__display-name'>
|
|
||||||
<Icon id='list-ul' icon={ListAltIcon} className='column-link__icon' />
|
|
||||||
{list.get('title')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__relationship'>
|
|
||||||
{button}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(List));
|
|
@ -1,76 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
|
||||||
import NewListForm from '../lists/components/new_list_form';
|
|
||||||
|
|
||||||
import Account from './components/account';
|
|
||||||
import List from './components/list';
|
|
||||||
// hack
|
|
||||||
|
|
||||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
|
||||||
if (!lists) {
|
|
||||||
return lists;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
listIds: getOrderedLists(state).map(list=>list.get('id')),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onInitialize: accountId => dispatch(setupListAdder(accountId)),
|
|
||||||
onReset: () => dispatch(resetListAdder()),
|
|
||||||
});
|
|
||||||
|
|
||||||
class ListAdder extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
accountId: PropTypes.string.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onInitialize: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
listIds: ImmutablePropTypes.list.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { onInitialize, accountId } = this.props;
|
|
||||||
onInitialize(accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
const { onReset } = this.props;
|
|
||||||
onReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { accountId, listIds } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='modal-root__modal list-adder'>
|
|
||||||
<div className='list-adder__account'>
|
|
||||||
<Account accountId={accountId} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NewListForm />
|
|
||||||
|
|
||||||
|
|
||||||
<div className='list-adder__lists'>
|
|
||||||
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListAdder));
|
|
213
app/javascript/flavours/glitch/features/list_adder/index.tsx
Normal file
213
app/javascript/flavours/glitch/features/list_adder/index.tsx
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||||
|
import { createList } from 'flavours/glitch/actions/lists_typed';
|
||||||
|
import {
|
||||||
|
apiGetAccountLists,
|
||||||
|
apiAddAccountToList,
|
||||||
|
apiRemoveAccountFromList,
|
||||||
|
} from 'flavours/glitch/api/lists';
|
||||||
|
import type { ApiListJSON } from 'flavours/glitch/api_types/lists';
|
||||||
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
|
import { CheckBox } from 'flavours/glitch/components/check_box';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
import { getOrderedLists } from 'flavours/glitch/selectors/lists';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
newList: {
|
||||||
|
id: 'lists.new_list_name',
|
||||||
|
defaultMessage: 'New list name',
|
||||||
|
},
|
||||||
|
createList: {
|
||||||
|
id: 'lists.create',
|
||||||
|
defaultMessage: 'Create',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
id: 'lightbox.close',
|
||||||
|
defaultMessage: 'Close',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListItem: React.FC<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (id: string, checked: boolean) => void;
|
||||||
|
}> = ({ id, title, checked, onChange }) => {
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(id, e.target.checked);
|
||||||
|
},
|
||||||
|
[id, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||||
|
<label className='lists__item'>
|
||||||
|
<div className='lists__item__title'>
|
||||||
|
<Icon id='list-ul' icon={ListAltIcon} />
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewListItem: React.FC<{
|
||||||
|
onCreate: (list: ApiListJSON) => void;
|
||||||
|
}> = ({ onCreate }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTitle(value);
|
||||||
|
},
|
||||||
|
[setTitle],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (title.trim().length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch(createList({ title })).then((result) => {
|
||||||
|
if (isFulfilled(result)) {
|
||||||
|
onCreate(result.payload);
|
||||||
|
setTitle('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}, [setTitle, dispatch, onCreate, title]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className='lists__item' onSubmit={handleSubmit}>
|
||||||
|
<label className='lists__item__title'>
|
||||||
|
<Icon id='list-ul' icon={ListAltIcon} />
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={title}
|
||||||
|
onChange={handleChange}
|
||||||
|
maxLength={30}
|
||||||
|
required
|
||||||
|
placeholder={intl.formatMessage(messages.newList)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Button text={intl.formatMessage(messages.createList)} type='submit' />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListAdder: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}> = ({ accountId, onClose }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||||
|
const [listIds, setListIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchLists());
|
||||||
|
|
||||||
|
apiGetAccountLists(accountId)
|
||||||
|
.then((data) => {
|
||||||
|
setListIds(data.map((l) => l.id));
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Nothing
|
||||||
|
});
|
||||||
|
}, [dispatch, setListIds, accountId]);
|
||||||
|
|
||||||
|
const handleToggle = useCallback(
|
||||||
|
(listId: string, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||||
|
|
||||||
|
apiAddAccountToList(listId, accountId).catch(() => {
|
||||||
|
setListIds((currentListIds) =>
|
||||||
|
currentListIds.filter((id) => id !== listId),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setListIds((currentListIds) =>
|
||||||
|
currentListIds.filter((id) => id !== listId),
|
||||||
|
);
|
||||||
|
|
||||||
|
apiRemoveAccountFromList(listId, accountId).catch(() => {
|
||||||
|
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setListIds, accountId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreate = useCallback(
|
||||||
|
(list: ApiListJSON) => {
|
||||||
|
setListIds((currentListIds) => [list.id, ...currentListIds]);
|
||||||
|
|
||||||
|
apiAddAccountToList(list.id, accountId).catch(() => {
|
||||||
|
setListIds((currentListIds) =>
|
||||||
|
currentListIds.filter((id) => id !== list.id),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setListIds, accountId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal dialog-modal'>
|
||||||
|
<div className='dialog-modal__header'>
|
||||||
|
<IconButton
|
||||||
|
className='dialog-modal__header__close'
|
||||||
|
title={intl.formatMessage(messages.close)}
|
||||||
|
icon='times'
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className='dialog-modal__header__title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.add_to_lists'
|
||||||
|
defaultMessage='Add {name} to lists'
|
||||||
|
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dialog-modal__content'>
|
||||||
|
<div className='lists-scrollable'>
|
||||||
|
<NewListItem onCreate={handleCreate} />
|
||||||
|
|
||||||
|
{lists.map((list) => (
|
||||||
|
<ListItem
|
||||||
|
key={list.id}
|
||||||
|
id={list.id}
|
||||||
|
title={list.title}
|
||||||
|
checked={listIds.includes(list.id)}
|
||||||
|
onChange={handleToggle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default ListAdder;
|
@ -1,82 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|
||||||
|
|
||||||
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
|
|
||||||
import { Avatar } from '../../../components/avatar';
|
|
||||||
import { DisplayName } from '../../../components/display_name';
|
|
||||||
import { IconButton } from '../../../components/icon_button';
|
|
||||||
import { makeGetAccount } from '../../../selectors';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
|
||||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { accountId, added }) => ({
|
|
||||||
account: getAccount(state, accountId),
|
|
||||||
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
|
||||||
onRemove: () => dispatch(removeFromListEditor(accountId)),
|
|
||||||
onAdd: () => dispatch(addToListEditor(accountId)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class Account extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account: ImmutablePropTypes.record.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onRemove: PropTypes.func.isRequired,
|
|
||||||
onAdd: PropTypes.func.isRequired,
|
|
||||||
added: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
added: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account, intl, onRemove, onAdd, added } = this.props;
|
|
||||||
|
|
||||||
let button;
|
|
||||||
|
|
||||||
if (added) {
|
|
||||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
|
||||||
} else {
|
|
||||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
<div className='account__relationship'>
|
|
||||||
{button}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
|
|
@ -1,76 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|
||||||
|
|
||||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
|
||||||
import { IconButton } from '../../../components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
value: state.getIn(['listEditor', 'title']),
|
|
||||||
disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
|
||||||
onSubmit: () => dispatch(submitListEditor(false)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class ListForm extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange = e => {
|
|
||||||
this.props.onChange(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubmit = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
this.props.onSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value, disabled, intl } = this.props;
|
|
||||||
|
|
||||||
const title = intl.formatMessage(messages.title);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
|
||||||
<input
|
|
||||||
className='setting-text'
|
|
||||||
value={value}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
disabled={disabled}
|
|
||||||
icon='check'
|
|
||||||
iconComponent={CheckIcon}
|
|
||||||
title={title}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListForm));
|
|
@ -1,83 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
|
|
||||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
value: state.getIn(['listEditor', 'suggestions', 'value']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onSubmit: value => dispatch(fetchListSuggestions(value)),
|
|
||||||
onClear: () => dispatch(clearListSuggestions()),
|
|
||||||
onChange: value => dispatch(changeListSuggestions(value)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class Search extends 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClear = () => {
|
|
||||||
this.props.onClear();
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value, intl } = this.props;
|
|
||||||
const hasValue = value.length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='list-editor__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' icon={SearchIcon} className={classNames({ active: !hasValue })} />
|
|
||||||
<Icon id='times-circle' icon={CancelIcon} aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
|
|
@ -1,83 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
|
|
||||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
|
||||||
import Motion from '../ui/util/optional_motion';
|
|
||||||
|
|
||||||
import Account from './components/account';
|
|
||||||
import EditListForm from './components/edit_list_form';
|
|
||||||
import Search from './components/search';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
|
||||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onInitialize: listId => dispatch(setupListEditor(listId)),
|
|
||||||
onClear: () => dispatch(clearListSuggestions()),
|
|
||||||
onReset: () => dispatch(resetListEditor()),
|
|
||||||
});
|
|
||||||
|
|
||||||
class ListEditor extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
listId: PropTypes.string.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onInitialize: PropTypes.func.isRequired,
|
|
||||||
onClear: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
accountIds: ImmutablePropTypes.list.isRequired,
|
|
||||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { onInitialize, listId } = this.props;
|
|
||||||
onInitialize(listId);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
const { onReset } = this.props;
|
|
||||||
onReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { accountIds, searchAccountIds, onClear } = this.props;
|
|
||||||
const showSearch = searchAccountIds.size > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='modal-root__modal list-editor'>
|
|
||||||
<EditListForm />
|
|
||||||
|
|
||||||
<Search />
|
|
||||||
|
|
||||||
<div className='drawer__pager'>
|
|
||||||
<div className='drawer__inner list-editor__accounts'>
|
|
||||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
|
|
||||||
|
|
||||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
|
||||||
{({ x }) => (
|
|
||||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
|
||||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListEditor));
|
|
@ -1,21 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
|
|
||||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
import { fetchList, updateList } from 'flavours/glitch/actions/lists';
|
import { fetchList } from 'flavours/glitch/actions/lists';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { connectListStream } from 'flavours/glitch/actions/streaming';
|
import { connectListStream } from 'flavours/glitch/actions/streaming';
|
||||||
import { expandListTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandListTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
@ -23,17 +21,10 @@ import Column from 'flavours/glitch/components/column';
|
|||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
import { RadioButton } from 'flavours/glitch/components/radio_button';
|
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
|
|
||||||
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
|
|
||||||
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
list: state.getIn(['lists', props.params.id]),
|
list: state.getIn(['lists', props.params.id]),
|
||||||
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
||||||
@ -115,13 +106,6 @@ class ListTimeline extends PureComponent {
|
|||||||
this.props.dispatch(expandListTimeline(id, { maxId }));
|
this.props.dispatch(expandListTimeline(id, { maxId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEditClick = () => {
|
|
||||||
this.props.dispatch(openModal({
|
|
||||||
modalType: 'LIST_EDITOR',
|
|
||||||
modalProps: { listId: this.props.params.id },
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
const { dispatch, columnId } = this.props;
|
const { dispatch, columnId } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
@ -129,25 +113,11 @@ class ListTimeline extends PureComponent {
|
|||||||
dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
|
dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRepliesPolicyChange = ({ target }) => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const { id } = this.props.params;
|
|
||||||
dispatch(updateList(id, undefined, false, undefined, target.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onExclusiveToggle = ({ target }) => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const { id } = this.props.params;
|
|
||||||
dispatch(updateList(id, undefined, false, target.checked, undefined));
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hasUnread, columnId, multiColumn, list, intl } = this.props;
|
const { hasUnread, columnId, multiColumn, list } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const title = list ? list.get('title') : id;
|
const title = list ? list.get('title') : id;
|
||||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
|
||||||
const isExclusive = list ? list.get('exclusive') : undefined;
|
|
||||||
|
|
||||||
if (typeof list === 'undefined') {
|
if (typeof list === 'undefined') {
|
||||||
return (
|
return (
|
||||||
@ -178,35 +148,14 @@ class ListTimeline extends PureComponent {
|
|||||||
>
|
>
|
||||||
<div className='column-settings'>
|
<div className='column-settings'>
|
||||||
<section className='column-header__links'>
|
<section className='column-header__links'>
|
||||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
<Link to={`/lists/${id}/edit`} className='text-btn column-header__setting-btn'>
|
||||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||||
</button>
|
</Link>
|
||||||
|
|
||||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
|
||||||
<div className='setting-toggle'>
|
|
||||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
|
||||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
|
||||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{replies_policy !== undefined && (
|
|
||||||
<section aria-labelledby={`list-${id}-replies-policy`}>
|
|
||||||
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
|
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
|
||||||
{ ['none', 'list', 'followed'].map(policy => (
|
|
||||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
@ -229,4 +178,4 @@ class ListTimeline extends PureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps)(injectIntl(ListTimeline)));
|
export default withRouter(connect(mapStateToProps)(ListTimeline));
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { changeListEditorTitle, submitListEditor } from 'flavours/glitch/actions/lists';
|
|
||||||
import { Button } from 'flavours/glitch/components/button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
|
|
||||||
title: { id: 'lists.new.create', defaultMessage: 'Add list' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
value: state.getIn(['listEditor', 'title']),
|
|
||||||
disabled: state.getIn(['listEditor', 'isSubmitting']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
|
||||||
onSubmit: () => dispatch(submitListEditor(true)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class NewListForm extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange = e => {
|
|
||||||
this.props.onChange(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubmit = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
this.props.onSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value, disabled, intl } = this.props;
|
|
||||||
|
|
||||||
const label = intl.formatMessage(messages.label);
|
|
||||||
const title = intl.formatMessage(messages.title);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
|
||||||
<label>
|
|
||||||
<span style={{ display: 'none' }}>{label}</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className='setting-text'
|
|
||||||
value={value}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
placeholder={label}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
disabled={disabled || !value}
|
|
||||||
text={title}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewListForm));
|
|
@ -1,91 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
|
||||||
import { fetchLists } from 'flavours/glitch/actions/lists';
|
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
|
||||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
|
||||||
import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
|
|
||||||
import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading';
|
|
||||||
|
|
||||||
import NewListForm from './components/new_list_form';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
|
||||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
|
||||||
if (!lists) {
|
|
||||||
return lists;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
lists: getOrderedLists(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
class Lists extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
params: PropTypes.object.isRequired,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
lists: ImmutablePropTypes.list,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
UNSAFE_componentWillMount () {
|
|
||||||
this.props.dispatch(fetchLists());
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl, lists, multiColumn } = this.props;
|
|
||||||
|
|
||||||
if (!lists) {
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column bindToDocument={!multiColumn} icon='bars' iconComponent={ListAltIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton>
|
|
||||||
<NewListForm />
|
|
||||||
|
|
||||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
|
||||||
<ScrollableList
|
|
||||||
scrollKey='lists'
|
|
||||||
emptyMessage={emptyMessage}
|
|
||||||
bindToDocument={!multiColumn}
|
|
||||||
>
|
|
||||||
{lists.map(list =>
|
|
||||||
<ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' iconComponent={ListAltIcon} text={list.get('title')} />,
|
|
||||||
)}
|
|
||||||
</ScrollableList>
|
|
||||||
|
|
||||||
<Helmet>
|
|
||||||
<title>{intl.formatMessage(messages.heading)}</title>
|
|
||||||
<meta name='robots' content='noindex' />
|
|
||||||
</Helmet>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(injectIntl(Lists));
|
|
145
app/javascript/flavours/glitch/features/lists/index.tsx
Normal file
145
app/javascript/flavours/glitch/features/lists/index.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { useEffect, useMemo, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||||
|
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import Column from 'flavours/glitch/components/column';
|
||||||
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
|
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||||
|
import { getOrderedLists } from 'flavours/glitch/selectors/lists';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||||
|
create: { id: 'lists.create_list', defaultMessage: 'Create list' },
|
||||||
|
edit: { id: 'lists.edit', defaultMessage: 'Edit list' },
|
||||||
|
delete: { id: 'lists.delete', defaultMessage: 'Delete list' },
|
||||||
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListItem: React.FC<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}> = ({ id, title }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'CONFIRM_DELETE_LIST',
|
||||||
|
modalProps: {
|
||||||
|
listId: id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const menu = useMemo(
|
||||||
|
() => [
|
||||||
|
{ text: intl.formatMessage(messages.edit), to: `/lists/${id}/edit` },
|
||||||
|
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
|
||||||
|
],
|
||||||
|
[intl, id, handleDeleteClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='lists__item'>
|
||||||
|
<Link to={`/lists/${id}`} className='lists__item__title'>
|
||||||
|
<Icon id='list-ul' icon={ListAltIcon} />
|
||||||
|
<span>{title}</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<DropdownMenuContainer
|
||||||
|
scrollKey='lists'
|
||||||
|
items={menu}
|
||||||
|
icons='ellipsis-h'
|
||||||
|
iconComponent={MoreHorizIcon}
|
||||||
|
direction='right'
|
||||||
|
title={intl.formatMessage(messages.more)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Lists: React.FC<{
|
||||||
|
multiColumn?: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchLists());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const emptyMessage = (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.no_lists_yet'
|
||||||
|
defaultMessage='No lists yet.'
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.create_a_list_to_organize'
|
||||||
|
defaultMessage='Create a new list to organize your Home feed'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
label={intl.formatMessage(messages.heading)}
|
||||||
|
>
|
||||||
|
<ColumnHeader
|
||||||
|
title={intl.formatMessage(messages.heading)}
|
||||||
|
icon='list-ul'
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
extraButton={
|
||||||
|
<Link
|
||||||
|
to='/lists/new'
|
||||||
|
className='column-header__button'
|
||||||
|
title={intl.formatMessage(messages.create)}
|
||||||
|
aria-label={intl.formatMessage(messages.create)}
|
||||||
|
>
|
||||||
|
<Icon id='plus' icon={AddIcon} />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='lists'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{lists.map((list) => (
|
||||||
|
<ListItem key={list.id} id={list.id} title={list.title} />
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.heading)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default Lists;
|
373
app/javascript/flavours/glitch/features/lists/members.tsx
Normal file
373
app/javascript/flavours/glitch/features/lists/members.tsx
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
|
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||||
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||||
|
import { fetchFollowing } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { importFetchedAccounts } from 'flavours/glitch/actions/importer';
|
||||||
|
import { fetchList } from 'flavours/glitch/actions/lists';
|
||||||
|
import { apiRequest } from 'flavours/glitch/api';
|
||||||
|
import {
|
||||||
|
apiGetAccounts,
|
||||||
|
apiAddAccountToList,
|
||||||
|
apiRemoveAccountFromList,
|
||||||
|
} from 'flavours/glitch/api/lists';
|
||||||
|
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
|
||||||
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
|
import Column from 'flavours/glitch/components/column';
|
||||||
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
|
import { FollowersCounter } from 'flavours/glitch/components/counters';
|
||||||
|
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
|
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||||
|
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||||
|
import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
|
||||||
|
placeholder: {
|
||||||
|
id: 'lists.search_placeholder',
|
||||||
|
defaultMessage: 'Search people you follow',
|
||||||
|
},
|
||||||
|
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
|
||||||
|
add: { id: 'lists.add_member', defaultMessage: 'Add' },
|
||||||
|
remove: { id: 'lists.remove_member', defaultMessage: 'Remove' },
|
||||||
|
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||||
|
});
|
||||||
|
|
||||||
|
type Mode = 'remove' | 'add';
|
||||||
|
|
||||||
|
const ColumnSearchHeader: React.FC<{
|
||||||
|
onBack: () => void;
|
||||||
|
onSubmit: (value: string) => void;
|
||||||
|
}> = ({ onBack, onSubmit }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setValue(value);
|
||||||
|
onSubmit(value);
|
||||||
|
},
|
||||||
|
[setValue, onSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
onSubmit(value);
|
||||||
|
}, [onSubmit, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonInTabsBar>
|
||||||
|
<form className='column-search-header' onSubmit={handleSubmit}>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='column-header__back-button compact'
|
||||||
|
onClick={onBack}
|
||||||
|
aria-label={intl.formatMessage(messages.back)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
id='chevron-left'
|
||||||
|
icon={ArrowBackIcon}
|
||||||
|
className='column-back-button__icon'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='search'
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</ButtonInTabsBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountItem: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
listId: string;
|
||||||
|
partOfList: boolean;
|
||||||
|
onToggle: (accountId: string) => void;
|
||||||
|
}> = ({ accountId, listId, partOfList, onToggle }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (partOfList) {
|
||||||
|
void apiRemoveAccountFromList(listId, accountId);
|
||||||
|
} else {
|
||||||
|
void apiAddAccountToList(listId, accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggle(accountId);
|
||||||
|
}, [accountId, listId, partOfList, onToggle]);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='account'>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<Link
|
||||||
|
key={account.id}
|
||||||
|
className='account__display-name'
|
||||||
|
title={account.acct}
|
||||||
|
to={`/@${account.acct}`}
|
||||||
|
data-hover-card-account={account.id}
|
||||||
|
>
|
||||||
|
<div className='account__avatar-wrapper'>
|
||||||
|
<Avatar account={account} size={36} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='account__contents'>
|
||||||
|
<DisplayName account={account} />
|
||||||
|
|
||||||
|
<div className='account__details'>
|
||||||
|
<ShortNumber
|
||||||
|
value={account.followers_count}
|
||||||
|
renderer={FollowersCounter}
|
||||||
|
/>{' '}
|
||||||
|
{firstVerifiedField && (
|
||||||
|
<VerifiedBadge link={firstVerifiedField.value} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className='account__relationship'>
|
||||||
|
<Button
|
||||||
|
text={intl.formatMessage(
|
||||||
|
partOfList ? messages.remove : messages.add,
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListMembers: React.FC<{
|
||||||
|
multiColumn?: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const followingAccountIds = useAppSelector(
|
||||||
|
(state) => state.user_lists.getIn(['following', me, 'items']) as string[],
|
||||||
|
);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||||
|
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [mode, setMode] = useState<Mode>('remove');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
setLoading(true);
|
||||||
|
dispatch(fetchList(id));
|
||||||
|
|
||||||
|
void apiGetAccounts(id)
|
||||||
|
.then((data) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
setAccountIds(data.map((a) => a.id));
|
||||||
|
setLoading(false);
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(fetchFollowing(me));
|
||||||
|
}
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const handleSearchClick = useCallback(() => {
|
||||||
|
setMode('add');
|
||||||
|
}, [setMode]);
|
||||||
|
|
||||||
|
const handleDismissSearchClick = useCallback(() => {
|
||||||
|
setMode('remove');
|
||||||
|
setSearching(false);
|
||||||
|
}, [setMode]);
|
||||||
|
|
||||||
|
const handleAccountToggle = useCallback(
|
||||||
|
(accountId: string) => {
|
||||||
|
const partOfList = accountIds.includes(accountId);
|
||||||
|
|
||||||
|
if (partOfList) {
|
||||||
|
setAccountIds(accountIds.filter((id) => id !== accountId));
|
||||||
|
} else {
|
||||||
|
setAccountIds([accountId, ...accountIds]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[accountIds, setAccountIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchRequestRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const handleSearch = useDebouncedCallback(
|
||||||
|
(value: string) => {
|
||||||
|
if (searchRequestRef.current) {
|
||||||
|
searchRequestRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.trim().length === 0) {
|
||||||
|
setSearching(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
searchRequestRef.current = new AbortController();
|
||||||
|
|
||||||
|
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
|
||||||
|
signal: searchRequestRef.current.signal,
|
||||||
|
params: {
|
||||||
|
q: value,
|
||||||
|
resolve: false,
|
||||||
|
following: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
setSearchAccountIds(data.map((a) => a.id));
|
||||||
|
setLoading(false);
|
||||||
|
setSearching(true);
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setSearching(true);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
{ leading: true, trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
let displayedAccountIds: string[];
|
||||||
|
|
||||||
|
if (mode === 'add') {
|
||||||
|
displayedAccountIds = searching ? searchAccountIds : followingAccountIds;
|
||||||
|
} else {
|
||||||
|
displayedAccountIds = accountIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
label={intl.formatMessage(messages.heading)}
|
||||||
|
>
|
||||||
|
{mode === 'remove' ? (
|
||||||
|
<ColumnHeader
|
||||||
|
title={intl.formatMessage(messages.heading)}
|
||||||
|
icon='list-ul'
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
extraButton={
|
||||||
|
<button
|
||||||
|
onClick={handleSearchClick}
|
||||||
|
type='button'
|
||||||
|
className='column-header__button'
|
||||||
|
title={intl.formatMessage(messages.enterSearch)}
|
||||||
|
aria-label={intl.formatMessage(messages.enterSearch)}
|
||||||
|
>
|
||||||
|
<Icon id='plus' icon={AddIcon} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ColumnSearchHeader
|
||||||
|
onBack={handleDismissSearchClick}
|
||||||
|
onSubmit={handleSearch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='list_members'
|
||||||
|
trackScroll={!multiColumn}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
isLoading={loading}
|
||||||
|
showLoading={loading && displayedAccountIds.length === 0}
|
||||||
|
hasMore={false}
|
||||||
|
footer={
|
||||||
|
mode === 'remove' && (
|
||||||
|
<>
|
||||||
|
<div className='spacer' />
|
||||||
|
|
||||||
|
<div className='column-footer'>
|
||||||
|
<Link to={`/lists/${id}`} className='button button--block'>
|
||||||
|
<FormattedMessage id='lists.done' defaultMessage='Done' />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
emptyMessage={
|
||||||
|
mode === 'remove' ? (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.no_members_yet'
|
||||||
|
defaultMessage='No members yet.'
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.find_users_to_add'
|
||||||
|
defaultMessage='Find users to add'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.no_results_found'
|
||||||
|
defaultMessage='No results found.'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{displayedAccountIds.map((accountId) => (
|
||||||
|
<AccountItem
|
||||||
|
key={accountId}
|
||||||
|
accountId={accountId}
|
||||||
|
listId={id}
|
||||||
|
partOfList={
|
||||||
|
displayedAccountIds === accountIds ||
|
||||||
|
accountIds.includes(accountId)
|
||||||
|
}
|
||||||
|
onToggle={handleAccountToggle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.heading)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default ListMembers;
|
296
app/javascript/flavours/glitch/features/lists/new.tsx
Normal file
296
app/javascript/flavours/glitch/features/lists/new.tsx
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useParams, useHistory, Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import { fetchList } from 'flavours/glitch/actions/lists';
|
||||||
|
import { createList, updateList } from 'flavours/glitch/actions/lists_typed';
|
||||||
|
import { apiGetAccounts } from 'flavours/glitch/api/lists';
|
||||||
|
import type { RepliesPolicyType } from 'flavours/glitch/api_types/lists';
|
||||||
|
import Column from 'flavours/glitch/components/column';
|
||||||
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
edit: { id: 'column.edit_list', defaultMessage: 'Edit list' },
|
||||||
|
create: { id: 'column.create_list', defaultMessage: 'Create list' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const MembersLink: React.FC<{
|
||||||
|
id: string;
|
||||||
|
}> = ({ id }) => {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [avatars, setAvatars] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void apiGetAccounts(id)
|
||||||
|
.then((data) => {
|
||||||
|
setCount(data.length);
|
||||||
|
setAvatars(data.slice(0, 3).map((a) => a.avatar));
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Nothing
|
||||||
|
});
|
||||||
|
}, [id, setCount, setAvatars]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/lists/${id}/members`} className='app-form__link'>
|
||||||
|
<div className='app-form__link__text'>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.list_members'
|
||||||
|
defaultMessage='List members'
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.list_members_count'
|
||||||
|
defaultMessage='{count, plural, one {# member} other {# members}}'
|
||||||
|
values={{ count }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='avatar-pile'>
|
||||||
|
{avatars.map((url) => (
|
||||||
|
<img key={url} src={url} alt='' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewList: React.FC<{
|
||||||
|
multiColumn?: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { id } = useParams<{ id?: string }>();
|
||||||
|
const intl = useIntl();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const list = useAppSelector((state) =>
|
||||||
|
id ? state.lists.get(id) : undefined,
|
||||||
|
);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [exclusive, setExclusive] = useState(false);
|
||||||
|
const [repliesPolicy, setRepliesPolicy] = useState<RepliesPolicyType>('list');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
dispatch(fetchList(id));
|
||||||
|
}
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && list) {
|
||||||
|
setTitle(list.title);
|
||||||
|
setExclusive(list.exclusive);
|
||||||
|
setRepliesPolicy(list.replies_policy);
|
||||||
|
}
|
||||||
|
}, [setTitle, setExclusive, setRepliesPolicy, id, list]);
|
||||||
|
|
||||||
|
const handleTitleChange = useCallback(
|
||||||
|
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTitle(value);
|
||||||
|
},
|
||||||
|
[setTitle],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExclusiveChange = useCallback(
|
||||||
|
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setExclusive(checked);
|
||||||
|
},
|
||||||
|
[setExclusive],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRepliesPolicyChange = useCallback(
|
||||||
|
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setRepliesPolicy(value as RepliesPolicyType);
|
||||||
|
},
|
||||||
|
[setRepliesPolicy],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
void dispatch(
|
||||||
|
updateList({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
exclusive,
|
||||||
|
replies_policy: repliesPolicy,
|
||||||
|
}),
|
||||||
|
).then(() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
void dispatch(
|
||||||
|
createList({
|
||||||
|
title,
|
||||||
|
exclusive,
|
||||||
|
replies_policy: repliesPolicy,
|
||||||
|
}),
|
||||||
|
).then((result) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
|
||||||
|
if (isFulfilled(result)) {
|
||||||
|
history.replace(`/lists/${result.payload.id}/edit`);
|
||||||
|
history.push(`/lists/${result.payload.id}/members`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [history, dispatch, setSubmitting, id, title, exclusive, repliesPolicy]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
label={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||||
|
>
|
||||||
|
<ColumnHeader
|
||||||
|
title={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||||
|
icon='list-ul'
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='scrollable'>
|
||||||
|
<form className='simple_form app-form' onSubmit={handleSubmit}>
|
||||||
|
<div className='fields-group'>
|
||||||
|
<div className='input with_label'>
|
||||||
|
<div className='label_input'>
|
||||||
|
<label htmlFor='list_title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.list_name'
|
||||||
|
defaultMessage='List name'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='label_input__wrapper'>
|
||||||
|
<input
|
||||||
|
id='list_title'
|
||||||
|
type='text'
|
||||||
|
value={title}
|
||||||
|
onChange={handleTitleChange}
|
||||||
|
maxLength={30}
|
||||||
|
required
|
||||||
|
placeholder=' '
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='fields-group'>
|
||||||
|
<div className='input with_label'>
|
||||||
|
<div className='label_input'>
|
||||||
|
<label htmlFor='list_replies_policy'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.show_replies_to'
|
||||||
|
defaultMessage='Include replies from list members to'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='label_input__wrapper'>
|
||||||
|
<select
|
||||||
|
id='list_replies_policy'
|
||||||
|
value={repliesPolicy}
|
||||||
|
onChange={handleRepliesPolicyChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.replies_policy.none'
|
||||||
|
defaultMessage='No one'
|
||||||
|
>
|
||||||
|
{(msg) => <option value='none'>{msg}</option>}
|
||||||
|
</FormattedMessage>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.replies_policy.list'
|
||||||
|
defaultMessage='Members of the list'
|
||||||
|
>
|
||||||
|
{(msg) => <option value='list'>{msg}</option>}
|
||||||
|
</FormattedMessage>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.replies_policy.followed'
|
||||||
|
defaultMessage='Any followed user'
|
||||||
|
>
|
||||||
|
{(msg) => <option value='followed'>{msg}</option>}
|
||||||
|
</FormattedMessage>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{id && (
|
||||||
|
<div className='fields-group'>
|
||||||
|
<MembersLink id={id} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='fields-group'>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||||
|
<label className='app-form__toggle'>
|
||||||
|
<div className='app-form__toggle__label'>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.exclusive'
|
||||||
|
defaultMessage='Hide members in Home'
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<span className='hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='lists.exclusive_hint'
|
||||||
|
defaultMessage='If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='app-form__toggle__toggle'>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
checked={exclusive}
|
||||||
|
onChange={handleExclusiveChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='actions'>
|
||||||
|
<button className='button' type='submit'>
|
||||||
|
{submitting ? (
|
||||||
|
<LoadingIndicator />
|
||||||
|
) : id ? (
|
||||||
|
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='lists.create' defaultMessage='Create' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{intl.formatMessage(id ? messages.edit : messages.create)}
|
||||||
|
</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default NewList;
|
@ -11,7 +11,6 @@ import {
|
|||||||
ReportModal,
|
ReportModal,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
EmbedModal,
|
EmbedModal,
|
||||||
ListEditor,
|
|
||||||
ListAdder,
|
ListAdder,
|
||||||
CompareHistoryModal,
|
CompareHistoryModal,
|
||||||
FilterModal,
|
FilterModal,
|
||||||
@ -72,7 +71,6 @@ export const MODAL_COMPONENTS = {
|
|||||||
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
'LIST_EDITOR': ListEditor,
|
|
||||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||||
'LIST_ADDER': ListAdder,
|
'LIST_ADDER': ListAdder,
|
||||||
'COMPARE_HISTORY': CompareHistoryModal,
|
'COMPARE_HISTORY': CompareHistoryModal,
|
||||||
|
@ -62,11 +62,13 @@ import {
|
|||||||
FollowedTags,
|
FollowedTags,
|
||||||
LinkTimeline,
|
LinkTimeline,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
|
Lists,
|
||||||
|
ListEdit,
|
||||||
|
ListMembers,
|
||||||
Blocks,
|
Blocks,
|
||||||
DomainBlocks,
|
DomainBlocks,
|
||||||
Mutes,
|
Mutes,
|
||||||
PinnedStatuses,
|
PinnedStatuses,
|
||||||
Lists,
|
|
||||||
GettingStartedMisc,
|
GettingStartedMisc,
|
||||||
Directory,
|
Directory,
|
||||||
Explore,
|
Explore,
|
||||||
@ -216,6 +218,9 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
|
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
|
||||||
|
<WrappedRoute path='/lists/new' component={ListEdit} content={children} />
|
||||||
|
<WrappedRoute path='/lists/:id/edit' component={ListEdit} content={children} />
|
||||||
|
<WrappedRoute path='/lists/:id/members' component={ListMembers} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
<WrappedRoute path='/notifications' component={NotificationsWrapper} content={children} exact />
|
<WrappedRoute path='/notifications' component={NotificationsWrapper} content={children} exact />
|
||||||
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||||
|
@ -158,10 +158,6 @@ export function EmbedModal () {
|
|||||||
return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'../components/embed_modal');
|
return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'../components/embed_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListEditor () {
|
|
||||||
return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'../../list_editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ListAdder () {
|
export function ListAdder () {
|
||||||
return import(/* webpackChunkName: "features/glitch/async/list_adder" */'../../list_adder');
|
return import(/* webpackChunkName: "features/glitch/async/list_adder" */'../../list_adder');
|
||||||
}
|
}
|
||||||
@ -229,3 +225,11 @@ export function LinkTimeline () {
|
|||||||
export function AnnualReportModal () {
|
export function AnnualReportModal () {
|
||||||
return import(/*webpackChunkName: "flavours/glitch/async/modals/annual_report_modal" */'../components/annual_report_modal');
|
return import(/*webpackChunkName: "flavours/glitch/async/modals/annual_report_modal" */'../components/annual_report_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ListEdit () {
|
||||||
|
return import(/*webpackChunkName: "flavours/glitch/async/lists" */'../../lists/new');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListMembers () {
|
||||||
|
return import(/* webpackChunkName: "flavours/glitch/async/lists" */'../../lists/members');
|
||||||
|
}
|
||||||
|
18
app/javascript/flavours/glitch/models/list.ts
Normal file
18
app/javascript/flavours/glitch/models/list.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiListJSON } from 'flavours/glitch/api_types/lists';
|
||||||
|
|
||||||
|
type ListShape = Required<ApiListJSON>; // no changes from server shape
|
||||||
|
export type List = RecordOf<ListShape>;
|
||||||
|
|
||||||
|
const ListFactory = Record<ListShape>({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
exclusive: false,
|
||||||
|
replies_policy: 'list',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createList(attributes: Partial<ListShape>) {
|
||||||
|
return ListFactory(attributes);
|
||||||
|
}
|
@ -17,9 +17,7 @@ import filters from './filters';
|
|||||||
import followed_tags from './followed_tags';
|
import followed_tags from './followed_tags';
|
||||||
import height_cache from './height_cache';
|
import height_cache from './height_cache';
|
||||||
import history from './history';
|
import history from './history';
|
||||||
import listAdder from './list_adder';
|
import { listsReducer } from './lists';
|
||||||
import listEditor from './list_editor';
|
|
||||||
import lists from './lists';
|
|
||||||
import local_settings from './local_settings';
|
import local_settings from './local_settings';
|
||||||
import { markersReducer } from './markers';
|
import { markersReducer } from './markers';
|
||||||
import media_attachments from './media_attachments';
|
import media_attachments from './media_attachments';
|
||||||
@ -71,9 +69,7 @@ const reducers = {
|
|||||||
notificationGroups: notificationGroupsReducer,
|
notificationGroups: notificationGroupsReducer,
|
||||||
height_cache,
|
height_cache,
|
||||||
custom_emojis,
|
custom_emojis,
|
||||||
lists,
|
lists: listsReducer,
|
||||||
listEditor,
|
|
||||||
listAdder,
|
|
||||||
filters,
|
filters,
|
||||||
conversations,
|
conversations,
|
||||||
suggestions,
|
suggestions,
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LIST_ADDER_RESET,
|
|
||||||
LIST_ADDER_SETUP,
|
|
||||||
LIST_ADDER_LISTS_FETCH_REQUEST,
|
|
||||||
LIST_ADDER_LISTS_FETCH_SUCCESS,
|
|
||||||
LIST_ADDER_LISTS_FETCH_FAIL,
|
|
||||||
LIST_EDITOR_ADD_SUCCESS,
|
|
||||||
LIST_EDITOR_REMOVE_SUCCESS,
|
|
||||||
} from '../actions/lists';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
|
||||||
accountId: null,
|
|
||||||
|
|
||||||
lists: ImmutableMap({
|
|
||||||
items: ImmutableList(),
|
|
||||||
loaded: false,
|
|
||||||
isLoading: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function listAdderReducer(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case LIST_ADDER_RESET:
|
|
||||||
return initialState;
|
|
||||||
case LIST_ADDER_SETUP:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.set('accountId', action.account.get('id'));
|
|
||||||
});
|
|
||||||
case LIST_ADDER_LISTS_FETCH_REQUEST:
|
|
||||||
return state.setIn(['lists', 'isLoading'], true);
|
|
||||||
case LIST_ADDER_LISTS_FETCH_FAIL:
|
|
||||||
return state.setIn(['lists', 'isLoading'], false);
|
|
||||||
case LIST_ADDER_LISTS_FETCH_SUCCESS:
|
|
||||||
return state.update('lists', lists => lists.withMutations(map => {
|
|
||||||
map.set('isLoading', false);
|
|
||||||
map.set('loaded', true);
|
|
||||||
map.set('items', ImmutableList(action.lists.map(item => item.id)));
|
|
||||||
}));
|
|
||||||
case LIST_EDITOR_ADD_SUCCESS:
|
|
||||||
return state.updateIn(['lists', 'items'], list => list.unshift(action.listId));
|
|
||||||
case LIST_EDITOR_REMOVE_SUCCESS:
|
|
||||||
return state.updateIn(['lists', 'items'], list => list.filterNot(item => item === action.listId));
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LIST_CREATE_REQUEST,
|
|
||||||
LIST_CREATE_FAIL,
|
|
||||||
LIST_CREATE_SUCCESS,
|
|
||||||
LIST_UPDATE_REQUEST,
|
|
||||||
LIST_UPDATE_FAIL,
|
|
||||||
LIST_UPDATE_SUCCESS,
|
|
||||||
LIST_EDITOR_RESET,
|
|
||||||
LIST_EDITOR_SETUP,
|
|
||||||
LIST_EDITOR_TITLE_CHANGE,
|
|
||||||
LIST_ACCOUNTS_FETCH_REQUEST,
|
|
||||||
LIST_ACCOUNTS_FETCH_SUCCESS,
|
|
||||||
LIST_ACCOUNTS_FETCH_FAIL,
|
|
||||||
LIST_EDITOR_SUGGESTIONS_READY,
|
|
||||||
LIST_EDITOR_SUGGESTIONS_CLEAR,
|
|
||||||
LIST_EDITOR_SUGGESTIONS_CHANGE,
|
|
||||||
LIST_EDITOR_ADD_SUCCESS,
|
|
||||||
LIST_EDITOR_REMOVE_SUCCESS,
|
|
||||||
} from '../actions/lists';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
|
||||||
listId: null,
|
|
||||||
isSubmitting: false,
|
|
||||||
isChanged: false,
|
|
||||||
title: '',
|
|
||||||
isExclusive: false,
|
|
||||||
|
|
||||||
accounts: ImmutableMap({
|
|
||||||
items: ImmutableList(),
|
|
||||||
loaded: false,
|
|
||||||
isLoading: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
suggestions: ImmutableMap({
|
|
||||||
value: '',
|
|
||||||
items: ImmutableList(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function listEditorReducer(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case LIST_EDITOR_RESET:
|
|
||||||
return initialState;
|
|
||||||
case LIST_EDITOR_SETUP:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.set('listId', action.list.get('id'));
|
|
||||||
map.set('title', action.list.get('title'));
|
|
||||||
map.set('isExclusive', action.list.get('is_exclusive'));
|
|
||||||
map.set('isSubmitting', false);
|
|
||||||
});
|
|
||||||
case LIST_EDITOR_TITLE_CHANGE:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.set('title', action.value);
|
|
||||||
map.set('isChanged', true);
|
|
||||||
});
|
|
||||||
case LIST_CREATE_REQUEST:
|
|
||||||
case LIST_UPDATE_REQUEST:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.set('isSubmitting', true);
|
|
||||||
map.set('isChanged', false);
|
|
||||||
});
|
|
||||||
case LIST_CREATE_FAIL:
|
|
||||||
case LIST_UPDATE_FAIL:
|
|
||||||
return state.set('isSubmitting', false);
|
|
||||||
case LIST_CREATE_SUCCESS:
|
|
||||||
case LIST_UPDATE_SUCCESS:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.set('isSubmitting', false);
|
|
||||||
map.set('listId', action.list.id);
|
|
||||||
});
|
|
||||||
case LIST_ACCOUNTS_FETCH_REQUEST:
|
|
||||||
return state.setIn(['accounts', 'isLoading'], true);
|
|
||||||
case LIST_ACCOUNTS_FETCH_FAIL:
|
|
||||||
return state.setIn(['accounts', 'isLoading'], false);
|
|
||||||
case LIST_ACCOUNTS_FETCH_SUCCESS:
|
|
||||||
return state.update('accounts', accounts => accounts.withMutations(map => {
|
|
||||||
map.set('isLoading', false);
|
|
||||||
map.set('loaded', true);
|
|
||||||
map.set('items', ImmutableList(action.accounts.map(item => item.id)));
|
|
||||||
}));
|
|
||||||
case LIST_EDITOR_SUGGESTIONS_CHANGE:
|
|
||||||
return state.setIn(['suggestions', 'value'], action.value);
|
|
||||||
case LIST_EDITOR_SUGGESTIONS_READY:
|
|
||||||
return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
|
|
||||||
case LIST_EDITOR_SUGGESTIONS_CLEAR:
|
|
||||||
return state.update('suggestions', suggestions => suggestions.withMutations(map => {
|
|
||||||
map.set('items', ImmutableList());
|
|
||||||
map.set('value', '');
|
|
||||||
}));
|
|
||||||
case LIST_EDITOR_ADD_SUCCESS:
|
|
||||||
return state.updateIn(['accounts', 'items'], list => list.unshift(action.accountId));
|
|
||||||
case LIST_EDITOR_REMOVE_SUCCESS:
|
|
||||||
return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.accountId));
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LIST_FETCH_SUCCESS,
|
|
||||||
LIST_FETCH_FAIL,
|
|
||||||
LISTS_FETCH_SUCCESS,
|
|
||||||
LIST_CREATE_SUCCESS,
|
|
||||||
LIST_UPDATE_SUCCESS,
|
|
||||||
LIST_DELETE_SUCCESS,
|
|
||||||
} from '../actions/lists';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
const normalizeList = (state, list) => state.set(list.id, fromJS(list));
|
|
||||||
|
|
||||||
const normalizeLists = (state, lists) => {
|
|
||||||
lists.forEach(list => {
|
|
||||||
state = normalizeList(state, list);
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function lists(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case LIST_FETCH_SUCCESS:
|
|
||||||
case LIST_CREATE_SUCCESS:
|
|
||||||
case LIST_UPDATE_SUCCESS:
|
|
||||||
return normalizeList(state, action.list);
|
|
||||||
case LISTS_FETCH_SUCCESS:
|
|
||||||
return normalizeLists(state, action.lists);
|
|
||||||
case LIST_DELETE_SUCCESS:
|
|
||||||
case LIST_FETCH_FAIL:
|
|
||||||
return state.set(action.id, false);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
49
app/javascript/flavours/glitch/reducers/lists.ts
Normal file
49
app/javascript/flavours/glitch/reducers/lists.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import type { Reducer } from '@reduxjs/toolkit';
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { createList, updateList } from 'flavours/glitch/actions/lists_typed';
|
||||||
|
import type { ApiListJSON } from 'flavours/glitch/api_types/lists';
|
||||||
|
import { createList as createListFromJSON } from 'flavours/glitch/models/list';
|
||||||
|
import type { List } from 'flavours/glitch/models/list';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LIST_FETCH_SUCCESS,
|
||||||
|
LIST_FETCH_FAIL,
|
||||||
|
LISTS_FETCH_SUCCESS,
|
||||||
|
LIST_DELETE_SUCCESS,
|
||||||
|
} from '../actions/lists';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap<string, List | null>();
|
||||||
|
type State = typeof initialState;
|
||||||
|
|
||||||
|
const normalizeList = (state: State, list: ApiListJSON) =>
|
||||||
|
state.set(list.id, createListFromJSON(list));
|
||||||
|
|
||||||
|
const normalizeLists = (state: State, lists: ApiListJSON[]) => {
|
||||||
|
lists.forEach((list) => {
|
||||||
|
state = normalizeList(state, list);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listsReducer: Reducer<State> = (state = initialState, action) => {
|
||||||
|
if (
|
||||||
|
createList.fulfilled.match(action) ||
|
||||||
|
updateList.fulfilled.match(action)
|
||||||
|
) {
|
||||||
|
return normalizeList(state, action.payload);
|
||||||
|
} else {
|
||||||
|
switch (action.type) {
|
||||||
|
case LIST_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, action.list as ApiListJSON);
|
||||||
|
case LISTS_FETCH_SUCCESS:
|
||||||
|
return normalizeLists(state, action.lists as ApiListJSON[]);
|
||||||
|
case LIST_DELETE_SUCCESS:
|
||||||
|
case LIST_FETCH_FAIL:
|
||||||
|
return state.set(action.id as string, null);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
15
app/javascript/flavours/glitch/selectors/lists.ts
Normal file
15
app/javascript/flavours/glitch/selectors/lists.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import type { List } from 'flavours/glitch/models/list';
|
||||||
|
import type { RootState } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
export const getOrderedLists = createSelector(
|
||||||
|
[(state: RootState) => state.lists],
|
||||||
|
(lists: ImmutableMap<string, List | null>) =>
|
||||||
|
lists
|
||||||
|
.toList()
|
||||||
|
.filter((item: List | null) => !!item)
|
||||||
|
.sort((a: List, b: List) => a.title.localeCompare(b.title))
|
||||||
|
.toArray(),
|
||||||
|
);
|
@ -630,16 +630,6 @@ body,
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple_form {
|
|
||||||
.actions {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-form-box {
|
.batch-form-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -4991,6 +4991,7 @@ a.status-card {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 13px;
|
width: 13px;
|
||||||
@ -5408,7 +5409,8 @@ a.status-card {
|
|||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -5430,6 +5432,17 @@ a.status-card {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-column-indicator {
|
||||||
|
&__arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
inset-inline-start: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(100%, -100%) rotate(12deg);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.follow_requests-unlocked_explanation {
|
.follow_requests-unlocked_explanation {
|
||||||
background: var(--surface-background-color);
|
background: var(--surface-background-color);
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
@ -6233,7 +6246,7 @@ a.status-card {
|
|||||||
|
|
||||||
.modal-root {
|
.modal-root {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 9999;
|
z-index: 9998;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-root__overlay {
|
.modal-root__overlay {
|
||||||
@ -6838,12 +6851,14 @@ a.status-card {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
|
box-sizing: border-box;
|
||||||
border-bottom: 1px solid var(--modal-border-color);
|
border-bottom: 1px solid var(--modal-border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
|
min-height: 61px;
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -8521,92 +8536,6 @@ noscript {
|
|||||||
background: rgba($base-overlay-background, 0.5);
|
background: rgba($base-overlay-background, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-adder,
|
|
||||||
.list-editor {
|
|
||||||
backdrop-filter: var(--background-filter);
|
|
||||||
background: var(--modal-background-color);
|
|
||||||
border: 1px solid var(--modal-border-color);
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 380px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
@media screen and (width <= 420px) {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-adder {
|
|
||||||
&__lists {
|
|
||||||
height: 50vh;
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list__wrapper {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list__display-name {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-editor {
|
|
||||||
h4 {
|
|
||||||
padding: 15px 0;
|
|
||||||
background: lighten($ui-base-color, 13%);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer__pager {
|
|
||||||
height: 50vh;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer__inner {
|
|
||||||
&.backdrop {
|
|
||||||
width: calc(100% - 60px);
|
|
||||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
|
||||||
border-radius: 0 0 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__accounts {
|
|
||||||
background: unset;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__display-name {
|
|
||||||
&:hover strong {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__avatar {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.focal-point {
|
.focal-point {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
@ -10677,7 +10606,7 @@ noscript {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 2rem;
|
bottom: 2rem;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
z-index: 999;
|
z-index: 9999;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@ -11686,3 +11615,87 @@ noscript {
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lists__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-inline-end: 13px;
|
||||||
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 16px 13px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: $secondary-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:is(a):hover,
|
||||||
|
&:is(a):focus,
|
||||||
|
&:is(a):active {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--input-placeholder-color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-search-header {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
border: 1px solid var(--background-border-color);
|
||||||
|
|
||||||
|
.column-header__back-button.compact {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: $primary-text-color;
|
||||||
|
font-size: 16px;
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--input-placeholder-color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-footer {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lists-scrollable {
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
@ -1256,6 +1256,8 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-form {
|
.app-form {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
&__avatar-input,
|
&__avatar-input,
|
||||||
&__header-input {
|
&__header-input {
|
||||||
display: block;
|
display: block;
|
||||||
@ -1371,4 +1373,55 @@ code {
|
|||||||
padding-inline-start: 16px;
|
padding-inline-start: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 8px 0;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: $primary-text-color;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-pile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 2px solid var(--background-color);
|
||||||
|
background: var(--surface-background-color);
|
||||||
|
margin-inline-end: -16px;
|
||||||
|
transform: rotate(0);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
transform: rotate(-4deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,4 +79,5 @@ body {
|
|||||||
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||||
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||||
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
||||||
|
--input-placeholder-color: #{transparentize($dark-text-color, 0.5)};
|
||||||
}
|
}
|
||||||
|
@ -125,4 +125,5 @@ $dismiss-overlay-width: 4rem;
|
|||||||
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
||||||
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
||||||
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
||||||
|
--input-placeholder-color: #{$dark-text-color};
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user