diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 06a19afc3..5640201c6 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -26,8 +26,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
-export const STATUS_REVEAL = 'STATUS_REVEAL';
-export const STATUS_HIDE = 'STATUS_HIDE';
+export const STATUS_REVEAL = 'STATUS_REVEAL';
+export const STATUS_HIDE = 'STATUS_HIDE';
+export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
export const REDRAFT = 'REDRAFT';
@@ -320,3 +321,11 @@ export function revealStatus(ids) {
ids,
};
};
+
+export function toggleStatusCollapse(id, isCollapsed) {
+ return {
+ type: STATUS_COLLAPSE,
+ id,
+ isCollapsed,
+ };
+}
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index bc2ac5e82..1a634b55d 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -17,6 +17,14 @@ export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
+export const CURRENTLY_VIEWING = 'CURRENTLY_VIEWING';
+
+export const updateCurrentlyViewing = (timeline, id) => ({
+ type: CURRENTLY_VIEWING,
+ timeline,
+ id,
+});
+
export const loadPending = timeline => ({
type: TIMELINE_LOAD_PENDING,
timeline,
diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js
index e453730ba..d475e5d1c 100644
--- a/app/javascript/mastodon/components/intersection_observer_article.js
+++ b/app/javascript/mastodon/components/intersection_observer_article.js
@@ -20,6 +20,8 @@ export default class IntersectionObserverArticle extends React.Component {
cachedHeight: PropTypes.number,
onHeightChange: PropTypes.func,
children: PropTypes.node,
+ currentlyViewing: PropTypes.number,
+ updateCurrentlyViewing: PropTypes.func,
};
state = {
@@ -48,6 +50,8 @@ export default class IntersectionObserverArticle extends React.Component {
);
this.componentMounted = true;
+
+ if(id === this.props.currentlyViewing) this.node.scrollIntoView();
}
componentWillUnmount () {
@@ -60,6 +64,8 @@ export default class IntersectionObserverArticle extends React.Component {
handleIntersection = (entry) => {
this.entry = entry;
+ if(entry.intersectionRatio > 0.75 && this.props.updateCurrentlyViewing) this.props.updateCurrentlyViewing(this.id);
+
scheduleIdleTask(this.calculateHeight);
this.setState(this.updateStateAfterIntersection);
}
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 421756803..6338ccd5c 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -36,6 +36,8 @@ export default class ScrollableList extends PureComponent {
emptyMessage: PropTypes.node,
children: PropTypes.node,
bindToDocument: PropTypes.bool,
+ currentlyViewing: PropTypes.number,
+ updateCurrentlyViewing: PropTypes.func,
};
static defaultProps = {
@@ -309,6 +311,8 @@ export default class ScrollableList extends PureComponent {
listLength={childrenCount}
intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
+ currentlyViewing={this.props.currentlyViewing}
+ updateCurrentlyViewing={this.props.updateCurrentlyViewing}
>
{React.cloneElement(child, {
getScrollPosition: this.getScrollPosition,
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index e120278a0..12fc4a9a6 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -76,6 +76,7 @@ class Status extends ImmutablePureComponent {
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
+ onToggleCollapsed: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
@@ -107,14 +108,6 @@ class Status extends ImmutablePureComponent {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
}
- getSnapshotBeforeUpdate () {
- if (this.props.getScrollPosition) {
- return this.props.getScrollPosition();
- } else {
- return null;
- }
- }
-
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
@@ -141,17 +134,6 @@ class Status extends ImmutablePureComponent {
}
}
- componentWillUnmount() {
- if (this.node && this.props.getScrollPosition) {
- const position = this.props.getScrollPosition();
- if (position !== null && this.node.offsetTop < position.top) {
- requestAnimationFrame(() => {
- this.props.updateScrollBottom(position.height - position.top);
- });
- }
- }
- }
-
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
@@ -196,7 +178,11 @@ class Status extends ImmutablePureComponent {
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
- };
+ }
+
+ handleCollapsedToggle = isCollapsed => {
+ this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
+ }
renderLoadingMediaGallery () {
return
;
@@ -466,7 +452,7 @@ class Status extends ImmutablePureComponent {
-
+
{media}
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index d13091325..5d921fd41 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -23,11 +23,11 @@ export default class StatusContent extends React.PureComponent {
onExpandedToggle: PropTypes.func,
onClick: PropTypes.func,
collapsable: PropTypes.bool,
+ onCollapsedToggle: PropTypes.func,
};
state = {
hidden: true,
- collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
};
_updateStatusLinks () {
@@ -62,14 +62,16 @@ export default class StatusContent extends React.PureComponent {
link.setAttribute('rel', 'noopener noreferrer');
}
- if (
- this.props.collapsable
- && this.props.onClick
- && this.state.collapsed === null
- && node.clientHeight > MAX_HEIGHT
- && this.props.status.get('spoiler_text').length === 0
- ) {
- this.setState({ collapsed: true });
+ if (this.props.status.get('collapsed', null) === null) {
+ let collapsed =
+ this.props.collapsable
+ && this.props.onClick
+ && node.clientHeight > MAX_HEIGHT
+ && this.props.status.get('spoiler_text').length === 0;
+
+ if(this.props.onCollapsedToggle) this.props.onCollapsedToggle(collapsed);
+
+ this.props.status.set('collapsed', collapsed);
}
}
@@ -178,6 +180,7 @@ export default class StatusContent extends React.PureComponent {
}
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
+ const renderReadMore = this.props.onClick && status.get('collapsed');
const content = { __html: status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
@@ -185,7 +188,7 @@ export default class StatusContent extends React.PureComponent {
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
- 'status__content--collapsed': this.state.collapsed === true,
+ 'status__content--collapsed': renderReadMore,
});
if (isRtl(status.get('search_index'))) {
@@ -237,7 +240,7 @@ export default class StatusContent extends React.PureComponent {
,
];
- if (this.state.collapsed) {
+ if (renderReadMore) {
output.push(readMoreButton);
}
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index e1b370c91..82e069601 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -26,6 +26,8 @@ export default class StatusList extends ImmutablePureComponent {
emptyMessage: PropTypes.node,
alwaysPrepend: PropTypes.bool,
timelineId: PropTypes.string,
+ currentlyViewing: PropTypes.number,
+ updateCurrentlyViewing: PropTypes.func,
};
static defaultProps = {
@@ -58,6 +60,12 @@ export default class StatusList extends ImmutablePureComponent {
this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
}, 300, { leading: true })
+ updateCurrentlyViewingWithCache = (id) => {
+ if(this.cachedCurrentlyViewing === id) return;
+ this.cachedCurrentlyViewing = id;
+ this.props.updateCurrentlyViewing(id);
+ }
+
_selectChild (index, align_top) {
const container = this.node.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
@@ -79,6 +87,7 @@ export default class StatusList extends ImmutablePureComponent {
render () {
const { statusIds, featuredStatusIds, shouldUpdateScroll, onLoadMore, timelineId, ...other } = this.props;
const { isLoading, isPartial } = other;
+ other.updateCurrentlyViewing = this.updateCurrentlyViewingWithCache;
if (isPartial) {
return ;
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 35c16a20c..2ba3a3123 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -23,6 +23,7 @@ import {
deleteStatus,
hideStatus,
revealStatus,
+ toggleStatusCollapse,
} from '../actions/statuses';
import {
unmuteAccount,
@@ -190,6 +191,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
+ onToggleCollapsed (status, isCollapsed) {
+ dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
+ },
+
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: {domain} }} />,
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index 9f6cbf988..33af628ca 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import StatusList from '../../../components/status_list';
-import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
+import { scrollTopTimeline, loadPending, updateCurrentlyViewing } from '../../../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect';
import { debounce } from 'lodash';
@@ -39,6 +39,7 @@ const makeMapStateToProps = () => {
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
numPending: getPendingStatusIds(state, { type: timelineId }).size,
+ currentlyViewing: state.getIn(['timelines', timelineId, 'currentlyViewing'], -1),
});
return mapStateToProps;
@@ -56,6 +57,7 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({
onLoadPending: () => dispatch(loadPending(timelineId)),
+ updateCurrentlyViewing: id => dispatch(updateCurrentlyViewing(timelineId, id)),
});
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 772f98bcb..398a48cff 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -12,6 +12,7 @@ import {
STATUS_UNMUTE_SUCCESS,
STATUS_REVEAL,
STATUS_HIDE,
+ STATUS_COLLAPSE,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
@@ -73,6 +74,8 @@ export default function statuses(state = initialState, action) {
}
});
});
+ case STATUS_COLLAPSE:
+ return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
default:
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 0d7222e10..970db425e 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -9,6 +9,7 @@ import {
TIMELINE_CONNECT,
TIMELINE_DISCONNECT,
TIMELINE_LOAD_PENDING,
+ CURRENTLY_VIEWING,
} from '../actions/timelines';
import {
ACCOUNT_BLOCK_SUCCESS,
@@ -28,6 +29,7 @@ const initialTimeline = ImmutableMap({
hasMore: true,
pendingItems: ImmutableList(),
items: ImmutableList(),
+ currentlyViewing: -1,
});
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
@@ -168,6 +170,8 @@ export default function timelines(state = initialState, action) {
initialTimeline,
map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items)
);
+ case CURRENTLY_VIEWING:
+ return state.update(action.timeline, initialTimeline, map => map.set('currentlyViewing', action.id));
default:
return state;
}