diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js index ec098608..f1cef65f 100644 --- a/app/javascript/gabsocial/actions/compose.js +++ b/app/javascript/gabsocial/actions/compose.js @@ -154,8 +154,6 @@ export function submitCompose(routerHistory) { }).then(function (response) { if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) { routerHistory.push('/messages'); - } else if (routerHistory && routerHistory.location.pathname === '/posts/new' && window.history.state) { - routerHistory.goBack(); } dispatch(insertIntoTagHistory(response.data.tags, status)); diff --git a/app/javascript/gabsocial/actions/timelines.js b/app/javascript/gabsocial/actions/timelines.js index 4214cbde..78f372b2 100644 --- a/app/javascript/gabsocial/actions/timelines.js +++ b/app/javascript/gabsocial/actions/timelines.js @@ -7,6 +7,7 @@ export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE'; export const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; @@ -210,3 +211,11 @@ export function disconnectTimeline(timeline) { timeline, }; }; + +export function scrollTopTimeline(timeline, top) { + return { + type: TIMELINE_SCROLL_TOP, + timeline, + top, + }; +}; diff --git a/app/javascript/gabsocial/components/scrollable_list.js b/app/javascript/gabsocial/components/scrollable_list.js index 33662df8..1dbbdb23 100644 --- a/app/javascript/gabsocial/components/scrollable_list.js +++ b/app/javascript/gabsocial/components/scrollable_list.js @@ -26,6 +26,8 @@ export default class ScrollableList extends PureComponent { alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, + onScrollToTop: PropTypes.func, + onScroll: PropTypes.func, }; state = { @@ -40,9 +42,9 @@ export default class ScrollableList extends PureComponent { scrollToTopOnMouseIdle = false; setScrollTop = newScrollTop => { - if (this.node.scrollTop !== newScrollTop) { + if (this.documentElement.scrollTop !== newScrollTop) { this.lastScrollWasSynthetic = true; - this.node.scrollTop = newScrollTop; + this.documentElement.scrollTop = newScrollTop; } }; @@ -60,7 +62,7 @@ export default class ScrollableList extends PureComponent { this.clearMouseIdleTimer(); this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); - if (!this.mouseMovedRecently && this.node.scrollTop === 0) { + if (!this.mouseMovedRecently && this.documentElement.scrollTop === 0) { // Only set if we just started moving and are scrolled to the top. this.scrollToTopOnMouseIdle = true; } @@ -79,19 +81,25 @@ export default class ScrollableList extends PureComponent { } componentDidMount () { + this.window = window; + this.documentElement = document.documentElement; + + this.attachScrollListener(); this.attachIntersectionObserver(); + // Handle initial scroll posiiton + this.handleScroll(); } getScrollPosition = () => { - if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return { height: this.node.scrollHeight, top: this.node.scrollTop }; + if (this.documentElement && (this.documentElement.scrollTop > 0 || this.mouseMovedRecently)) { + return { height: this.documentElement.scrollHeight, top: this.documentElement.scrollTop }; } else { return null; } } updateScrollBottom = (snapshot) => { - const newScrollTop = this.node.scrollHeight - snapshot; + const newScrollTop = this.documentElement.scrollHeight - snapshot; this.setScrollTop(newScrollTop); } @@ -100,7 +108,61 @@ export default class ScrollableList extends PureComponent { // Reset the scroll position when a new child comes in in order not to // jerk the scrollbar around if you're already scrolled down the page. if (snapshot !== null) { - this.setScrollTop(this.node.scrollHeight - snapshot); + this.setScrollTop(this.documentElement.scrollHeight - snapshot); + } + } + + attachScrollListener () { + this.window.addEventListener('scroll', this.handleScroll); + this.window.addEventListener('wheel', this.handleWheel); + } + + detachScrollListener () { + this.window.removeEventListener('scroll', this.handleScroll); + this.window.removeEventListener('wheel', this.handleWheel); + } + + handleScroll = throttle(() => { + if (this.window) { + const { scrollTop, scrollHeight, clientHeight } = this.documentElement; + const offset = scrollHeight - scrollTop - clientHeight; + + if (600 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) { + this.props.onLoadMore(); + } + + if (scrollTop < 100 && this.props.onScrollToTop) { + this.props.onScrollToTop(); + } else if (this.props.onScroll) { + this.props.onScroll(); + } + + if (!this.lastScrollWasSynthetic) { + // If the last scroll wasn't caused by setScrollTop(), assume it was + // intentional and cancel any pending scroll reset on mouse idle + this.scrollToTopOnMouseIdle = false; + } + this.lastScrollWasSynthetic = false; + } + }, 150, { + trailing: true, + }); + + handleWheel = throttle(() => { + this.scrollToTopOnMouseIdle = false; + }, 150, { + trailing: true, + }); + + getSnapshotBeforeUpdate (prevProps) { + 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.documentElement.scrollTop > 0 || this.mouseMovedRecently)) { + return this.documentElement.scrollHeight - this.documentElement.scrollTop; + } else { + return null; } } @@ -116,10 +178,7 @@ export default class ScrollableList extends PureComponent { } attachIntersectionObserver () { - this.intersectionObserverWrapper.connect({ - root: this.node, - rootMargin: '300% 0px', - }); + this.intersectionObserverWrapper.connect(); } detachIntersectionObserver () { @@ -139,10 +198,6 @@ export default class ScrollableList extends PureComponent { return firstChild && firstChild.key; } - setRef = (c) => { - this.node = c; - } - handleLoadMore = e => { e.preventDefault(); this.props.onLoadMore(); @@ -159,7 +214,7 @@ export default class ScrollableList extends PureComponent { if (showLoading) { scrollableArea = ( -
+
{prepend}
diff --git a/app/javascript/gabsocial/components/status_list.js b/app/javascript/gabsocial/components/status_list.js index 91bbc511..03df0c25 100644 --- a/app/javascript/gabsocial/components/status_list.js +++ b/app/javascript/gabsocial/components/status_list.js @@ -25,6 +25,8 @@ export default class StatusList extends ImmutablePureComponent { timelineId: PropTypes.string, queuedItemSize: PropTypes.number, onDequeueTimeline: PropTypes.func, + onScrollToTop: PropTypes.func, + onScroll: PropTypes.func, }; componentDidMount() { @@ -133,7 +135,7 @@ export default class StatusList extends ImmutablePureComponent { return [ , - + {scrollableContent} ]; diff --git a/app/javascript/gabsocial/components/timeline_queue_button_header.js b/app/javascript/gabsocial/components/timeline_queue_button_header.js index e1bfe6d0..f50e736f 100644 --- a/app/javascript/gabsocial/components/timeline_queue_button_header.js +++ b/app/javascript/gabsocial/components/timeline_queue_button_header.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { shortNumberFormat } from '../utils/numbers'; +import classNames from 'classnames'; export default class TimelineQueueButtonHeader extends React.PureComponent { static propTypes = { @@ -18,19 +19,21 @@ export default class TimelineQueueButtonHeader extends React.PureComponent { render () { const { count, itemType, onClick } = this.props; - if (count <= 0) return null; + const classes = classNames('timeline-queue-header', { + 'hidden': (count <= 0) + }); return ( -
+ ); diff --git a/app/javascript/gabsocial/features/account_gallery/index.js b/app/javascript/gabsocial/features/account_gallery/index.js index 8d77b27c..9c2fc52e 100644 --- a/app/javascript/gabsocial/features/account_gallery/index.js +++ b/app/javascript/gabsocial/features/account_gallery/index.js @@ -12,7 +12,6 @@ import Column from '../ui/components/column'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { getAccountGallery } from 'gabsocial/selectors'; import MediaItem from './components/media_item'; -import { ScrollContainer } from 'react-router-scroll-4'; import LoadMore from 'gabsocial/components/load_more'; import MissingIndicator from 'gabsocial/components/missing_indicator'; import { openModal } from 'gabsocial/actions/modal'; @@ -189,46 +188,44 @@ class AccountGallery extends ImmutablePureComponent { return ( - -
-
-
- - - - - - - - - -
+
+
+
+ + + + + + + + +
- -
- {attachments.map((attachment, index) => attachment === null ? ( - 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> - ) : ( - - ))} - - { - attachments.size == 0 && -
- -
- } - - {loadOlder} -
- - {isLoading && attachments.size === 0 && ( -
- -
- )}
- + +
+ {attachments.map((attachment, index) => attachment === null ? ( + 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> + ) : ( + + ))} + + { + attachments.size == 0 && +
+ +
+ } + + {loadOlder} +
+ + {isLoading && attachments.size === 0 && ( +
+ +
+ )} +
); } diff --git a/app/javascript/gabsocial/features/compose/components/compose_form.js b/app/javascript/gabsocial/features/compose/components/compose_form.js index 8f595904..03818e2a 100644 --- a/app/javascript/gabsocial/features/compose/components/compose_form.js +++ b/app/javascript/gabsocial/features/compose/components/compose_form.js @@ -174,14 +174,6 @@ class ComposeForm extends ImmutablePureComponent { this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); this.autosuggestTextarea.textarea.focus(); - } else if(prevProps.isSubmitting && !this.props.isSubmitting) { - this.autosuggestTextarea.textarea.focus(); - } else if (this.props.spoiler !== prevProps.spoiler) { - if (this.props.spoiler) { - this.spoilerText.input.focus(); - } else { - this.autosuggestTextarea.textarea.focus(); - } } } diff --git a/app/javascript/gabsocial/features/status/index.js b/app/javascript/gabsocial/features/status/index.js index 0e8c3e00..e88ab4c1 100644 --- a/app/javascript/gabsocial/features/status/index.js +++ b/app/javascript/gabsocial/features/status/index.js @@ -33,7 +33,6 @@ import { import { initMuteModal } from '../../actions/mutes'; import { initReport } from '../../actions/reports'; import { makeGetStatus } from '../../selectors'; -import { ScrollContainer } from 'react-router-scroll-4'; import ColumnHeader from '../../components/column_header'; import StatusContainer from '../../containers/status_container'; import { openModal } from '../../actions/modal'; @@ -471,43 +470,41 @@ class Status extends ImmutablePureComponent { /> } - -
- {ancestors} +
+ {ancestors} - -
- + +
+ - -
-
+ +
+
- {descendants} -
- + {descendants} +
); } diff --git a/app/javascript/gabsocial/features/ui/components/columns_area.js b/app/javascript/gabsocial/features/ui/components/columns_area.js index 361291ef..6715e86c 100644 --- a/app/javascript/gabsocial/features/ui/components/columns_area.js +++ b/app/javascript/gabsocial/features/ui/components/columns_area.js @@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ReactSwipeableViews from 'react-swipeable-views'; import { links, getIndex, getLink } from './tabs_bar'; import { Link } from 'react-router-dom'; +import { me } from 'gabsocial/initial_state'; import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; @@ -64,8 +65,7 @@ class ColumnsArea extends ImmutablePureComponent { {layout.RIGHT}
- - {floatingActionButton} + {me && floatingActionButton}
diff --git a/app/javascript/gabsocial/features/ui/containers/status_list_container.js b/app/javascript/gabsocial/features/ui/containers/status_list_container.js index ee58b9ed..f130f9f4 100644 --- a/app/javascript/gabsocial/features/ui/containers/status_list_container.js +++ b/app/javascript/gabsocial/features/ui/containers/status_list_container.js @@ -5,6 +5,7 @@ import { createSelector } from 'reselect'; import { debounce } from 'lodash'; import { me } from '../../../initial_state'; import { dequeueTimeline } from 'gabsocial/actions/timelines'; +import { scrollTopTimeline } from '../../../actions/timelines'; const makeGetStatusIds = () => createSelector([ (state, { type }) => state.getIn(['settings', type], ImmutableMap()), @@ -45,6 +46,12 @@ const mapDispatchToProps = (dispatch, ownProps) => ({ onDequeueTimeline(timelineId) { dispatch(dequeueTimeline(timelineId, ownProps.onLoadMore)); }, + onScrollToTop: debounce(() => { + dispatch(scrollTopTimeline(ownProps.timelineId, true)); + }, 100), + onScroll: debounce(() => { + dispatch(scrollTopTimeline(ownProps.timelineId, false)); + }, 100), }); export default connect(mapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/gabsocial/reducers/timelines.js b/app/javascript/gabsocial/reducers/timelines.js index b0c1babf..11acf1e6 100644 --- a/app/javascript/gabsocial/reducers/timelines.js +++ b/app/javascript/gabsocial/reducers/timelines.js @@ -10,6 +10,7 @@ import { TIMELINE_UPDATE_QUEUE, TIMELINE_DEQUEUE, MAX_QUEUED_ITEMS, + TIMELINE_SCROLL_TOP, } from '../actions/timelines'; import { ACCOUNT_BLOCK_SUCCESS, @@ -137,6 +138,13 @@ const filterTimelines = (state, relationship, statuses) => { return state; }; +const updateTop = (state, timeline, top) => { + return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { + if (top) mMap.set('unread', 0); + mMap.set('top', top); + })); +}; + const filterTimeline = (timeline, state, relationship, statuses) => state.updateIn([timeline, 'items'], ImmutableList(), list => list.filterNot(statusId => @@ -171,6 +179,8 @@ export default function timelines(state = initialState, action) { return filterTimeline('home', state, action.relationship, action.statuses); case TIMELINE_CONNECT: return state.update(action.timeline, initialTimeline, map => map.set('online', true)); + case TIMELINE_SCROLL_TOP: + return updateTop(state, action.timeline, action.top); case TIMELINE_DISCONNECT: return state.update( action.timeline, diff --git a/app/javascript/styles/gabsocial/basics.scss b/app/javascript/styles/gabsocial/basics.scss index d3edae2a..1996b64f 100644 --- a/app/javascript/styles/gabsocial/basics.scss +++ b/app/javascript/styles/gabsocial/basics.scss @@ -48,7 +48,6 @@ body { &.app-body { position: absolute; width: 100%; - height: 100%; padding: 0; overflow: hidden; overflow-y: scroll; diff --git a/app/javascript/styles/gabsocial/components.scss b/app/javascript/styles/gabsocial/components.scss index 11373a5a..8ee1c6f2 100644 --- a/app/javascript/styles/gabsocial/components.scss +++ b/app/javascript/styles/gabsocial/components.scss @@ -5108,21 +5108,31 @@ noscript { .timeline-queue-header { display: block; width: 100%; - height: 52px; + max-height: 46px; position: relative; background-color: darken($ui-base-color, 8%); border-bottom: 1px solid; border-top: 1px solid; border-color: darken($ui-base-color, 4%); + transition: max-height 2.5s ease; + overflow: hidden; + + &.hidden { + max-height: 0px; + } &__btn { display: block; width: 100%; height: 100%; text-align: center; - line-height: 52px; + line-height: 46px; font-size: 14px; cursor: pointer; color: $secondary-text-color; + + span { + height: 46px; + } } }