Wip: bookmark statuses view and adder

This commit is contained in:
KMY 2023-08-26 13:25:57 +09:00
parent f6bdd9b6de
commit 87490a3220
30 changed files with 616 additions and 24 deletions

View file

@ -150,10 +150,10 @@ export const createBookmarkCategoryFail = error => ({
error,
});
export const updateBookmarkCategory = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
export const updateBookmarkCategory = (id, title, shouldReset) => (dispatch, getState) => {
dispatch(updateBookmarkCategoryRequest(id));
api(getState).put(`/api/v1/bookmark_categories/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
api(getState).put(`/api/v1/bookmark_categories/${id}`, { title }).then(({ data }) => {
dispatch(updateBookmarkCategorySuccess(data));
if (shouldReset) {

View file

@ -37,6 +37,7 @@ const messages = defineMessages({
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
emojiReaction: { id: 'status.emoji_reaction', defaultMessage: 'Stamp' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
bookmarkCategory: { id: 'status.bookmark_category', defaultMessage: 'Bookmark category' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
@ -92,6 +93,7 @@ class StatusActionBar extends ImmutablePureComponent {
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
onBookmark: PropTypes.func,
onBookmarkCategoryAdder: PropTypes.func,
onFilter: PropTypes.func,
onAddFilter: PropTypes.func,
onInteractionModal: PropTypes.func,
@ -167,6 +169,10 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onBookmark(this.props.status);
};
handleBookmarkCategoryAdderClick = () => {
this.props.onBookmarkCategoryAdder(this.props.status);
};
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
};
@ -300,6 +306,7 @@ class StatusActionBar extends ImmutablePureComponent {
}
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
menu.push({ text: intl.formatMessage(messages.bookmarkCategory), action: this.handleBookmarkCategoryAdderClick });
if (writtenByMe && pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });

View file

@ -142,6 +142,15 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}
},
onBookmarkCategoryAdder (status) {
dispatch(openModal({
modalType: 'BOOKMARK_CATEGORY_ADDER',
modalProps: {
statusId: status.get('id'),
},
}));
},
onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));

View file

@ -17,11 +17,12 @@ import ScrollableList from 'mastodon/components/scrollable_list';
import ColumnLink from 'mastodon/features/ui/components/column_link';
import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';
import NewListForm from './components/new_list_form';
import NewListForm from './components/new_bookmark_category_form';
const messages = defineMessages({
heading: { id: 'column.bookmark_categories', defaultMessage: 'Bookmark categories' },
subheading: { id: 'bookmark_categories_ex.subheading', defaultMessage: 'Your categories' },
subheading: { id: 'bookmark_categories.subheading', defaultMessage: 'Your categories' },
allBookmarks: { id: 'bookmark_categories.all_bookmarks', defaultMessage: 'All bookmarks' },
});
const getOrderedCategories = createSelector([state => state.get('bookmark_categories')], categories => {
@ -69,6 +70,7 @@ class BookmarkCategories extends ImmutablePureComponent {
<NewListForm />
<ColumnLink to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.allBookmarks)} />,
<ScrollableList
scrollKey='bookmark_categories'
emptyMessage={emptyMessage}

View file

@ -0,0 +1,43 @@
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.map.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));

View file

@ -0,0 +1,72 @@
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 { Icon } from 'mastodon/components/icon';
import { removeFromBookmarkCategoryAdder, addToBookmarkCategoryAdder } from '../../../actions/bookmark_categories';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({
remove: { id: 'bookmark_categories.status.remove', defaultMessage: 'Remove from bookmark category' },
add: { id: 'bookmark_categories.status.add', defaultMessage: 'Add to bookmark category' },
});
const MapStateToProps = (state, { bookmarkCategoryId, added }) => ({
bookmarkCategory: state.get('bookmark_categories').get(bookmarkCategoryId),
added: typeof added === 'undefined' ? state.getIn(['bookmarkCategoryAdder', 'bookmarkCategories', 'items']).includes(bookmarkCategoryId) : added,
});
const mapDispatchToProps = (dispatch, { bookmarkCategoryId }) => ({
onRemove: () => dispatch(removeFromBookmarkCategoryAdder(bookmarkCategoryId)),
onAdd: () => dispatch(addToBookmarkCategoryAdder(bookmarkCategoryId)),
});
class BookmarkCategory extends ImmutablePureComponent {
static propTypes = {
bookmarkCategory: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
added: PropTypes.bool,
};
static defaultProps = {
added: false,
};
render () {
const { bookmarkCategory, intl, onRemove, onAdd, added } = this.props;
let button;
if (added) {
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
}
return (
<div className='list'>
<div className='list__wrapper'>
<div className='list__display-name'>
<Icon id='user-bookmarkCategory' className='column-link__icon' fixedWidth />
{bookmarkCategory.get('title')}
</div>
<div className='account__relationship'>
{button}
</div>
</div>
</div>
);
}
}
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(BookmarkCategory));

View file

@ -0,0 +1,77 @@
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 { createSelector } from 'reselect';
import { setupBookmarkCategoryAdder, resetBookmarkCategoryAdder } from '../../actions/bookmark_categories';
import NewBookmarkCategoryForm from '../bookmark_categories/components/new_bookmark_category_form';
// import Account from './components/account';
import BookmarkCategory from './components/bookmark_category';
const getOrderedBookmarkCategories = createSelector([state => state.get('bookmark_categories')], bookmarkCategories => {
if (!bookmarkCategories) {
return bookmarkCategories;
}
return bookmarkCategories.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => ({
bookmarkCategoryIds: getOrderedBookmarkCategories(state).map(bookmarkCategory=>bookmarkCategory.get('id')),
});
const mapDispatchToProps = dispatch => ({
onInitialize: statusId => dispatch(setupBookmarkCategoryAdder(statusId)),
onReset: () => dispatch(resetBookmarkCategoryAdder()),
});
class BookmarkCategoryAdder extends ImmutablePureComponent {
static propTypes = {
statusId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
onInitialize: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
bookmarkCategoryIds: ImmutablePropTypes.list.isRequired,
};
componentDidMount () {
const { onInitialize, statusId } = this.props;
onInitialize(statusId);
}
componentWillUnmount () {
const { onReset } = this.props;
onReset();
}
render () {
const { bookmarkCategoryIds } = this.props;
return (
<div className='modal-root__modal list-adder'>
{/*
<div className='list-adder__account'>
<Account accountId={accountId} />
</div>
*/}
<NewBookmarkCategoryForm />
<div className='list-adder__lists'>
{bookmarkCategoryIds.map(BookmarkCategoryId => <BookmarkCategory key={BookmarkCategoryId} bookmarkCategoryId={BookmarkCategoryId} />)}
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(BookmarkCategoryAdder));

View file

@ -99,8 +99,8 @@ class BookmarkCategoryStatuses extends ImmutablePureComponent {
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='bookmark_ex'
title={intl.formatMessage(messages.heading)}
icon='bookmark'
title={bookmarkCategory.get('title')}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}

View file

@ -72,6 +72,7 @@ class ActionBar extends PureComponent {
onEmojiReact: PropTypes.func.isRequired,
onReference: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onBookmarkCategoryAdder: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,

View file

@ -374,6 +374,15 @@ class Status extends ImmutablePureComponent {
}
};
handleBookmarkCategoryAdderClick = (status) => {
this.props.dispatch(openModal({
modalType: 'BOOKMARK_CATEGORY_ADDER',
modalProps: {
statusId: status.get('id'),
},
}));
};
handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;
@ -737,6 +746,7 @@ class Status extends ImmutablePureComponent {
onReblogForceModal={this.handleReblogForceModalClick}
onReference={this.handleReference}
onBookmark={this.handleBookmarkClick}
onBookmarkCategoryAdder={this.handleBookmarkCategoryAdderClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}

View file

@ -15,6 +15,7 @@ import {
AntennaAdder,
CircleEditor,
CircleAdder,
BookmarkCategoryAdder,
CompareHistoryModal,
FilterModal,
InteractionModal,
@ -55,6 +56,7 @@ export const MODAL_COMPONENTS = {
'LIST_ADDER': ListAdder,
'ANTENNA_ADDER': AntennaAdder,
'CIRCLE_ADDER': CircleAdder,
'BOOKMARK_CATEGORY_ADDER': BookmarkCategoryAdder,
'COMPARE_HISTORY': CompareHistoryModal,
'FILTER': FilterModal,
'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,

View file

@ -130,6 +130,10 @@ export function BookmarkCategoryStatuses () {
return import(/* webpackChunkName: "features/bookmark_category_statuses" */'../../bookmark_category_statuses');
}
export function BookmarkCategoryAdder () {
return import(/* webpackChunkName: "features/bookmark_category_adder" */'../../bookmark_category_adder');
}
export function Blocks () {
return import(/* webpackChunkName: "features/blocks" */'../../blocks');
}

View file

@ -14,6 +14,9 @@ import {
BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS,
BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL,
} from '../actions/bookmark_categories';
import {
UNBOOKMARK_SUCCESS,
} from '../actions/interactions';
const initialState = ImmutableMap();
@ -27,8 +30,8 @@ const normalizeBookmarkCategories = (state, bookmarkCategories) => {
return state;
};
const normalizeBookmarkCategoryStatuses = (state, bookmaryCategoryId, statuses, next) => {
return state.updateIn([bookmaryCategoryId, 'items'], listMap => listMap.withMutations(map => {
const normalizeBookmarkCategoryStatuses = (state, bookmarkCategoryId, statuses, next) => {
return state.update(bookmarkCategoryId, listMap => listMap.withMutations(map => {
map.set('next', next);
map.set('loaded', true);
map.set('isLoading', false);
@ -37,13 +40,20 @@ const normalizeBookmarkCategoryStatuses = (state, bookmaryCategoryId, statuses,
};
const appendToBookmarkCategoryStatuses = (state, bookmarkCategoryId, statuses, next) => {
return state.updateIn([bookmarkCategoryId, 'items'], listMap => listMap.withMutations(map => {
return state.update(bookmarkCategoryId, listMap => listMap.withMutations(map => {
map.set('next', next);
map.set('isLoading', false);
map.set('items', map.get('items').union(statuses.map(item => item.id)));
}));
};
const removeStatusFromAllBookmarkCategories = (state, status) => {
state.toList().forEach((bookmarkCategory) => {
state = state.updateIn([bookmarkCategory.get('id'), 'items'], items => items.delete(status.get('id')));
});
return state;
};
export default function bookmarkCategories(state = initialState, action) {
switch(action.type) {
case BOOKMARK_CATEGORY_FETCH_SUCCESS:
@ -65,6 +75,8 @@ export default function bookmarkCategories(state = initialState, action) {
return normalizeBookmarkCategoryStatuses(state, action.id, action.statuses, action.next);
case BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS:
return appendToBookmarkCategoryStatuses(state, action.id, action.statuses, action.next);
case UNBOOKMARK_SUCCESS:
return removeStatusFromAllBookmarkCategories(state, action.status);
default:
return state;
}

View file

@ -0,0 +1,48 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import {
BOOKMARK_CATEGORY_ADDER_RESET,
BOOKMARK_CATEGORY_ADDER_SETUP,
BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST,
BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS,
BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL,
BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS,
BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS,
} from '../actions/bookmark_categories';
const initialState = ImmutableMap({
statusId: null,
bookmarkCategories: ImmutableMap({
items: ImmutableList(),
loaded: false,
isLoading: false,
}),
});
export default function bookmarkCategoryAdderReducer(state = initialState, action) {
switch(action.type) {
case BOOKMARK_CATEGORY_ADDER_RESET:
return initialState;
case BOOKMARK_CATEGORY_ADDER_SETUP:
return state.withMutations(map => {
map.set('statusId', action.status.get('id'));
});
case BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST:
return state.setIn(['bookmarkCategories', 'isLoading'], true);
case BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL:
return state.setIn(['bookmarkCategories', 'isLoading'], false);
case BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS:
return state.update('bookmarkCategories', bookmarkCategories => bookmarkCategories.withMutations(map => {
map.set('isLoading', false);
map.set('loaded', true);
map.set('items', ImmutableList(action.bookmarkCategories.map(item => item.id)));
}));
case BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS:
return state.updateIn(['bookmarkCategories', 'items'], bookmarkCategory => bookmarkCategory.unshift(action.bookmarkCategoryId));
case BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS:
return state.updateIn(['bookmarkCategories', 'items'], bookmarkCategory => bookmarkCategory.filterNot(item => item === action.bookmarkCategoryId));
default:
return state;
}
}

View file

@ -10,12 +10,10 @@ import {
BOOKMARK_CATEGORY_EDITOR_RESET,
BOOKMARK_CATEGORY_EDITOR_SETUP,
BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE,
BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS,
BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS,
} from '../actions/bookmark_categories';
const initialState = ImmutableMap({
bookmaryCategoryId: null,
bookmarkCategoryId: null,
isSubmitting: false,
isChanged: false,
title: '',
@ -33,15 +31,15 @@ const initialState = ImmutableMap({
}),
});
export default function bookmaryCategoryEditorReducer(state = initialState, action) {
export default function bookmarkCategoryEditorReducer(state = initialState, action) {
switch(action.type) {
case BOOKMARK_CATEGORY_EDITOR_RESET:
return initialState;
case BOOKMARK_CATEGORY_EDITOR_SETUP:
return state.withMutations(map => {
map.set('bookmaryCategoryId', action.bookmaryCategory.get('id'));
map.set('title', action.bookmaryCategory.get('title'));
map.set('isExclusive', action.bookmaryCategory.get('is_exclusive'));
map.set('bookmarkCategoryId', action.bookmarkCategory.get('id'));
map.set('title', action.bookmarkCategory.get('title'));
map.set('isExclusive', action.bookmarkCategory.get('is_exclusive'));
map.set('isSubmitting', false);
});
case BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE:
@ -62,12 +60,8 @@ export default function bookmaryCategoryEditorReducer(state = initialState, acti
case BOOKMARK_CATEGORY_UPDATE_SUCCESS:
return state.withMutations(map => {
map.set('isSubmitting', false);
map.set('bookmaryCategoryId', action.bookmaryCategory.id);
map.set('bookmarkCategoryId', action.bookmarkCategory.id);
});
case BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS:
return state.updateIn(['accounts', 'items'], bookmaryCategory => bookmaryCategory.unshift(action.accountId));
case BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS:
return state.updateIn(['accounts', 'items'], bookmaryCategory => bookmaryCategory.filterNot(item => item === action.accountId));
default:
return state;
}

View file

@ -13,6 +13,7 @@ import antennaEditor from './antenna_editor';
import antennas from './antennas';
import blocks from './blocks';
import bookmark_categories from './bookmark_categories';
import bookmarkCategoryAdder from './bookmark_category_adder';
import bookmarkCategoryEditor from './bookmark_category_editor';
import boosts from './boosts';
import circleAdder from './circle_adder';
@ -93,6 +94,7 @@ const reducers = {
circleAdder,
bookmark_categories,
bookmarkCategoryEditor,
bookmarkCategoryAdder,
filters,
conversations,
suggestions,

View file

@ -4,6 +4,9 @@ import {
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
} from '../actions/accounts';
import {
BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS,
} from '../actions/bookmark_categories';
import {
BOOKMARKED_STATUSES_FETCH_REQUEST,
BOOKMARKED_STATUSES_FETCH_SUCCESS,
@ -98,11 +101,15 @@ const appendToList = (state, listType, statuses, next) => {
};
const prependOneToList = (state, listType, status) => {
return prependOneToListById(state, listType, status.get('id'));
};
const prependOneToListById = (state, listType, statusId) => {
return state.updateIn([listType, 'items'], (list) => {
if (list.includes(status.get('id'))) {
if (list.includes(statusId)) {
return list;
} else {
return ImmutableOrderedSet([status.get('id')]).union(list);
return ImmutableOrderedSet([statusId]).union(list);
}
});
};
@ -163,6 +170,8 @@ export default function statusLists(state = initialState, action) {
return removeOneFromList(state, 'emoji_reactions', action.status);
case BOOKMARK_SUCCESS:
return prependOneToList(state, 'bookmarks', action.status);
case BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS:
return prependOneToListById(state, 'bookmarks', action.statusId);
case UNBOOKMARK_SUCCESS:
return removeOneFromList(state, 'bookmarks', action.status);
case PINNED_STATUSES_FETCH_SUCCESS:

View file

@ -1,5 +1,9 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import {
BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST,
BOOKMARK_CATEGORY_EDITOR_ADD_FAIL,
} from '../actions/bookmark_categories';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { normalizeStatusTranslation } from '../actions/importer/normalizer';
import {
@ -111,6 +115,10 @@ export default function statuses(state = initialState, action) {
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], true);
case BOOKMARK_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false);
case BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST:
return state.get(action.statusId) === undefined ? state : state.setIn([action.statusId, 'bookmarked'], true);
case BOOKMARK_CATEGORY_EDITOR_ADD_FAIL:
return state.get(action.statusId) === undefined ? state : state.setIn([action.statusId, 'bookmarked'], false);
case UNBOOKMARK_REQUEST:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false);
case UNBOOKMARK_FAIL: