Add favourites list
This commit is contained in:
parent
b290979e82
commit
33785332f1
25 changed files with 364 additions and 71 deletions
|
@ -8,10 +8,18 @@ class Api::V1::ListsController < Api::BaseController
|
|||
before_action :set_list, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@lists = List.where(account: current_account).all
|
||||
@lists = load_lists.all
|
||||
render json: @lists, each_serializer: REST::ListSerializer
|
||||
end
|
||||
|
||||
def load_lists
|
||||
if list_params[:favourite]
|
||||
List.where(account: current_account).where(favourite: true)
|
||||
else
|
||||
List.where(account: current_account)
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @list, serializer: REST::ListSerializer
|
||||
end
|
||||
|
@ -31,6 +39,16 @@ class Api::V1::ListsController < Api::BaseController
|
|||
render_empty
|
||||
end
|
||||
|
||||
def favourite
|
||||
@list.favourite!
|
||||
render json: @list, serializer: REST::ListSerializer
|
||||
end
|
||||
|
||||
def unfavourite
|
||||
@list.unfavourite!
|
||||
render json: @list, serializer: REST::ListSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_list
|
||||
|
@ -38,6 +56,6 @@ class Api::V1::ListsController < Api::BaseController
|
|||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:title, :replies_policy)
|
||||
params.permit(:title, :replies_policy, :favourite)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,14 @@ export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
|||
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
||||
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
||||
|
||||
export const LIST_FAVOURITE_REQUEST = 'LIST_FAVOURITE_REQUEST';
|
||||
export const LIST_FAVOURITE_SUCCESS = 'LIST_FAVOURITE_SUCCESS';
|
||||
export const LIST_FAVOURITE_FAIL = 'LIST_FAVOURITE_FAIL';
|
||||
|
||||
export const LIST_UNFAVOURITE_REQUEST = 'LIST_UNFAVOURITE_REQUEST';
|
||||
export const LIST_UNFAVOURITE_SUCCESS = 'LIST_UNFAVOURITE_SUCCESS';
|
||||
export const LIST_UNFAVOURITE_FAIL = 'LIST_UNFAVOURITE_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';
|
||||
|
@ -206,6 +214,54 @@ export const deleteListFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const favouriteList = id => (dispatch, getState) => {
|
||||
dispatch(favouriteListRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/lists/${id}/favourite`)
|
||||
.then(({ data }) => dispatch(favouriteListSuccess(data)))
|
||||
.catch(err => dispatch(favouriteListFail(id, err)));
|
||||
};
|
||||
|
||||
export const favouriteListRequest = id => ({
|
||||
type: LIST_FAVOURITE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const favouriteListSuccess = list => ({
|
||||
type: LIST_FAVOURITE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const favouriteListFail = (id, error) => ({
|
||||
type: LIST_FAVOURITE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const unfavouriteList = id => (dispatch, getState) => {
|
||||
dispatch(unfavouriteListRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/lists/${id}/unfavourite`)
|
||||
.then(({ data }) => dispatch(unfavouriteListSuccess(data)))
|
||||
.catch(err => dispatch(unfavouriteListFail(id, err)));
|
||||
};
|
||||
|
||||
export const unfavouriteListRequest = id => ({
|
||||
type: LIST_UNFAVOURITE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const unfavouriteListSuccess = list => ({
|
||||
type: LIST_UNFAVOURITE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const unfavouriteListFail = (id, error) => ({
|
||||
type: LIST_UNFAVOURITE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchListAccounts = listId => (dispatch, getState) => {
|
||||
dispatch(fetchListAccountsRequest(listId));
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Iterable, fromJS } from 'immutable';
|
||||
import { hydrateCompose } from './compose';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { fetchListsSuccess } from './lists';
|
||||
|
||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||
|
@ -20,5 +21,6 @@ export function hydrateStore(rawState) {
|
|||
|
||||
dispatch(hydrateCompose());
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
dispatch(fetchListsSuccess(Object.values(rawState.lists)));
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,11 @@ export default class IntersectionObserverArticle extends React.Component {
|
|||
cachedHeight: PropTypes.number,
|
||||
onHeightChange: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
tabIndex: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
tabIndex: '0',
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -102,7 +107,7 @@ export default class IntersectionObserverArticle extends React.Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { children, id, index, listLength, cachedHeight } = this.props;
|
||||
const { children, id, index, listLength, cachedHeight, tabIndex } = this.props;
|
||||
const { isIntersecting, isHidden } = this.state;
|
||||
|
||||
if (!isIntersecting && (isHidden || cachedHeight)) {
|
||||
|
@ -113,7 +118,7 @@ export default class IntersectionObserverArticle extends React.Component {
|
|||
aria-setsize={listLength}
|
||||
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
|
||||
data-id={id}
|
||||
tabIndex='0'
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{children && React.cloneElement(children, { hidden: true })}
|
||||
</article>
|
||||
|
@ -121,7 +126,7 @@ export default class IntersectionObserverArticle extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
|
||||
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={tabIndex}>
|
||||
{children && React.cloneElement(children, { hidden: false })}
|
||||
</article>
|
||||
);
|
||||
|
|
|
@ -45,10 +45,12 @@ class ScrollableList extends PureComponent {
|
|||
children: PropTypes.node,
|
||||
bindToDocument: PropTypes.bool,
|
||||
preventScroll: PropTypes.bool,
|
||||
tabIndex: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
trackScroll: true,
|
||||
tabIndex: '0',
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -289,7 +291,7 @@ class ScrollableList extends PureComponent {
|
|||
}
|
||||
|
||||
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, emptyMessage, onLoadMore, tabIndex } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
const childrenCount = React.Children.count(children);
|
||||
|
||||
|
@ -325,6 +327,7 @@ class ScrollableList extends PureComponent {
|
|||
listLength={childrenCount}
|
||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{React.cloneElement(child, {
|
||||
getScrollPosition: this.getScrollPosition,
|
||||
|
|
|
@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, profile_directory, showTrends, enable_limited_timeline } from '../../initial_state';
|
||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { fetchFavouriteDomains } from 'mastodon/actions/favourite_domains';
|
||||
import { fetchFavouriteTags } from 'mastodon/actions/favourite_tags';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
@ -49,7 +48,7 @@ const messages = defineMessages({
|
|||
trends: { id: 'navigation_bar.trends', defaultMessage: 'Trends' },
|
||||
information_acct: { id: 'navigation_bar.information_acct', defaultMessage: 'Fedibird info' },
|
||||
hashtag_fedibird: { id: 'navigation_bar.hashtag_fedibird', defaultMessage: 'fedibird' },
|
||||
lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
|
||||
lists_subheading: { id: 'column_subheading.favourite_lists', defaultMessage: 'Favourite Lists' },
|
||||
favourite_domains_subheading: { id: 'column_subheading.favourite_domains', defaultMessage: 'Favourite domains' },
|
||||
favourite_tags_subheading: { id: 'column_subheading.favourite_tags', defaultMessage: 'Favourite tags' },
|
||||
});
|
||||
|
@ -65,7 +64,6 @@ const mapStateToProps = state => ({
|
|||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
|
||||
fetchLists: () => dispatch(fetchLists()),
|
||||
fetchFavouriteDomains: () => dispatch(fetchFavouriteDomains()),
|
||||
fetchFavouriteTags: () => dispatch(fetchFavouriteTags()),
|
||||
});
|
||||
|
@ -96,7 +94,6 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
columns: ImmutablePropTypes.list,
|
||||
multiColumn: PropTypes.bool,
|
||||
fetchFollowRequests: PropTypes.func.isRequired,
|
||||
fetchLists: PropTypes.func.isRequired,
|
||||
fetchFavouriteDomains: PropTypes.func.isRequired,
|
||||
fetchFavouriteTags: PropTypes.func.isRequired,
|
||||
unreadFollowRequests: PropTypes.number,
|
||||
|
@ -107,7 +104,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { fetchFollowRequests, fetchLists, fetchFavouriteDomains, fetchFavouriteTags, multiColumn } = this.props;
|
||||
const { fetchFollowRequests, fetchFavouriteDomains, fetchFavouriteTags, multiColumn } = this.props;
|
||||
|
||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||
this.context.router.history.replace('/timelines/home');
|
||||
|
@ -115,7 +112,6 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
fetchFollowRequests();
|
||||
fetchLists();
|
||||
fetchFavouriteDomains();
|
||||
fetchFavouriteTags();
|
||||
}
|
||||
|
@ -220,7 +216,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
<ColumnLink key='circles' icon='user-circle' text={intl.formatMessage(messages.circles)} to='/circles' />,
|
||||
);
|
||||
|
||||
height += 48*5;
|
||||
height += 48*6;
|
||||
|
||||
if (lists && !lists.isEmpty()) {
|
||||
navItems.push(<ColumnSubheading key='header-lists' text={intl.formatMessage(messages.lists_subheading)} />);
|
||||
|
|
79
app/javascript/mastodon/features/lists/components/list.js
Normal file
79
app/javascript/mastodon/features/lists/components/list.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../../../components/icon';
|
||||
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { favouriteList, unfavouriteList } from '../../../actions/lists';
|
||||
|
||||
const messages = defineMessages({
|
||||
favourite: { id: 'lists.favourite', defaultMessage: 'Favourite list' },
|
||||
edit: { id: 'lists.edit', defaultMessage: 'Edit list' },
|
||||
});
|
||||
|
||||
export default @connect()
|
||||
@injectIntl
|
||||
class List extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
favourite: PropTypes.bool.isRequired,
|
||||
animate: PropTypes.bool,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
animate: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
activate: false,
|
||||
deactivate: false,
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!nextProps.animate) return;
|
||||
|
||||
if (this.props.favourite && !nextProps.favourite) {
|
||||
this.setState({ activate: false, deactivate: true });
|
||||
} else if (!this.props.favourite && nextProps.favourite) {
|
||||
this.setState({ activate: true, deactivate: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.id }));
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
const { id, favourite, dispatch } = this.props;
|
||||
|
||||
if (favourite) {
|
||||
dispatch(unfavouriteList(id));
|
||||
} else {
|
||||
dispatch(favouriteList(id));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id, text, favourite, intl } = this.props;
|
||||
const { activate, deactivate } = this.state;
|
||||
|
||||
return (
|
||||
<div className='list-link'>
|
||||
<div className='list-name'><ColumnLink to={`/timelines/list/${id}`} icon='list-ul' text={text} /></div>
|
||||
<button className={classNames('list-favourite-button icon-button star-icon', {active: favourite, pressed: favourite, activate, deactivate})} title={intl.formatMessage(messages.favourite)} onClick={this.handleFavouriteClick}>
|
||||
<Icon id='star' className='column-link__icon' fixedWidth />
|
||||
</button>
|
||||
<button className='list-edit-button' title={intl.formatMessage(messages.edit)} onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' className='column-link__icon' fixedWidth />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -8,9 +8,9 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
|||
import { fetchLists } from '../../actions/lists';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ColumnLink from '../ui/components/column_link';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import NewListForm from './components/new_list_form';
|
||||
import List from './components/list';
|
||||
import { createSelector } from 'reselect';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
|
@ -71,9 +71,10 @@ class Lists extends ImmutablePureComponent {
|
|||
emptyMessage={emptyMessage}
|
||||
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
|
||||
bindToDocument={!multiColumn}
|
||||
tabIndex='-1'
|
||||
>
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
|
||||
<List key={list.get('id')} id={list.get('id')} text={list.get('title')} favourite={list.get('favourite')} animate />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
|
|
|
@ -83,6 +83,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
isModalOpen: PropTypes.bool.isRequired,
|
||||
singleColumn: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
favouriteLists: ImmutablePropTypes.list,
|
||||
links: PropTypes.node,
|
||||
};
|
||||
|
||||
|
@ -95,7 +96,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
componentWillReceiveProps() {
|
||||
if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getSwipeableIndex(this.context.router.history.location.pathname)) {
|
||||
if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getSwipeableIndex(this.props.favouriteLists, this.context.router.history.location.pathname)) {
|
||||
this.setState({ shouldAnimate: false });
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +117,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
this.setState({ renderComposePanel: !this.mediaQuery.matches });
|
||||
}
|
||||
|
||||
this.lastIndex = getSwipeableIndex(this.context.router.history.location.pathname);
|
||||
this.lastIndex = getSwipeableIndex(this.props.favouriteLists, this.context.router.history.location.pathname);
|
||||
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
|
||||
|
||||
this.setState({ shouldAnimate: true });
|
||||
|
@ -141,7 +142,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
|
||||
}
|
||||
|
||||
const newIndex = getSwipeableIndex(this.context.router.history.location.pathname);
|
||||
const newIndex = getSwipeableIndex(this.props.favouriteLists, this.context.router.history.location.pathname);
|
||||
|
||||
if (this.lastIndex !== newIndex) {
|
||||
this.lastIndex = newIndex;
|
||||
|
@ -187,14 +188,14 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
document.querySelector(nextLinkSelector).classList.add('active');
|
||||
|
||||
if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
|
||||
this.context.router.history.push(getSwipeableLink(this.pendingIndex));
|
||||
this.context.router.history.push(getSwipeableLink(this.props.favouriteLists, this.pendingIndex));
|
||||
this.pendingIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleAnimationEnd = () => {
|
||||
if (typeof this.pendingIndex === 'number') {
|
||||
this.context.router.history.push(getSwipeableLink(this.pendingIndex));
|
||||
this.context.router.history.push(getSwipeableLink(this.props.favouriteLists, this.pendingIndex));
|
||||
this.pendingIndex = null;
|
||||
}
|
||||
}
|
||||
|
@ -212,8 +213,8 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
renderView = (link, index) => {
|
||||
const columnIndex = getSwipeableIndex(this.context.router.history.location.pathname);
|
||||
const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] });
|
||||
const columnIndex = getSwipeableIndex(this.props.favouriteLists, this.context.router.history.location.pathname);
|
||||
const title = link.props['data-preview-title'] ?? this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] });
|
||||
const icon = link.props['data-preview-icon'];
|
||||
|
||||
const view = (index === columnIndex) ?
|
||||
|
@ -239,7 +240,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
const { columns, children, singleColumn, isModalOpen, links, intl } = this.props;
|
||||
const { shouldAnimate, renderComposePanel } = this.state;
|
||||
|
||||
const columnIndex = getSwipeableIndex(this.context.router.history.location.pathname);
|
||||
const columnIndex = getSwipeableIndex(this.props.favouriteLists, this.context.router.history.location.pathname);
|
||||
|
||||
if (singleColumn) {
|
||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className={classNames('floating-action-button', { 'bottom-bar': place_tab_bar_at_bottom })} aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePropTypes, { list } from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
|
@ -13,7 +12,7 @@ export const getOrderedLists = createSelector([state => state.get('lists')], lis
|
|||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(10);
|
||||
return lists.toList().filter(item => !!item && item.get('favourite')).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -29,11 +28,6 @@ class ListPanel extends ImmutablePureComponent {
|
|||
lists: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchLists());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { lists } = this.props;
|
||||
|
||||
|
|
|
@ -7,57 +7,90 @@ import { isUserTouching } from '../../../is_mobile';
|
|||
import Icon from 'mastodon/components/icon';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import { place_tab_bar_at_bottom, show_tab_bar_label, enable_limited_timeline } from 'mastodon/initial_state';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const links = [
|
||||
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.home' defaultMessage='Home' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link tabs-bar__limited' to='/timelines/limited' data-preview-title-id='column.limited' data-preview-icon='lock' ><Icon id='lock' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.limited_timeline' defaultMessage='Limited' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.limited_timeline' defaultMessage='Ltd.' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.notifications' defaultMessage='Notif.' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.public_timeline' defaultMessage='FTL' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/lists' data-preview-title-id='column.lists' data-preview-icon='list-ul' ><Icon id='list-ul' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.lists' defaultMessage='Lists' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.lists' defaultMessage='List' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.search' defaultMessage='Search' /></span></NavLink>,
|
||||
// <a className='tabs-bar__external-link' href='/settings/preferences' to='/settings/preferences' data-preview-title-id='navigation_bar.preferences' data-preview-icon='cog' ><Icon id='cog' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.preferences' defaultMessage='Pref.' /></span></a>,
|
||||
// <a className='tabs-bar__external-link' href='/auth/sign_out' to='/auth/sign_out' data-method='delete' data-preview-title-id='navigation_bar.logout' data-preview-icon='sign-out' ><Icon id='sign-out' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.logout' defaultMessage='Logout' /></span></a>,
|
||||
<NavLink className='tabs-bar__link hamburger' to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
||||
];
|
||||
const link_home = <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.home' defaultMessage='Home' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.home' defaultMessage='Home' children={msg=> <>{msg}</>} /></span></NavLink>;
|
||||
const link_limited = <NavLink className='tabs-bar__link tabs-bar__limited' to='/timelines/limited' data-preview-title-id='column.limited' data-preview-icon='lock' ><Icon id='lock' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.limited_timeline' defaultMessage='Limited' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.limited_timeline' defaultMessage='Ltd.' children={msg=> <>{msg}</>} /></span></NavLink>;
|
||||
const link_notifications = <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.notifications' defaultMessage='Notif.' children={msg=> <>{msg}</>} /></span></NavLink>;
|
||||
const link_public = <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.public_timeline' defaultMessage='FTL' children={msg=> <>{msg}</>} /></span></NavLink>;
|
||||
// const link_lists = <NavLink className='tabs-bar__link' exact to='/lists' data-preview-title-id='column.lists' data-preview-icon='list-ul' ><Icon id='list-ul' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.lists' defaultMessage='Lists' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.lists' defaultMessage='List' children={msg=> <>{msg}</>} /></span></NavLink>;
|
||||
const link_search = <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.search' defaultMessage='Search' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.search' defaultMessage='Search' children={msg=> <>{msg}</>} /></span></NavLink>;
|
||||
// const link_preferences = <a className='tabs-bar__external-link' href='/settings/preferences' to='/settings/preferences' data-preview-title-id='navigation_bar.preferences' data-preview-icon='cog' ><Icon id='cog' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.preferences' defaultMessage='Pref.' children={msg=> <>{msg}</>} /></span></a>;
|
||||
// const link_sign_out = <a className='tabs-bar__external-link' href='/auth/sign_out' to='/auth/sign_out' data-method='delete' data-preview-title-id='navigation_bar.logout' data-preview-icon='sign-out' ><Icon id='sign-out' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' children={msg=> <>{msg}</>} /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.logout' defaultMessage='Logout' children={msg=> <>{msg}</>} /></span></a>;
|
||||
const link_started = <NavLink className='tabs-bar__link hamburger' to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>;
|
||||
|
||||
export const getLinks = memoize(() => {
|
||||
return links.filter(link => {
|
||||
const classes = link.props.className.split(/\s+/);
|
||||
return !(!enable_limited_timeline && classes.includes('tabs-bar__limited'));
|
||||
});
|
||||
export const getLinks = memoize((favouriteLists = null) => {
|
||||
const link_favourite_lists = favouriteLists ? favouriteLists.map(list => {
|
||||
if (!list.get('favourite')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink className='tabs-bar__link' exact to={`/timelines/list/${list.get('id')}`} data-preview-title={list.get('title')} data-preview-title-id={`list/${list.get('id')}`} data-preview-icon='list-ul' >
|
||||
<Icon id='list-ul' fixedWidth /><span className='tabs-bar__link__full-label'>{list.get('title')}</span>
|
||||
<span className='tabs-bar__link__short-label'>{list.get('title')}</span>
|
||||
</NavLink>
|
||||
);
|
||||
}).toArray() : null;
|
||||
|
||||
return [
|
||||
link_home,
|
||||
enable_limited_timeline ? link_limited : null,
|
||||
link_favourite_lists,
|
||||
link_notifications,
|
||||
link_public,
|
||||
link_search,
|
||||
link_started,
|
||||
].flat().filter(v => !!v);
|
||||
});
|
||||
|
||||
export const getSwipeableLinks = memoize(() => {
|
||||
return getLinks().filter(link => {
|
||||
export const getSwipeableLinks = memoize((favouriteLists = null) => {
|
||||
return getLinks(favouriteLists).filter(link => {
|
||||
const classes = link.props.className.split(/\s+/);
|
||||
return classes.includes('tabs-bar__link');
|
||||
});
|
||||
});
|
||||
|
||||
export function getSwipeableIndex (path) {
|
||||
return getSwipeableLinks().findIndex(link => link.props.to === path);
|
||||
export function getSwipeableIndex (favouriteLists = null, path) {
|
||||
return getSwipeableLinks(favouriteLists).findIndex(link => link.props.to === path);
|
||||
}
|
||||
|
||||
export function getSwipeableLink (index) {
|
||||
return getSwipeableLinks()[index].props.to;
|
||||
export function getSwipeableLink (favouriteLists = null, index) {
|
||||
return getSwipeableLinks(favouriteLists)[index].props.to;
|
||||
}
|
||||
|
||||
export function getIndex (path) {
|
||||
return getLinks().findIndex(link => link.props.to === path);
|
||||
export function getIndex (favouriteLists = null, path) {
|
||||
return getLinks(favouriteLists).findIndex(link => link.props.to === path);
|
||||
}
|
||||
|
||||
export function getLink (index) {
|
||||
return getLinks()[index].props.to;
|
||||
export function getLink (favouriteLists = null, index) {
|
||||
return getLinks(favouriteLists)[index].props.to;
|
||||
}
|
||||
|
||||
export const getFavouriteOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item && item.get('favourite')).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
favouriteLists: getFavouriteOrderedLists(state),
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@withRouter
|
||||
@connect(mapStateToProps)
|
||||
class TabsBar extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
favouriteLists: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
setRef = ref => {
|
||||
|
@ -65,6 +98,8 @@ class TabsBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const { favouriteLists } = this.props;
|
||||
|
||||
// Only apply optimization for touch devices, which we assume are slower
|
||||
// We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
|
||||
if (isUserTouching()) {
|
||||
|
@ -75,7 +110,7 @@ class TabsBar extends React.PureComponent {
|
|||
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
|
||||
const currentTab = tabs.find(tab => tab.classList.contains('active'));
|
||||
const nextTab = tabs.find(tab => tab.contains(e.target));
|
||||
const { props: { to } } = getLinks()[Array(...this.node.childNodes).indexOf(nextTab)];
|
||||
const { props: { to } } = getLinks(favouriteLists)[Array(...this.node.childNodes).indexOf(nextTab)];
|
||||
|
||||
|
||||
if (currentTab !== nextTab) {
|
||||
|
@ -97,12 +132,12 @@ class TabsBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
const { intl: { formatMessage }, favouriteLists } = this.props;
|
||||
|
||||
return (
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<nav className={classNames('tabs-bar', { 'bottom-bar': place_tab_bar_at_bottom })} ref={this.setRef}>
|
||||
{getLinks().map(link => React.cloneElement(link, { key: link.props.to, className: classNames(link.props.className, { 'short-label': show_tab_bar_label }), onClick: link.props.href ? null : this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
||||
{getLinks(favouriteLists).map(link => React.cloneElement(link, { key: link.props.to, className: classNames(link.props.className, { 'short-label': show_tab_bar_label }), onClick: link.props.href ? null : this.handleClick, 'aria-label': link.props['data-preview-title'] ?? formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
||||
</nav>
|
||||
|
||||
<div id='tabs-bar__portal' />
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ColumnsArea from '../components/columns_area';
|
||||
import { getSwipeableLinks } from '../components/tabs_bar';
|
||||
import { getSwipeableLinks, getFavouriteOrderedLists } from '../components/tabs_bar';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
isModalOpen: !!state.get('modal').modalType,
|
||||
links: getSwipeableLinks(),
|
||||
});
|
||||
const mapStateToProps = state => {
|
||||
const favouriteLists = getFavouriteOrderedLists(state)
|
||||
|
||||
return {
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
isModalOpen: !!state.get('modal').modalType,
|
||||
favouriteLists: favouriteLists,
|
||||
links: getSwipeableLinks(favouriteLists),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, null, null, { forwardRef: true })(ColumnsArea);
|
||||
|
|
|
@ -109,8 +109,8 @@
|
|||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
"column_subheading.favourite_domains": "Favourite domains",
|
||||
"column_subheading.favourite_lists": "Favourite lists",
|
||||
"column_subheading.favourite_tags": "Favourite tags",
|
||||
"column_subheading.lists": "Lists",
|
||||
"column_subheading.settings": "Settings",
|
||||
"community.column_settings.local_only": "Local only",
|
||||
"community.column_settings.media_only": "Media Only",
|
||||
|
@ -315,6 +315,7 @@
|
|||
"lists.delete": "Delete list",
|
||||
"lists.edit": "Edit list",
|
||||
"lists.edit.submit": "Change title",
|
||||
"lists.favourite": "Favourite list",
|
||||
"lists.new.create": "Add list",
|
||||
"lists.new.title_placeholder": "New list title",
|
||||
"lists.replies_policy.followed": "Any followed user",
|
||||
|
|
|
@ -109,8 +109,8 @@
|
|||
"column_header.show_settings": "設定を表示",
|
||||
"column_header.unpin": "ピン留めを外す",
|
||||
"column_subheading.favourite_domains": "お気に入りドメイン",
|
||||
"column_subheading.favourite_lists": "お気に入りリスト",
|
||||
"column_subheading.favourite_tags": "お気に入りハッシュタグ",
|
||||
"column_subheading.lists": "リスト",
|
||||
"column_subheading.settings": "設定",
|
||||
"community.column_settings.local_only": "ローカルのみ表示",
|
||||
"community.column_settings.media_only": "メディアのみ表示",
|
||||
|
@ -316,6 +316,7 @@
|
|||
"lists.delete": "リストを削除",
|
||||
"lists.edit": "リストを編集",
|
||||
"lists.edit.submit": "タイトルを変更",
|
||||
"lists.favourite": "リストをお気に入り",
|
||||
"lists.new.create": "リストを作成",
|
||||
"lists.new.title_placeholder": "新規リスト名",
|
||||
"lists.replies_policy.followed": "フォロー中のユーザー全員",
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
LIST_CREATE_SUCCESS,
|
||||
LIST_UPDATE_SUCCESS,
|
||||
LIST_DELETE_SUCCESS,
|
||||
LIST_FAVOURITE_SUCCESS,
|
||||
LIST_UNFAVOURITE_SUCCESS,
|
||||
} from '../actions/lists';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
|
@ -25,6 +27,8 @@ export default function lists(state = initialState, action) {
|
|||
case LIST_FETCH_SUCCESS:
|
||||
case LIST_CREATE_SUCCESS:
|
||||
case LIST_UPDATE_SUCCESS:
|
||||
case LIST_FAVOURITE_SUCCESS:
|
||||
case LIST_UNFAVOURITE_SUCCESS:
|
||||
return normalizeList(state, action.list);
|
||||
case LISTS_FETCH_SUCCESS:
|
||||
return normalizeLists(state, action.lists);
|
||||
|
|
|
@ -121,6 +121,8 @@ html {
|
|||
.getting-started,
|
||||
.scrollable {
|
||||
.column-link,
|
||||
.list-favourite-button,
|
||||
.list-edit-button,
|
||||
.circle-edit-button,
|
||||
.circle-delete-button {
|
||||
background: $white;
|
||||
|
|
|
@ -6828,6 +6828,48 @@ noscript {
|
|||
background: rgba($base-overlay-background, 0.5);
|
||||
}
|
||||
|
||||
.list-link {
|
||||
display: flex;
|
||||
|
||||
.list-name {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.list-favourite-button,
|
||||
.list-edit-button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.list-name,
|
||||
.list-favourite-button,
|
||||
.list-edit-button {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
color: $primary-text-color;
|
||||
// padding: 15px;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 11%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border-radius: 0;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-editor {
|
||||
background: $ui-base-color;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -486,6 +486,12 @@ body.rtl {
|
|||
left: 20px;
|
||||
}
|
||||
|
||||
.list-link .list-name,
|
||||
.list-link .list-favourite-button,
|
||||
.list-link .list-edit-button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.circle-dropdown .circle-dropdown__menu {
|
||||
background-position: left 8px center;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# replies_policy :integer default("list"), not null
|
||||
# favourite :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class List < ApplicationRecord
|
||||
|
@ -40,6 +41,20 @@ class List < ApplicationRecord
|
|||
|
||||
before_destroy :clean_feed_manager
|
||||
|
||||
scope :favourites, -> { where(favourite: true) }
|
||||
|
||||
def favourite?
|
||||
favourite
|
||||
end
|
||||
|
||||
def favourite!
|
||||
update!(favourite: true)
|
||||
end
|
||||
|
||||
def unfavourite!
|
||||
update!(favourite: false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_feed_manager
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InitialStateSerializer < ActiveModel::Serializer
|
||||
attributes :meta, :compose, :accounts,
|
||||
attributes :meta, :compose, :accounts, :lists,
|
||||
:media_attachments, :settings
|
||||
|
||||
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
|
@ -87,6 +87,11 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
store
|
||||
end
|
||||
|
||||
def lists
|
||||
store = ActiveModelSerializers::SerializableResource.new(object.current_account.owned_lists, each_serializer: REST::ListSerializer) if object.current_account
|
||||
store
|
||||
end
|
||||
|
||||
def media_attachments
|
||||
{ accept_content_types: MediaAttachment.supported_file_extensions + MediaAttachment.supported_mime_types }
|
||||
end
|
||||
|
|
|
@ -105,6 +105,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
[
|
||||
:favourite_hashtag,
|
||||
:favourite_domain,
|
||||
:favourite_list,
|
||||
:status_expire,
|
||||
:follow_no_delivery,
|
||||
:follow_hashtag,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::ListSerializer < ActiveModel::Serializer
|
||||
attributes :id, :title, :replies_policy
|
||||
attributes :id, :title, :replies_policy, :favourite
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
|
|
|
@ -493,7 +493,12 @@ Rails.application.routes.draw do
|
|||
resources :lists, only: [:index, :create, :show, :update, :destroy] do
|
||||
resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts'
|
||||
resource :subscribes, only: [:show, :create, :destroy], controller: 'lists/subscribes'
|
||||
end
|
||||
|
||||
member do
|
||||
post :favourite
|
||||
post :unfavourite
|
||||
end
|
||||
end
|
||||
|
||||
resources :circles, only: [:index, :create, :show, :update, :destroy] do
|
||||
resource :accounts, only: [:show, :create, :destroy], controller: 'circles/accounts'
|
||||
|
|
15
db/migrate/20210621035518_add_favourite_to_lists.rb
Normal file
15
db/migrate/20210621035518_add_favourite_to_lists.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||
|
||||
class AddFavouriteToLists < ActiveRecord::Migration[6.1]
|
||||
include Mastodon::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
safety_assured { add_column_with_default :lists, :favourite, :boolean, default: false, allow_null: false }
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :lists, :favourite
|
||||
end
|
||||
end
|
|
@ -641,6 +641,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
|
|||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "replies_policy", default: 0, null: false
|
||||
t.boolean "favourite", default: false, null: false
|
||||
t.index ["account_id"], name: "index_lists_on_account_id"
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue