Skip to content

Commit

Permalink
Merge branch 'master' of github.com:tootsuite/mastodon
Browse files Browse the repository at this point in the history
  • Loading branch information
yi0713 committed Nov 6, 2018
2 parents b203c96 + 535ce84 commit 7201287
Show file tree
Hide file tree
Showing 83 changed files with 1,044 additions and 88 deletions.
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.7)
active_model_serializers (0.10.8)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
Expand Down Expand Up @@ -421,7 +421,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.0.5)
rack-attack (5.4.1)
rack-attack (5.4.2)
rack (>= 1.0, < 3)
rack-cors (1.0.2)
rack-protection (2.0.4)
Expand Down Expand Up @@ -473,7 +473,7 @@ GEM
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3)
rdf (>= 2.2, < 4.0)
redis (4.0.2)
redis (4.0.3)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/timelines/tag_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def tagged_statuses
end

def tag_timeline_statuses
Status.as_tag_timeline(@tag, current_account, truthy_param?(:local))
HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
end

def insert_pagination_headers
Expand Down
7 changes: 4 additions & 3 deletions app/controllers/tags_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ def show
end

format.rss do
@statuses = Status.as_tag_timeline(@tag).limit(PAGE_SIZE)
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status)

render xml: RSS::TagSerializer.render(@tag, @statuses)
end

format.json do
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local])
.paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status)

render json: collection_presenter,
Expand All @@ -46,7 +47,7 @@ def set_instance_presenter

def collection_presenter
ActivityPub::CollectionPresenter.new(
id: tag_url(@tag),
id: tag_url(@tag, params.slice(:any, :all, :none)),
type: :ordered,
size: @tag.statuses.count,
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
Expand Down
54 changes: 54 additions & 0 deletions app/javascript/mastodon/actions/lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ 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) => {
if (getState().getIn(['lists', id])) {
return;
Expand Down Expand Up @@ -316,3 +323,50 @@ export const removeFromListFail = (listId, accountId, error) => ({
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, getState) => {
dispatch(fetchAccountListsRequest(accountId));

api(getState).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'])));
};

6 changes: 3 additions & 3 deletions app/javascript/mastodon/actions/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getLocale } from '../locales';

const { messages } = getLocale();

export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
export function connectTimelineStream (timelineId, path, pollingRefresh = null, accept = null) {

return connectStream (path, pollingRefresh, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
Expand All @@ -24,7 +24,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
Expand All @@ -51,6 +51,6 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
export const connectHashtagStream = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
29 changes: 27 additions & 2 deletions app/javascript/mastodon/actions/timelines.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';

export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';

export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
Expand All @@ -13,10 +14,14 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';

export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';

export function updateTimeline(timeline, status) {
export function updateTimeline(timeline, status, accept) {
return (dispatch, getState) => {
const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];

if (typeof accept === 'function' && !accept(status)) {
return;
}

dispatch(importFetchedStatus(status));

dispatch({
Expand Down Expand Up @@ -44,8 +49,20 @@ export function deleteFromTimelines(id) {
};
};

export function clearTimeline(timeline) {
return (dispatch) => {
dispatch({ type: TIMELINE_CLEAR, timeline });
};
};

const noOp = () => {};

const parseTags = (tags = {}, mode) => {
return (tags[mode] || []).map((tag) => {
return tag.value;
});
};

export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
Expand Down Expand Up @@ -79,9 +96,17 @@ export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done =
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
any: parseTags(tags, 'any'),
all: parseTags(tags, 'all'),
none: parseTags(tags, 'none'),
}, done);
};

export function expandTimelineRequest(timeline) {
return {
type: TIMELINE_EXPAND_REQUEST,
Expand Down
50 changes: 48 additions & 2 deletions app/javascript/mastodon/components/scrollable_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { List as ImmutableList } from 'immutable';
import classNames from 'classnames';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';

const MOUSE_IDLE_DELAY = 300;

export default class ScrollableList extends PureComponent {

static contextTypes = {
Expand Down Expand Up @@ -37,6 +39,8 @@ export default class ScrollableList extends PureComponent {

state = {
fullscreen: null,
mouseMovedRecently: false,
scrollToTopOnMouseIdle: false,
};

intersectionObserverWrapper = new IntersectionObserverWrapper();
Expand All @@ -60,6 +64,47 @@ export default class ScrollableList extends PureComponent {
trailing: true,
});

mouseIdleTimer = null;

clearMouseIdleTimer = () => {
if (this.mouseIdleTimer === null) {
return;
}
clearTimeout(this.mouseIdleTimer);
this.mouseIdleTimer = null;
};

handleMouseMove = throttle(() => {
// As long as the mouse keeps moving, clear and restart the idle timer.
this.clearMouseIdleTimer();
this.mouseIdleTimer =
setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);

this.setState(({
mouseMovedRecently,
scrollToTopOnMouseIdle,
}) => ({
mouseMovedRecently: true,
// Only set scrollToTopOnMouseIdle if we just started moving and were
// scrolled to the top. Otherwise, just retain the previous state.
scrollToTopOnMouseIdle:
mouseMovedRecently
? scrollToTopOnMouseIdle
: (this.node.scrollTop === 0),
}));
}, MOUSE_IDLE_DELAY / 2);

handleMouseIdle = () => {
if (this.state.scrollToTopOnMouseIdle) {
this.node.scrollTop = 0;
this.props.onScrollToTop();
}
this.setState({
mouseMovedRecently: false,
scrollToTopOnMouseIdle: false,
});
}

componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
Expand All @@ -73,7 +118,7 @@ export default class ScrollableList extends PureComponent {
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
if (someItemInserted && this.node.scrollTop > 0) {
if ((someItemInserted && this.node.scrollTop > 0) || this.state.mouseMovedRecently) {
return this.node.scrollHeight - this.node.scrollTop;
} else {
return null;
Expand All @@ -93,6 +138,7 @@ export default class ScrollableList extends PureComponent {
}

componentWillUnmount () {
this.clearMouseIdleTimer();
this.detachScrollListener();
this.detachIntersectionObserver();
detachFullscreenListener(this.onFullScreenChange);
Expand Down Expand Up @@ -151,7 +197,7 @@ export default class ScrollableList extends PureComponent {

if (isLoading || childrenCount > 0 || !emptyMessage) {
scrollableArea = (
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
<div role='feed' className='item-list'>
{prepend}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const messages = defineMessages({
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
});

export default @injectIntl
Expand All @@ -51,6 +52,7 @@ class ActionBar extends React.PureComponent {
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};

Expand Down Expand Up @@ -105,6 +107,7 @@ class ActionBar extends React.PureComponent {
}

menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent {
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
};

Expand Down Expand Up @@ -78,6 +79,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onEndorseToggle(this.props.account);
}

handleAddToList = () => {
this.props.onAddToList(this.props.account);
}

render () {
const { account, hideTabs } = this.props;

Expand Down Expand Up @@ -106,6 +111,7 @@ export default class Header extends ImmutablePureComponent {
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
/>

{!hideTabs && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(unblockDomain(domain));
},

onAddToList(account){
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},

});

export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
Loading

0 comments on commit 7201287

Please sign in to comment.