diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 5acbf749..0fa6d072 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,7 +2,7 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject, :verify, :unverify, :add_donor_badge, :remove_donor_badge, :add_investor_badge, :remove_investor_badge] + before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject, :verify, :unverify, :add_donor_badge, :remove_donor_badge, :add_investor_badge, :remove_investor_badge, :edit_pro, :save_pro] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] @@ -162,6 +162,17 @@ module Admin redirect_to admin_account_path(@account.id) end + def edit_pro + authorize @account, :edit_pro? + end + + def save_pro + authorize @account, :edit_pro? + + @account.update!(pro_params) + redirect_to edit_pro_admin_account_path(@account.id) + end + private def set_account @@ -196,5 +207,9 @@ module Admin :staff ) end + + def pro_params + params.require(:account).permit(:is_pro, :pro_expires_at) + end end end diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js index 579292d5..e377ece9 100644 --- a/app/javascript/gabsocial/actions/compose.js +++ b/app/javascript/gabsocial/actions/compose.js @@ -155,8 +155,6 @@ export function submitCompose(routerHistory, group) { }).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/icon_with_badge.js b/app/javascript/gabsocial/components/icon_with_badge.js index aa87f425..c42b9337 100644 --- a/app/javascript/gabsocial/components/icon_with_badge.js +++ b/app/javascript/gabsocial/components/icon_with_badge.js @@ -1,14 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import Icon from 'gabsocial/components/icon'; +import { shortNumberFormat } from 'gabsocial/utils/numbers'; -const formatNumber = num => num > 40 ? '40+' : num; +const IconWithBadge = ({ id, count, className }) => { + if (count < 1) return null; -const IconWithBadge = ({ id, count, className }) => ( - - {count > 0 && {formatNumber(count)}} - -); + return ( + + {count > 0 && {shortNumberFormat(count)}} + + ) +}; IconWithBadge.propTypes = { id: PropTypes.string.isRequired, @@ -16,4 +19,4 @@ IconWithBadge.propTypes = { className: PropTypes.string, }; -export default IconWithBadge; \ No newline at end of file +export default IconWithBadge; 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.js b/app/javascript/gabsocial/components/status.js index f8d588a4..bc72ffed 100644 --- a/app/javascript/gabsocial/components/status.js +++ b/app/javascript/gabsocial/components/status.js @@ -168,8 +168,7 @@ class Status extends ImmutablePureComponent { return; } - const { status } = this.props; - this.context.router.history.push(`/${status.getIn(['account', 'acct'])}/posts/${status.getIn(['reblog', 'id'], status.get('id'))}`); + this.context.router.history.push(`/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`); } handleExpandClick = (e) => { @@ -178,8 +177,7 @@ class Status extends ImmutablePureComponent { return; } - const { status } = this.props; - this.context.router.history.push(`/${status.getIn(['account', 'acct'])}/posts/${status.getIn(['reblog', 'id'], status.get('id'))}`); + this.context.router.history.push(`/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`); } } @@ -218,7 +216,7 @@ class Status extends ImmutablePureComponent { } handleHotkeyOpen = () => { - this.context.router.history.push(`/${status.getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`); + this.context.router.history.push(`/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`); } handleHotkeyOpenProfile = () => { diff --git a/app/javascript/gabsocial/components/status_list.js b/app/javascript/gabsocial/components/status_list.js index 77e12293..dad1889b 100644 --- a/app/javascript/gabsocial/components/status_list.js +++ b/app/javascript/gabsocial/components/status_list.js @@ -26,6 +26,8 @@ export default class StatusList extends ImmutablePureComponent { queuedItemSize: PropTypes.number, onDequeueTimeline: PropTypes.func, withGroupAdmin: PropTypes.bool, + onScrollToTop: PropTypes.func, + onScroll: PropTypes.func, }; componentDidMount() { @@ -135,7 +137,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 bd0734dc..8dd548ae 100644 --- a/app/javascript/gabsocial/features/compose/components/compose_form.js +++ b/app/javascript/gabsocial/features/compose/components/compose_form.js @@ -175,14 +175,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/reblogs/index.js b/app/javascript/gabsocial/features/reblogs/index.js index 34976469..bebd2812 100644 --- a/app/javascript/gabsocial/features/reblogs/index.js +++ b/app/javascript/gabsocial/features/reblogs/index.js @@ -4,16 +4,28 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from '../../components/loading_indicator'; +import MissingIndicator from '../../components/missing_indicator'; import { fetchReblogs } from '../../actions/interactions'; +import { fetchStatus } from '../../actions/statuses'; import { FormattedMessage } from 'react-intl'; import AccountContainer from '../../containers/account_container'; import Column from '../ui/components/column'; import ColumnBackButton from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; +import { makeGetStatus } from '../../selectors'; -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), -}); +const mapStateToProps = (state, props) => { + const getStatus = makeGetStatus(); + const status = getStatus(state, { + id: props.params.statusId, + username: props.params.username + }); + + return { + status, + accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), + } +}; export default @connect(mapStateToProps) class Reblogs extends ImmutablePureComponent { @@ -22,20 +34,23 @@ class Reblogs extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, + status: ImmutablePropTypes.map, }; componentWillMount () { this.props.dispatch(fetchReblogs(this.props.params.statusId)); + this.props.dispatch(fetchStatus(this.props.params.statusId)); } componentWillReceiveProps(nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this.props.dispatch(fetchReblogs(nextProps.params.statusId)); + this.props.dispatch(fetchStatus(nextProps.params.statusId)); } } render () { - const { accountIds } = this.props; + const { accountIds, status } = this.props; if (!accountIds) { return ( @@ -45,6 +60,14 @@ class Reblogs extends ImmutablePureComponent { ); } + if (!status) { + return ( + + + + ); + } + const emptyMessage = ; return ( diff --git a/app/javascript/gabsocial/features/status/index.js b/app/javascript/gabsocial/features/status/index.js index 59244854..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'; @@ -63,7 +62,11 @@ const makeMapStateToProps = () => { const getStatus = makeGetStatus(); const mapStateToProps = (state, props) => { - const status = getStatus(state, { id: props.params.statusId }); + const status = getStatus(state, { + id: props.params.statusId, + username: props.params.username + }); + let ancestorsIds = Immutable.List(); let descendantsIds = Immutable.List(); @@ -467,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..34981a6a 100644 --- a/app/javascript/gabsocial/features/ui/components/columns_area.js +++ b/app/javascript/gabsocial/features/ui/components/columns_area.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -15,33 +15,20 @@ import BundleColumnError from './bundle_column_error'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; import Icon from 'gabsocial/components/icon'; -const messages = defineMessages({ - publish: { id: 'compose_form.publish', defaultMessage: 'Gab' }, -}); - -const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/); - export default @(component => injectIntl(component, { withRef: true })) class ColumnsArea extends ImmutablePureComponent { - static contextTypes = { - router: PropTypes.object.isRequired, - }; - static propTypes = { intl: PropTypes.object.isRequired, columns: ImmutablePropTypes.list.isRequired, - isModalOpen: PropTypes.bool.isRequired, children: PropTypes.node, layout: PropTypes.object, }; render () { - const { columns, children, isModalOpen, intl, onOpenCompose } = this.props; + const { columns, children, intl } = this.props; const layout = this.props.layout || {LEFT:null,RIGHT:null}; - const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : ; - return (
@@ -64,8 +51,6 @@ class ColumnsArea extends ImmutablePureComponent { {layout.RIGHT}
- - {floatingActionButton}
diff --git a/app/javascript/gabsocial/features/ui/containers/columns_area_container.js b/app/javascript/gabsocial/features/ui/containers/columns_area_container.js index 1985ac11..4e26f301 100644 --- a/app/javascript/gabsocial/features/ui/containers/columns_area_container.js +++ b/app/javascript/gabsocial/features/ui/containers/columns_area_container.js @@ -1,17 +1,8 @@ import { connect } from 'react-redux'; import ColumnsArea from '../components/columns_area'; -import { openModal } from '../../../actions/modal'; const mapStateToProps = state => ({ columns: state.getIn(['settings', 'columns']), - isModalOpen: !!state.get('modal').modalType, }); - -const mapDispatchToProps = (dispatch) => ({ - onOpenCompose() { - dispatch(openModal('COMPOSE')); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(ColumnsArea); +export default connect(mapStateToProps, null, null, { forwardRef: true })(ColumnsArea); 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/features/ui/index.js b/app/javascript/gabsocial/features/ui/index.js index 623a8e23..1f299890 100644 --- a/app/javascript/gabsocial/features/ui/index.js +++ b/app/javascript/gabsocial/features/ui/index.js @@ -70,6 +70,7 @@ import '../../components/status'; const messages = defineMessages({ beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Gab Social.' }, + publish: { id: 'compose_form.publish', defaultMessage: 'Gab' }, }); const mapStateToProps = state => ({ @@ -132,6 +133,8 @@ const LAYOUT = { }, }; +const shouldHideFAB = path => path.match(/^\/posts\/|^\/search|^\/getting-started/); + class SwitchingColumnsArea extends React.PureComponent { static propTypes = { @@ -487,9 +490,13 @@ class UI extends React.PureComponent { this.context.router.history.push('/follow_requests'); } + handleOpenComposeModal = () => { + this.props.dispatch(openModal("COMPOSE")); + } + render () { const { draggingOver } = this.state; - const { children, isComposing, location, dropdownMenuIsOpen } = this.props; + const { intl, children, isComposing, location, dropdownMenuIsOpen } = this.props; const handlers = me ? { help: this.handleHotkeyToggleHelp, @@ -509,6 +516,8 @@ class UI extends React.PureComponent { goToRequests: this.handleHotkeyGoToRequests, } : {}; + const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : ; + return (
@@ -517,6 +526,8 @@ class UI extends React.PureComponent { {children} + {me && floatingActionButton} + diff --git a/app/javascript/gabsocial/reducers/timelines.js b/app/javascript/gabsocial/reducers/timelines.js index 6c3f64d9..a088aecb 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, @@ -138,6 +139,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 => @@ -176,6 +184,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/gabsocial/selectors/index.js b/app/javascript/gabsocial/selectors/index.js index 70f08a8e..5ebd1e9c 100644 --- a/app/javascript/gabsocial/selectors/index.js +++ b/app/javascript/gabsocial/selectors/index.js @@ -70,14 +70,21 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), + (state, { username }) => username, getFilters, ], - (statusBase, statusReblog, accountBase, accountReblog, filters) => { + (statusBase, statusReblog, accountBase, accountReblog, username, filters) => { if (!statusBase) { return null; } + const accountUsername = accountBase.get('acct'); + //Must be owner of status if username exists + if (accountUsername !== username && username !== undefined) { + return null; + } + if (statusReblog) { statusReblog = statusReblog.set('account', accountReblog); } else { 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; + } } } diff --git a/app/javascript/styles/gabsocial/components/tabs-bar.scss b/app/javascript/styles/gabsocial/components/tabs-bar.scss index 1fcae0d4..825ebadf 100644 --- a/app/javascript/styles/gabsocial/components/tabs-bar.scss +++ b/app/javascript/styles/gabsocial/components/tabs-bar.scss @@ -138,7 +138,7 @@ height: 38px; border-bottom: 4px solid $gab-default-text-light; } - & span {display: none;} + & > span {display: none;} } &.home { padding: 16px 0 0 25px; diff --git a/app/javascript/styles/gabsocial/polls.scss b/app/javascript/styles/gabsocial/polls.scss index b1cc6017..660f4a5e 100644 --- a/app/javascript/styles/gabsocial/polls.scss +++ b/app/javascript/styles/gabsocial/polls.scss @@ -29,6 +29,7 @@ overflow: hidden; text-overflow: ellipsis; color: #fff; + body.theme-gabsocial-light & {color: $gab-default-text-light;} input[type=radio], input[type=checkbox] { display: none; diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index d838f16e..7e95367f 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -61,6 +61,10 @@ class AccountPolicy < ApplicationPolicy staff? end + def edit_pro? + staff? + end + def update_badges? staff? end diff --git a/app/views/admin/accounts/_edit_pro_fields.html.haml b/app/views/admin/accounts/_edit_pro_fields.html.haml new file mode 100644 index 00000000..a48a5980 --- /dev/null +++ b/app/views/admin/accounts/_edit_pro_fields.html.haml @@ -0,0 +1,7 @@ +.fields-row + .fields-row__column.fields-row__column-6.fields-group + %label{for: "is_pro"} + PRO + = f.check_box :is_pro, wrapper: :with_label, hint: false, id: "is_pro" + .fields-row__column.fields-row__column-6.fields-group + = f.input :pro_expires_at, as: :string, wrapper: :with_label, hint: false \ No newline at end of file diff --git a/app/views/admin/accounts/edit_pro.html.haml b/app/views/admin/accounts/edit_pro.html.haml new file mode 100644 index 00000000..66a0f17d --- /dev/null +++ b/app/views/admin/accounts/edit_pro.html.haml @@ -0,0 +1,8 @@ +- content_for :page_title do + = 'Edit PRO status of @' + @account.acct + += simple_form_for @account, url: save_pro_admin_account_path(@account.id), method: :put do |f| + = render 'edit_pro_fields', f: f + + .actions + = f.button :button, t('generic.save_changes'), type: :submit \ No newline at end of file diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index a45f7a6a..4e709352 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -134,6 +134,9 @@ - if @account.is_pro? =fa_icon 'check' %time.formatted{ datetime: @account.pro_expires_at.iso8601, title: l(@account.pro_expires_at) }= l @account.pro_expires_at + %td + - if @account.local? + = table_link_to '', t('admin.accounts.edit_pro'), edit_pro_admin_account_path(@account.id), class: 'button' if can?(:verify, @account) %tr %th= t('admin.accounts.is_verified') diff --git a/config/routes.rb b/config/routes.rb index 24c5d152..78a44017 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -201,6 +201,8 @@ Rails.application.routes.draw do post :remove_donor_badge post :add_investor_badge post :remove_investor_badge + get :edit_pro + put :save_pro end resource :change_email, only: [:show, :update]