diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index b498a1ed..3cdc9fb9 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -4,6 +4,7 @@ class HomeController < ApplicationController before_action :authenticate_user! before_action :set_referrer_policy_header before_action :set_initial_state_json + before_action :set_data_for_meta def index @body_classes = 'app-body' @@ -11,17 +12,40 @@ class HomeController < ApplicationController private + def set_data_for_meta + return if find_route_matches + + if params[:username].present? + @account = Account.find_local(params[:username]) + elsif params[:account_username].present? + @account = Account.find_local(params[:account_username]) + + if params[:id].present? && !@account.nil? + @status = @account.statuses.find(params[:id]) + @stream_entry = @status.stream_entry + @type = @stream_entry.activity_type.downcase + end + end + + if request.path.starts_with?('/tags') && params[:tag].present? + @tag = Tag.find_normalized(params[:tag]) + end + + end + def authenticate_user! return if user_signed_in? # if no current user, dont allow to navigate to these paths - matches = request.path.match(/\A\/(home|groups|tags|lists|notifications|explore|follow_requests|blocks|domain_blocks|mutes)/) - - if matches + if find_route_matches redirect_to(homepage_path) end end + def find_route_matches + request.path.match(/\A\/(home|groups|lists|notifications|explore|follow_requests|blocks|domain_blocks|mutes)/) + end + def set_initial_state_json serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) @initial_state_json = serializable_resource.to_json diff --git a/app/javascript/gabsocial/actions/sidebar.js b/app/javascript/gabsocial/actions/sidebar.js new file mode 100644 index 00000000..bd646796 --- /dev/null +++ b/app/javascript/gabsocial/actions/sidebar.js @@ -0,0 +1,14 @@ +export const SIDEBAR_OPEN = 'SIDEBAR_OPEN'; +export const SIDEBAR_CLOSE = 'SIDEBAR_CLOSE'; + +export function openSidebar() { + return { + type: SIDEBAR_OPEN, + }; +}; + +export function closeSidebar() { + return { + type: SIDEBAR_CLOSE, + }; +}; diff --git a/app/javascript/gabsocial/actions/statuses.js b/app/javascript/gabsocial/actions/statuses.js index 1b2744c4..06b40542 100644 --- a/app/javascript/gabsocial/actions/statuses.js +++ b/app/javascript/gabsocial/actions/statuses.js @@ -3,7 +3,7 @@ import openDB from '../storage/db'; import { evictStatus } from '../storage/modifier'; import { deleteFromTimelines } from './timelines'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; -import { ensureComposeIsVisible } from './compose'; +import { openModal } from './modal'; import { me } from 'gabsocial/initial_state'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; @@ -159,7 +159,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { if (withRedraft) { dispatch(redraft(status, response.data.text)); - ensureComposeIsVisible(getState, routerHistory); + dispatch(openModal('COMPOSE')); } }).catch(error => { dispatch(deleteStatusFail(id, error)); @@ -272,7 +272,7 @@ export function muteStatusFail(id, error) { export function unmuteStatus(id) { return (dispatch, getState) => { if (!me) return; - + dispatch(unmuteStatusRequest(id)); api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => { diff --git a/app/javascript/gabsocial/components/sidebar_menu.js b/app/javascript/gabsocial/components/sidebar_menu.js new file mode 100644 index 00000000..410eae55 --- /dev/null +++ b/app/javascript/gabsocial/components/sidebar_menu.js @@ -0,0 +1,212 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Link, NavLink } from 'react-router-dom'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { injectIntl, defineMessages } from 'react-intl'; +import classNames from 'classnames'; +import Avatar from './avatar'; +import IconButton from './icon_button'; +import Icon from './icon'; +import DisplayName from './display_name'; +import { closeSidebar } from '../actions/sidebar'; +import { shortNumberFormat } from '../utils/numbers'; +import { me } from '../initial_state'; +import { makeGetAccount } from '../selectors'; + +const messages = defineMessages({ + followers: { id: 'account.followers', defaultMessage: 'Followers' }, + follows: { id: 'account.follows', defaultMessage: 'Follows' }, + profile: { id: 'account.profile', defaultMessage: 'Profile' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, + filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, + lists: { id: 'column.lists', defaultMessage: 'Lists', }, + apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' }, + news: { id: 'tabs_bar.news', defaultMessage: 'News' }, + more: { id: 'sidebar.more', defaultMessage: 'More' }, + partners: { id: 'promo.partners', defaultMessage: 'Affiliate Partners' }, + store: { id: 'promo.store', defaultMessage: 'Store' }, +}) + +const mapStateToProps = state => { + const getAccount = makeGetAccount(); + + return { + account: getAccount(state, me), + sidebarOpen: state.get('sidebar').sidebarOpen, + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + onClose () { + dispatch(closeSidebar()); + }, +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class SidebarMenu extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + account: ImmutablePropTypes.map, + sidebarOpen: PropTypes.bool, + onClose: PropTypes.func.isRequired, + }; + + state = { + moreOpen: false, + } + + componentDidUpdate () { + if (!me) return; + + if (this.props.sidebarOpen) { + document.body.classList.add('with-modals--active'); + } else { + document.body.classList.remove('with-modals--active'); + } + } + + toggleMore = () => { + this.setState({ + moreOpen: !this.state.moreOpen + }); + } + + handleSidebarClose = () => { + this.props.onClose(); + this.setState({ + moreOpen: false, + }); + } + + render () { + const { sidebarOpen, intl, account } = this.props; + const { moreOpen } = this.state; + + if (!me || !account) return null; + + const acct = account.get('acct'); + + const classes = classNames('sidebar-menu__root', { + 'sidebar-menu__root--visible': sidebarOpen, + }); + + const moreIcon = moreOpen ? 'minus' : 'plus'; + const moreContainerStyle = { display: moreOpen ? 'block' : 'none' }; + + return ( +
+
+
+ +
+ Account Info + +
+ +
+ +
+
+ + + +
+
+ +
+ +
+ + {shortNumberFormat(account.get('followers_count'))} + {intl.formatMessage(messages.followers)} + + + {shortNumberFormat(account.get('following_count'))} + {intl.formatMessage(messages.follows)} + +
+ +
+ +
+ + + {intl.formatMessage(messages.profile)} + + + + {intl.formatMessage(messages.news)} + + + + {intl.formatMessage(messages.store)} + + + + {intl.formatMessage(messages.partners)} + + + + {intl.formatMessage(messages.apps)} + + + + {intl.formatMessage(messages.preferences)} + +
+ +
+
+ + {intl.formatMessage(messages.more)} +
+
+ + + {intl.formatMessage(messages.lists)} + + + + {intl.formatMessage(messages.follow_requests)} + + + + {intl.formatMessage(messages.blocks)} + + + + {intl.formatMessage(messages.domain_blocks)} + + + + {intl.formatMessage(messages.mutes)} + + + + {intl.formatMessage(messages.filters)} + +
+
+ +
+ + {intl.formatMessage(messages.logout)} + +
+ +
+
+
+ ); + } + +} diff --git a/app/javascript/gabsocial/components/status_content.js b/app/javascript/gabsocial/components/status_content.js index ca47143f..0b274e72 100644 --- a/app/javascript/gabsocial/components/status_content.js +++ b/app/javascript/gabsocial/components/status_content.js @@ -45,7 +45,7 @@ export default class StatusContent extends React.PureComponent { } link.classList.add('status-link'); - let mention = this.props.status.get('mentions').find(item => link.href === `/${item.get('acct')}`); + let mention = this.props.status.get('mentions').find(item => link.href === `${item.get('url')}`); if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); @@ -139,7 +139,7 @@ export default class StatusContent extends React.PureComponent { const { status, reblogContent } = this.props; const properContent = status.get('contentHtml'); - + return reblogContent ? `${reblogContent}
${properContent}
` : properContent; diff --git a/app/javascript/gabsocial/features/compose/components/action_bar.js b/app/javascript/gabsocial/features/compose/components/action_bar.js index 06c12e4b..2a710287 100644 --- a/app/javascript/gabsocial/features/compose/components/action_bar.js +++ b/app/javascript/gabsocial/features/compose/components/action_bar.js @@ -48,7 +48,7 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); - menu.push({ text: intl.formatMessage(messages.filters), to: '/filters' }); + menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); menu.push(null); menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick }); menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); diff --git a/app/javascript/gabsocial/features/ui/components/promo_panel.js b/app/javascript/gabsocial/features/ui/components/promo_panel.js index 2cb47fa3..659f52fa 100644 --- a/app/javascript/gabsocial/features/ui/components/promo_panel.js +++ b/app/javascript/gabsocial/features/ui/components/promo_panel.js @@ -1,32 +1,41 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import { me } from '../../../initial_state'; -import { makeGetAccount } from '../../../selectors'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; import Icon from 'gabsocial/components/icon'; -const PromoPanel = () => ( -
-
+export default class PromoPanel extends React.PureComponent { + render() { + return ( +
+
+ -
- - - - -

- -

+ + + + + +
- -
-
-); - -export default PromoPanel; + ) + } +} \ No newline at end of file diff --git a/app/javascript/gabsocial/features/ui/components/tabs_bar.js b/app/javascript/gabsocial/features/ui/components/tabs_bar.js index 1164aba2..cb9280cb 100644 --- a/app/javascript/gabsocial/features/ui/components/tabs_bar.js +++ b/app/javascript/gabsocial/features/ui/components/tabs_bar.js @@ -2,47 +2,50 @@ import React from 'react'; import PropTypes from 'prop-types'; import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { debounce } from 'lodash'; +import { throttle } from 'lodash'; import { connect } from 'react-redux'; -import { isUserTouching } from '../../../is_mobile'; import { me } from '../../../initial_state'; -import { Link } from 'react-router-dom'; +import classNames from 'classnames'; import NotificationsCounterIcon from './notifications_counter_icon'; import SearchContainer from 'gabsocial/features/compose/containers/search_container'; import Avatar from '../../../components/avatar'; import ActionBar from 'gabsocial/features/compose/components/action_bar'; import { openModal } from '../../../actions/modal'; +import { openSidebar } from '../../../actions/sidebar'; export const privateLinks = [ - + , - + + , - + + , - + + , - - - , - + + , ]; export const publicLinks = [ - + , - + + , - + + , ]; @@ -54,18 +57,104 @@ class TabsBar extends React.PureComponent { intl: PropTypes.object.isRequired, history: PropTypes.object.isRequired, onOpenCompose: PropTypes.func, + onOpenSidebar: PropTypes.func.isRequired, + } + + state = { + collapsed: false, + } + + static contextTypes = { + router: PropTypes.object, + } + + lastScrollTop = 0; + + componentDidMount () { + this.window = window; + this.documentElement = document.scrollingElement || document.documentElement; + + this.attachScrollListener(); + // Handle initial scroll posiiton + this.handleScroll(); + } + + componentWillUnmount () { + this.detachScrollListener(); } setRef = ref => { this.node = ref; } + attachScrollListener () { + this.window.addEventListener('scroll', this.handleScroll); + } + + detachScrollListener () { + this.window.removeEventListener('scroll', this.handleScroll); + } + + handleScroll = throttle(() => { + if (this.window) { + const { pageYOffset, innerWidth } = this.window; + if (innerWidth > 895) return; + const { scrollTop } = this.documentElement; + + let st = pageYOffset || scrollTop; + if (st > this.lastScrollTop){ + let offset = st - this.lastScrollTop; + if (offset > 50) this.setState({collapsed: true}); + } else { + let offset = this.lastScrollTop - st; + if (offset > 50) this.setState({collapsed: false}); + } + + this.lastScrollTop = st <= 0 ? 0 : st; + } + }, 150, { + trailing: true, + }); + render () { - const { intl: { formatMessage }, account, onOpenCompose } = this.props; + const { intl: { formatMessage }, account, onOpenCompose, onOpenSidebar } = this.props; + const { collapsed } = this.state; const links = account ? privateLinks : publicLinks; + const pathname = this.context.router.route.location.pathname || ''; + let pathTitle = ''; + if (pathname.includes('/home')) { + pathTitle = 'Home'; + } else if (pathname.includes('/timeline/all')) { + pathTitle = 'All'; + } else if (pathname.includes('/group')) { + pathTitle = 'Groups'; + } else if (pathname.includes('/tags')) { + pathTitle = 'Tags'; + } else if (pathname.includes('/list')) { + pathTitle = 'Lists'; + } else if (pathname.includes('/notifications')) { + pathTitle = 'Notifications'; + } else if (pathname.includes('/search')) { + pathTitle = 'Search'; + } else if (pathname.includes('/follow_requests')) { + pathTitle = 'Requests'; + } else if (pathname.includes('/blocks')) { + pathTitle = 'Blocks'; + } else if (pathname.includes('/domain_blocks')) { + pathTitle = 'Domain Blocks'; + } else if (pathname.includes('/mutes')) { + pathTitle = 'Mutes'; + } else { + pathTitle = 'Profile'; + } + + const classes = classNames('tabs-bar', { + 'tabs-bar--collapsed': collapsed, + }) + return ( -