+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/modal_root/index.scss b/app/javascript/gabsocial/components/modal_base/modal_base.scss
similarity index 93%
rename from app/javascript/gabsocial/components/modal_root/index.scss
rename to app/javascript/gabsocial/components/modal_base/modal_base.scss
index c81242e8..b2127a42 100644
--- a/app/javascript/gabsocial/components/modal_root/index.scss
+++ b/app/javascript/gabsocial/components/modal_base/modal_base.scss
@@ -1,4 +1,4 @@
-.modal-root {
+.modal-base {
position: relative;
z-index: 9999;
@@ -25,7 +25,7 @@
}
}
-// .modal-root__modal {
+// .modal-base__modal {
// pointer-events: auto;
// display: flex;
// z-index: 9999;
diff --git a/app/javascript/gabsocial/components/modal_loading/index.js b/app/javascript/gabsocial/components/modal_loading/index.js
new file mode 100644
index 00000000..abf856e0
--- /dev/null
+++ b/app/javascript/gabsocial/components/modal_loading/index.js
@@ -0,0 +1 @@
+export { default } from './modal_loading';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/modal_loading/modal_loading.js b/app/javascript/gabsocial/components/modal_loading/modal_loading.js
new file mode 100644
index 00000000..63a012c0
--- /dev/null
+++ b/app/javascript/gabsocial/components/modal_loading/modal_loading.js
@@ -0,0 +1,23 @@
+import { PureComponent } from 'react';
+import ColumnIndicator from '../column_indicator';
+
+import './modal_loading.scss';
+
+// Keep the markup in sync with
+// (make sure they have the same dimensions)
+export default class ModalLoading extends PureComponent {
+ render() {
+ return (
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/modal_loading/modal_loading.scss b/app/javascript/gabsocial/components/modal_loading/modal_loading.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/app/javascript/gabsocial/components/modal_root/index.js b/app/javascript/gabsocial/components/modal_root/index.js
index bc35ef81..aef205be 100644
--- a/app/javascript/gabsocial/components/modal_root/index.js
+++ b/app/javascript/gabsocial/components/modal_root/index.js
@@ -1,137 +1 @@
-import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
-import classNames from 'classnames';
-import { openModal } from '../../actions/modal';
-import { cancelReplyCompose } from '../../actions/compose';
-
-import './index.scss';
-
-const messages = defineMessages({
- confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
-});
-
-const mapStateToProps = state => ({
- composeText: state.getIn(['compose', 'text']),
-});
-
-const mapDispatchToProps = (dispatch) => ({
- onOpenModal(type, opts) {
- dispatch(openModal(type, opts));
- },
- onCancelReplyCompose() {
- dispatch(cancelReplyCompose());
- },
-});
-
-class ModalRoot extends PureComponent {
-
- static propTypes = {
- children: PropTypes.node,
- onClose: PropTypes.func.isRequired,
- onOpenModal: PropTypes.func.isRequired,
- onCancelReplyCompose: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- composeText: PropTypes.string,
- type: PropTypes.string,
- };
-
- state = {
- revealed: !!this.props.children,
- };
-
- activeElement = this.state.revealed ? document.activeElement : null;
-
- handleKeyUp = (e) => {
- if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
- && !!this.props.children) {
- this.handleOnClose();
- }
- }
-
- handleOnClose = () => {
- const { onOpenModal, composeText, onClose, intl, type, onCancelReplyCompose } = this.props;
-
- if (composeText && type === 'COMPOSE') {
- onOpenModal('CONFIRM', {
- message:
,
- confirm: intl.formatMessage(messages.confirm),
- onConfirm: () => onCancelReplyCompose(),
- onCancel: () => onOpenModal('COMPOSE'),
- });
- } else {
- onClose();
- }
- };
-
- componentDidMount () {
- window.addEventListener('keyup', this.handleKeyUp, false);
- }
-
- componentWillReceiveProps (nextProps) {
- if (!!nextProps.children && !this.props.children) {
- this.activeElement = document.activeElement;
-
- this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
- } else if (!nextProps.children) {
- this.setState({ revealed: false });
- }
-
- if (!nextProps.children && !!this.props.children) {
- this.activeElement.focus();
- this.activeElement = null;
- }
- }
-
- componentDidUpdate (prevProps) {
- if (!this.props.children && !!prevProps.children) {
- this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
- }
-
- if (this.props.children) {
- requestAnimationFrame(() => {
- this.setState({ revealed: true });
- });
- }
- }
-
- componentWillUnmount () {
- window.removeEventListener('keyup', this.handleKeyUp);
- }
-
- getSiblings = () => {
- return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
- }
-
- setRef = ref => {
- this.node = ref;
- }
-
- render () {
- const { children } = this.props;
- const { revealed } = this.state;
- const visible = !!children;
-
- if (!visible) {
- return (
-
- );
- }
-
- const classes = classNames('modal-root', {
- 'modal-root--hidden': !revealed,
- });
-
- return (
-
-
-
this.handleOnClose()} />
-
- {children}
-
-
-
- );
- }
-
-}
-
-export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ModalRoot));
+export { default } from './modal_root';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/ui/components/modal_root.js b/app/javascript/gabsocial/components/modal_root/modal_root.js
similarity index 69%
rename from app/javascript/gabsocial/features/ui/components/modal_root.js
rename to app/javascript/gabsocial/components/modal_root/modal_root.js
index 8a000df7..3b0488b6 100644
--- a/app/javascript/gabsocial/features/ui/components/modal_root.js
+++ b/app/javascript/gabsocial/components/modal_root/modal_root.js
@@ -1,16 +1,18 @@
-import Base from '../../../components/modal_root';
-import BundleContainer from '../containers/bundle_container';
-import BundleModalError from './bundle_modal_error';
-import ModalLoading from './modal_loading';
-import ActionsModal from './actions_modal';
-import MediaModal from './media_modal';
-import VideoModal from './video_modal';
-import BoostModal from './boost_modal';
-import ConfirmationModal from './confirmation_modal';
-import FocalPointModal from './focal_point_modal';
-import HotkeysModal from './hotkeys_modal';
-import ComposeModal from './compose_modal';
-import UnauthorizedModal from './unauthorized_modal';
+import Base from '../modal_base';
+import Bundle from '../../features/ui/util/bundle';
+import BundleModalError from '../bundle_modal_error';
+import {
+ ModalLoading,
+ ActionsModal,
+ MediaModal,
+ VideoModal,
+ BoostModal,
+ ConfirmationModal,
+ FocalPointModal,
+ HotkeysModal,
+ ComposeModal,
+ UnauthorizedModal,
+} from '../modal';
import {
MuteModal,
@@ -18,7 +20,7 @@ import {
EmbedModal,
ListEditor,
ListAdder,
-} from '../../../features/ui/util/async-components';
+} from '../../features/ui/util/async-components';
const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
@@ -77,9 +79,9 @@ export default class ModalRoot extends PureComponent {
return (
{visible && (
-
+
{(SpecificComponent) => }
-
+
)}
);
diff --git a/app/javascript/gabsocial/components/notification_counter/index.js b/app/javascript/gabsocial/components/notification_counter/index.js
index fdce4129..60ee4617 100644
--- a/app/javascript/gabsocial/components/notification_counter/index.js
+++ b/app/javascript/gabsocial/components/notification_counter/index.js
@@ -1,27 +1 @@
-import { shortNumberFormat } from '../../utils/numbers';
-
-import './index.scss';
-
-const mapStateToProps = state => ({
- count: state.getIn(['notifications', 'unread']),
-});
-
-class NotificationCounter extends PureComponent {
-
- static propTypes = {
- count: PropTypes.number.isRequired,
- };
-
- render() {
- const { count } = this.props;
-
- if (count < 1) return null;
-
- return (
-
{shortNumberFormat(count)}
- );
- }
-
-}
-
-export default connect(mapStateToProps)(NotificationCounter);
\ No newline at end of file
+export { default } from './notification_counter';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/notification_counter/notification_counter.js b/app/javascript/gabsocial/components/notification_counter/notification_counter.js
new file mode 100644
index 00000000..66cde8f0
--- /dev/null
+++ b/app/javascript/gabsocial/components/notification_counter/notification_counter.js
@@ -0,0 +1,26 @@
+import { shortNumberFormat } from '../../utils/numbers';
+
+import './notification_counter.scss';
+
+const mapStateToProps = state => ({
+ count: state.getIn(['notifications', 'unread']),
+});
+
+export default @connect(mapStateToProps)
+class NotificationCounter extends PureComponent {
+
+ static propTypes = {
+ count: PropTypes.number.isRequired,
+ };
+
+ render() {
+ const { count } = this.props;
+
+ if (count < 1) return null;
+
+ return (
+
{shortNumberFormat(count)}
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/notification_counter/index.scss b/app/javascript/gabsocial/components/notification_counter/notification_counter.scss
similarity index 100%
rename from app/javascript/gabsocial/components/notification_counter/index.scss
rename to app/javascript/gabsocial/components/notification_counter/notification_counter.scss
diff --git a/app/javascript/gabsocial/components/panel/index.js b/app/javascript/gabsocial/components/panel/index.js
new file mode 100644
index 00000000..13f4b326
--- /dev/null
+++ b/app/javascript/gabsocial/components/panel/index.js
@@ -0,0 +1,9 @@
+import SignUpPanel from './sign_up_panel';
+import TrendsPanel from './trends_panel';
+import WhoToFollowPanel from './who_to_follow_panel';
+
+export {
+ SignUpPanel,
+ TrendsPanel,
+ WhoToFollowPanel,
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/panel/panel.scss b/app/javascript/gabsocial/components/panel/panel.scss
new file mode 100644
index 00000000..5cdad7d1
--- /dev/null
+++ b/app/javascript/gabsocial/components/panel/panel.scss
@@ -0,0 +1,132 @@
+.panel {
+ display: flex;
+ width: 100%;
+ border-radius: 10px;
+ flex-direction: column;
+ height: auto;
+ box-sizing: border-box;
+ background: $gab-background-container;
+
+ body.theme-gabsocial-light & {
+ // @include light-theme-shadow();
+ background: $gab-background-container-light;
+ }
+
+ &:not(:last-of-type) {
+ margin-bottom: 10px;
+ }
+
+ &__content {
+ width: 100%;
+ padding-top: 8px;
+ }
+
+ &__list {
+ padding: 0 5px;
+ }
+
+ &__subtitle {
+ display: block;
+ padding: 0 15px;
+ color: $secondary-text-color;
+ }
+
+ &__form {
+ display: block;
+ padding: 15px;
+
+ &.button {
+ width: 100%;
+ }
+ }
+
+ .wtf-panel-list-item {
+ display: block;
+ padding-bottom: 10px;
+
+ &:not(:first-of-type) {
+ margin-top: 12px;
+ }
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: row;
+ min-height: 46px;
+ margin-left: 58px;
+ }
+
+ &__account-block {
+ display: flex;
+ position: relative;
+ align-items: baseline;
+ padding-right: 10px;
+
+ &__avatar {
+ height: 46px;
+ width: 46px;
+ background-color: red;
+ left: -58px;
+ position: absolute;
+ }
+
+ &__name {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: column;
+ margin-top: 6px;
+
+ &__name {
+ color: $primary-text-color;
+ font-size: 14px;
+ font-weight: bold;
+ line-height: 16px;
+ margin-bottom: 2px;
+ max-height: 32px; //2 lines of text
+ overflow: hidden;
+ }
+
+ &__username {
+ color: $lighter-text-color;
+ font-size: 12px;
+ line-height: 14px;
+ }
+ }
+ }
+
+ &__follow-block {
+ margin-left: auto;
+ padding-top: 6px;
+
+ &__button {
+ display: flex;
+ }
+
+ &__icon {
+ color: $primary-text-color;
+ }
+ }
+ }
+}
+
+.panel-header {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: 10px;
+ padding: 15px 15px 0 15px;
+
+ &__icon {
+ margin-right: 10px;
+ }
+
+ &__title {
+ flex: 1 1;
+ color: $primary-text-color;
+ font-size: 16px;
+ font-weight: bold;
+ line-height: 19px;
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/panel/panel_layout.js b/app/javascript/gabsocial/components/panel/panel_layout.js
new file mode 100644
index 00000000..3fcef739
--- /dev/null
+++ b/app/javascript/gabsocial/components/panel/panel_layout.js
@@ -0,0 +1,26 @@
+import './panel.scss';
+
+export default class PanelLayout extends PureComponent {
+ static propTypes = {
+ title: PropTypes.string,
+ icon: PropTypes.string,
+ children: PropTypes.node,
+ };
+
+ render() {
+ const {title, icon, children} = this.props;
+
+ return (
+
+
+ {icon && }
+ {title}
+
+
+ {children}
+
+
+ );
+ };
+
+};
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/panel/sign_up_panel.js b/app/javascript/gabsocial/components/panel/sign_up_panel.js
new file mode 100644
index 00000000..f8aa2e73
--- /dev/null
+++ b/app/javascript/gabsocial/components/panel/sign_up_panel.js
@@ -0,0 +1,31 @@
+import { injectIntl, defineMessages } from 'react-intl';
+import { me } from '../../initial_state';
+import PanelLayout from './panel_layout';
+
+const messages = defineMessages({
+ title: { id: 'signup_panel.title', defaultMessage: 'New to Gab?' },
+ subtitle: { id: 'signup_panel.subtitle', defaultMessage: 'Sign up now to speak freely.' },
+ register: { id: 'account.register', defaultMessage: 'Sign up?' },
+});
+
+export default @injectIntl
+class SignUpPanel extends PureComponent {
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ }
+
+ render() {
+ if (me) return null;
+
+ const { intl } = this.props;
+
+ return (
+
+ {intl.formatMessage(messages.subtitle)}
+
+
+ )
+ }
+}
diff --git a/app/javascript/gabsocial/components/panel/trends_panel.js b/app/javascript/gabsocial/components/panel/trends_panel.js
new file mode 100644
index 00000000..4d35ebf2
--- /dev/null
+++ b/app/javascript/gabsocial/components/panel/trends_panel.js
@@ -0,0 +1,53 @@
+import { injectIntl, defineMessages } from 'react-intl';
+import { fetchTrends } from '../../actions/trends';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import TrendingItem from '../../components/trending_item';
+import PanelLayout from './panel_layout';
+
+const messages = defineMessages({
+ title: { id:'trends.title', defaultMessage: 'Trends' },
+});
+
+const mapStateToProps = state => ({
+ trends: state.getIn(['trends', 'items']),
+});
+
+const mapDispatchToProps = dispatch => {
+ return {
+ fetchTrends: () => dispatch(fetchTrends()),
+ }
+};
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+class TrendsPanel extends ImmutablePureComponent {
+
+ static propTypes = {
+ trends: ImmutablePropTypes.list.isRequired,
+ fetchTrends: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ componentDidMount () {
+ this.props.fetchTrends();
+ }
+
+ render() {
+ const { intl, trends } = this.props;
+
+ if (trends.isEmpty()) {
+ return null;
+ }
+
+ return (
+
+
+ {trends && trends.map(hashtag => (
+
+ ))}
+
+
+ );
+ }
+};
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/panel/who_to_follow_panel.js b/app/javascript/gabsocial/components/panel/who_to_follow_panel.js
new file mode 100644
index 00000000..7ec26a00
--- /dev/null
+++ b/app/javascript/gabsocial/components/panel/who_to_follow_panel.js
@@ -0,0 +1,62 @@
+import { defineMessages, injectIntl } from 'react-intl';
+import { fetchSuggestions, dismissSuggestion } from '../../actions/suggestions';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import AccountContainer from '../../containers/account_container';
+import PanelLayout from './panel_layout';
+
+const messages = defineMessages({
+ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
+ title: { id: 'who_to_follow.title', defaultMessage: 'Who To Follow' },
+});
+
+const mapStateToProps = state => ({
+ suggestions: state.getIn(['suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => {
+ return {
+ fetchSuggestions: () => dispatch(fetchSuggestions()),
+ dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
+ }
+};
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+class WhoToFollowPanel extends ImmutablePureComponent {
+
+ static propTypes = {
+ suggestions: ImmutablePropTypes.list.isRequired,
+ fetchSuggestions: PropTypes.func.isRequired,
+ dismissSuggestion: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ componentDidMount () {
+ this.props.fetchSuggestions();
+ }
+
+ render() {
+ const { intl, suggestions, dismissSuggestion } = this.props;
+
+ if (suggestions.isEmpty()) {
+ return null;
+ }
+
+ return (
+
+
+ {suggestions && suggestions.map(accountId => (
+
+ ))}
+
+
+ );
+ };
+};
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/permalink/index.js b/app/javascript/gabsocial/components/permalink/index.js
index 84c992dd..27745e0a 100644
--- a/app/javascript/gabsocial/components/permalink/index.js
+++ b/app/javascript/gabsocial/components/permalink/index.js
@@ -1,41 +1 @@
-import classNames from 'classnames';
-
-export default class Permalink extends PureComponent {
-
- static contextTypes = {
- router: PropTypes.object,
- };
-
- static propTypes = {
- className: PropTypes.string,
- href: PropTypes.string.isRequired,
- to: PropTypes.string.isRequired,
- children: PropTypes.node,
- onInterceptClick: PropTypes.func,
- };
-
- handleClick = e => {
- if (this.props.onInterceptClick && this.props.onInterceptClick()) {
- e.preventDefault();
- return;
- }
-
- if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.context.router.history.push(this.props.to);
- }
- }
-
- render () {
- const { href, children, className, onInterceptClick, ...other } = this.props;
-
- const classes = classNames('permalink', className);
-
- return (
-
- {children}
-
- );
- }
-
-}
+export { default } from './permalink';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/permalink/permalink.js b/app/javascript/gabsocial/components/permalink/permalink.js
new file mode 100644
index 00000000..84c992dd
--- /dev/null
+++ b/app/javascript/gabsocial/components/permalink/permalink.js
@@ -0,0 +1,41 @@
+import classNames from 'classnames';
+
+export default class Permalink extends PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ className: PropTypes.string,
+ href: PropTypes.string.isRequired,
+ to: PropTypes.string.isRequired,
+ children: PropTypes.node,
+ onInterceptClick: PropTypes.func,
+ };
+
+ handleClick = e => {
+ if (this.props.onInterceptClick && this.props.onInterceptClick()) {
+ e.preventDefault();
+ return;
+ }
+
+ if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ this.context.router.history.push(this.props.to);
+ }
+ }
+
+ render () {
+ const { href, children, className, onInterceptClick, ...other } = this.props;
+
+ const classes = classNames('permalink', className);
+
+ return (
+
+ {children}
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/poll/index.js b/app/javascript/gabsocial/components/poll/index.js
index 2aed3ec2..d03385a3 100644
--- a/app/javascript/gabsocial/components/poll/index.js
+++ b/app/javascript/gabsocial/components/poll/index.js
@@ -1,175 +1 @@
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import escapeTextContentForBrowser from 'escape-html';
-import spring from 'react-motion/lib/spring';
-import Motion from '../../features/ui/util/optional_motion';
-import { vote, fetchPoll } from '../../actions/polls';
-import emojify from '../../features/emoji/emoji';
-import RelativeTimestamp from '../relative_timestamp';
-import Button from '../button';
-
-import './index.scss';
-
-const messages = defineMessages({
- closed: { id: 'poll.closed', defaultMessage: 'Closed' },
-});
-
-const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
- obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
- return obj;
-}, {});
-
-export default @injectIntl
-class Poll extends ImmutablePureComponent {
-
- static propTypes = {
- poll: ImmutablePropTypes.map,
- intl: PropTypes.object.isRequired,
- dispatch: PropTypes.func,
- disabled: PropTypes.bool,
- };
-
- state = {
- selected: {},
- };
-
- handleOptionChange = e => {
- const { target: { value } } = e;
-
- if (this.props.poll.get('multiple')) {
- const tmp = { ...this.state.selected };
- if (tmp[value]) {
- delete tmp[value];
- } else {
- tmp[value] = true;
- }
- this.setState({ selected: tmp });
- } else {
- const tmp = {};
- tmp[value] = true;
- this.setState({ selected: tmp });
- }
- };
-
- handleVote = () => {
- if (this.props.disabled) return;
-
- this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
- };
-
- handleRefresh = () => {
- if (this.props.disabled) return;
-
- this.props.dispatch(fetchPoll(this.props.poll.get('id')));
- };
-
- renderOption (option, optionIndex) {
- const { poll, disabled } = this.props;
- const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
- const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
- const active = !!this.state.selected[`${optionIndex}`];
- const showResults = poll.get('voted') || poll.get('expired');
- const multiple = poll.get('multiple');
-
- let titleEmojified = option.get('title_emojified');
- if (!titleEmojified) {
- const emojiMap = makeEmojiMap(poll);
- titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
- }
-
- const chartClasses = classNames('poll__chart', {
- 'poll__chart--leading': leading,
- });
-
- const textClasses = classNames('poll__text', {
- selectable: !showResults,
- });
-
- const inputClasses = classNames('poll__input', {
- 'poll__input--checkbox': multiple,
- 'poll__input--active': active,
- });
-
- return (
-
- {
- showResults && (
-
- {({ width }) =>
-
- }
-
- )
- }
-
-
-
-
- {!showResults && }
- {showResults && {Math.round(percent)}% }
-
-
-
-
- );
- }
-
- render () {
- const { poll, intl } = this.props;
-
- if (!poll) return null;
-
- const timeRemaining = poll.get('expired') ?
- intl.formatMessage(messages.closed)
- :
;
- const showResults = poll.get('voted') || poll.get('expired');
- const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
-
- return (
-
-
- {poll.get('options').map((option, i) => this.renderOption(option, i))}
-
-
-
- {
- !showResults &&
-
-
-
- }
- {
- showResults && !this.props.disabled &&
-
-
-
-
- ·
-
- }
-
- {
- poll.get('expires_at') &&
- · {timeRemaining}
- }
-
-
- );
- }
-
-}
+export { default } from './poll';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/poll/poll.js b/app/javascript/gabsocial/components/poll/poll.js
new file mode 100644
index 00000000..5c05e6e7
--- /dev/null
+++ b/app/javascript/gabsocial/components/poll/poll.js
@@ -0,0 +1,180 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import escapeTextContentForBrowser from 'escape-html';
+import spring from 'react-motion/lib/spring';
+import Motion from '../../features/ui/util/optional_motion';
+import { vote, fetchPoll } from '../../actions/polls';
+import emojify from '../emoji/emoji';
+import RelativeTimestamp from '../relative_timestamp/relative_timestamp';
+import Button from '../button';
+
+import './poll.scss';
+
+const mapStateToProps = (state, { pollId }) => ({
+ poll: state.getIn(['polls', pollId]),
+});
+
+const messages = defineMessages({
+ closed: { id: 'poll.closed', defaultMessage: 'Closed' },
+});
+
+const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
+ obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
+ return obj;
+}, {});
+
+export default @connect(mapStateToProps)
+@injectIntl
+class Poll extends ImmutablePureComponent {
+
+ static propTypes = {
+ poll: ImmutablePropTypes.map,
+ intl: PropTypes.object.isRequired,
+ dispatch: PropTypes.func,
+ disabled: PropTypes.bool,
+ };
+
+ state = {
+ selected: {},
+ };
+
+ handleOptionChange = e => {
+ const { target: { value } } = e;
+
+ if (this.props.poll.get('multiple')) {
+ const tmp = { ...this.state.selected };
+ if (tmp[value]) {
+ delete tmp[value];
+ } else {
+ tmp[value] = true;
+ }
+ this.setState({ selected: tmp });
+ } else {
+ const tmp = {};
+ tmp[value] = true;
+ this.setState({ selected: tmp });
+ }
+ };
+
+ handleVote = () => {
+ if (this.props.disabled) return;
+
+ this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
+ };
+
+ handleRefresh = () => {
+ if (this.props.disabled) return;
+
+ this.props.dispatch(fetchPoll(this.props.poll.get('id')));
+ };
+
+ renderOption (option, optionIndex) {
+ const { poll, disabled } = this.props;
+ const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
+ const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
+ const active = !!this.state.selected[`${optionIndex}`];
+ const showResults = poll.get('voted') || poll.get('expired');
+ const multiple = poll.get('multiple');
+
+ let titleEmojified = option.get('title_emojified');
+ if (!titleEmojified) {
+ const emojiMap = makeEmojiMap(poll);
+ titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
+ }
+
+ const chartClasses = classNames('poll__chart', {
+ 'poll__chart--leading': leading,
+ });
+
+ const textClasses = classNames('poll__text', {
+ selectable: !showResults,
+ });
+
+ const inputClasses = classNames('poll__input', {
+ 'poll__input--checkbox': multiple,
+ 'poll__input--active': active,
+ });
+
+ return (
+
+ {
+ showResults && (
+
+ {({ width }) =>
+
+ }
+
+ )
+ }
+
+
+
+
+ {!showResults && }
+ {showResults && {Math.round(percent)}% }
+
+
+
+
+ );
+ }
+
+ render () {
+ const { poll, intl } = this.props;
+
+ if (!poll) return null;
+
+ const timeRemaining = poll.get('expired') ?
+ intl.formatMessage(messages.closed)
+ :
;
+ const showResults = poll.get('voted') || poll.get('expired');
+ const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
+
+ return (
+
+
+ {poll.get('options').map((option, i) => this.renderOption(option, i))}
+
+
+
+ {
+ !showResults &&
+
+
+
+ }
+ {
+ showResults && !this.props.disabled &&
+
+
+
+
+ ·
+
+ }
+
+ {
+ poll.get('expires_at') &&
+ · {timeRemaining}
+ }
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/poll/index.scss b/app/javascript/gabsocial/components/poll/poll.scss
similarity index 98%
rename from app/javascript/gabsocial/components/poll/index.scss
rename to app/javascript/gabsocial/components/poll/poll.scss
index 318f0d33..6358c576 100644
--- a/app/javascript/gabsocial/components/poll/index.scss
+++ b/app/javascript/gabsocial/components/poll/poll.scss
@@ -79,10 +79,9 @@
flex: 0 0 auto;
margin-right: 10px;
top: -1px;
- border-radius: 50%;
vertical-align: middle;
- @include size(18px);
+ @include circle(18px);
&--checkbox {
border-radius: 4px;
diff --git a/app/javascript/gabsocial/components/promo_panel/index.js b/app/javascript/gabsocial/components/promo_panel/index.js
new file mode 100644
index 00000000..abc14f9a
--- /dev/null
+++ b/app/javascript/gabsocial/components/promo_panel/index.js
@@ -0,0 +1 @@
+export { default } from './promo_panel';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/promo_panel/promo_panel.js b/app/javascript/gabsocial/components/promo_panel/promo_panel.js
new file mode 100644
index 00000000..92c3b736
--- /dev/null
+++ b/app/javascript/gabsocial/components/promo_panel/promo_panel.js
@@ -0,0 +1,43 @@
+import { FormattedMessage } from 'react-intl';
+import Icon from 'gabsocial/components/icon';
+
+import './promo_panel.scss';
+
+export default class PromoPanel extends PureComponent {
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/javascript/gabsocial/components/promo_panel/promo_panel.scss b/app/javascript/gabsocial/components/promo_panel/promo_panel.scss
new file mode 100644
index 00000000..3f16194a
--- /dev/null
+++ b/app/javascript/gabsocial/components/promo_panel/promo_panel.scss
@@ -0,0 +1,29 @@
+.promo-panel {
+ margin-top: 10px;
+ padding: 10px 10px 20px 10px;
+ border-bottom: 1px solid lighten($ui-base-color, 4%);
+}
+
+.promo-panel-item {
+ display: block;
+
+ &:not(:first-of-type) {
+ margin-top: 20px;
+ }
+
+ &__icon {
+ margin-right: 12px;
+ }
+
+ &__message {
+ display: block;
+ font-size: 14px;
+ line-height: 16px;
+ margin-top: 6px;
+ color: $primary-text-color;
+
+ &--dark {
+ color: $ui-secondary-color;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/relative_timestamp/index.js b/app/javascript/gabsocial/components/relative_timestamp/index.js
index 4e2c7ac2..2c1b0d1c 100644
--- a/app/javascript/gabsocial/components/relative_timestamp/index.js
+++ b/app/javascript/gabsocial/components/relative_timestamp/index.js
@@ -1,184 +1 @@
-import { injectIntl, defineMessages } from 'react-intl';
-
-const messages = defineMessages({
- just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
- seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
- minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
- hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
- days: { id: 'relative_time.days', defaultMessage: '{number}d' },
- moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
- seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
- minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
- hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
- days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
-});
-
-const dateFormatOptions = {
- hour12: false,
- year: 'numeric',
- month: 'short',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
-};
-
-const shortDateFormatOptions = {
- month: 'short',
- day: 'numeric',
-};
-
-const SECOND = 1000;
-const MINUTE = 1000 * 60;
-const HOUR = 1000 * 60 * 60;
-const DAY = 1000 * 60 * 60 * 24;
-
-const MAX_DELAY = 2147483647;
-
-const selectUnits = delta => {
- const absDelta = Math.abs(delta);
-
- if (absDelta < MINUTE) {
- return 'second';
- } else if (absDelta < HOUR) {
- return 'minute';
- } else if (absDelta < DAY) {
- return 'hour';
- }
-
- return 'day';
-};
-
-const getUnitDelay = units => {
- switch (units) {
- case 'second':
- return SECOND;
- case 'minute':
- return MINUTE;
- case 'hour':
- return HOUR;
- case 'day':
- return DAY;
- default:
- return MAX_DELAY;
- }
-};
-
-export const timeAgoString = (intl, date, now, year) => {
- const delta = now - date.getTime();
-
- let relativeTime;
-
- if (delta < 10 * SECOND) {
- relativeTime = intl.formatMessage(messages.just_now);
- } else if (delta < 7 * DAY) {
- if (delta < MINUTE) {
- relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
- } else if (delta < HOUR) {
- relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
- } else if (delta < DAY) {
- relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
- } else {
- relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
- }
- } else if (date.getFullYear() === year) {
- relativeTime = intl.formatDate(date, shortDateFormatOptions);
- } else {
- relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
- }
-
- return relativeTime;
-};
-
-const timeRemainingString = (intl, date, now) => {
- const delta = date.getTime() - now;
-
- let relativeTime;
-
- if (delta < 10 * SECOND) {
- relativeTime = intl.formatMessage(messages.moments_remaining);
- } else if (delta < MINUTE) {
- relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
- } else if (delta < HOUR) {
- relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
- } else if (delta < DAY) {
- relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
- } else {
- relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
- }
-
- return relativeTime;
-};
-
-export default @injectIntl
-class RelativeTimestamp extends Component {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- timestamp: PropTypes.string.isRequired,
- year: PropTypes.number.isRequired,
- futureDate: PropTypes.bool,
- };
-
- state = {
- now: this.props.intl.now(),
- };
-
- static defaultProps = {
- year: (new Date()).getFullYear(),
- };
-
- shouldComponentUpdate (nextProps, nextState) {
- // As of right now the locale doesn't change without a new page load,
- // but we might as well check in case that ever changes.
- return this.props.timestamp !== nextProps.timestamp ||
- this.props.intl.locale !== nextProps.intl.locale ||
- this.state.now !== nextState.now;
- }
-
- componentWillReceiveProps (nextProps) {
- if (this.props.timestamp !== nextProps.timestamp) {
- this.setState({ now: this.props.intl.now() });
- }
- }
-
- componentDidMount () {
- this._scheduleNextUpdate(this.props, this.state);
- }
-
- componentWillUpdate (nextProps, nextState) {
- this._scheduleNextUpdate(nextProps, nextState);
- }
-
- componentWillUnmount () {
- clearTimeout(this._timer);
- }
-
- _scheduleNextUpdate (props, state) {
- clearTimeout(this._timer);
-
- const { timestamp } = props;
- const delta = (new Date(timestamp)).getTime() - state.now;
- const unitDelay = getUnitDelay(selectUnits(delta));
- const unitRemainder = Math.abs(delta % unitDelay);
- const updateInterval = 1000 * 10;
- const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
-
- this._timer = setTimeout(() => {
- this.setState({ now: this.props.intl.now() });
- }, delay);
- }
-
- render () {
- const { timestamp, intl, year, futureDate } = this.props;
-
- const date = new Date(timestamp);
- const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year);
-
- return (
-
- {relativeTime}
-
- );
- }
-
-}
+export { default } from './relative_timestamp';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/relative_timestamp/relative_timestamp.js b/app/javascript/gabsocial/components/relative_timestamp/relative_timestamp.js
new file mode 100644
index 00000000..4e2c7ac2
--- /dev/null
+++ b/app/javascript/gabsocial/components/relative_timestamp/relative_timestamp.js
@@ -0,0 +1,184 @@
+import { injectIntl, defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+ just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
+ seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
+ minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
+ hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
+ days: { id: 'relative_time.days', defaultMessage: '{number}d' },
+ moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
+ seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
+ minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
+ hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
+ days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
+});
+
+const dateFormatOptions = {
+ hour12: false,
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+};
+
+const shortDateFormatOptions = {
+ month: 'short',
+ day: 'numeric',
+};
+
+const SECOND = 1000;
+const MINUTE = 1000 * 60;
+const HOUR = 1000 * 60 * 60;
+const DAY = 1000 * 60 * 60 * 24;
+
+const MAX_DELAY = 2147483647;
+
+const selectUnits = delta => {
+ const absDelta = Math.abs(delta);
+
+ if (absDelta < MINUTE) {
+ return 'second';
+ } else if (absDelta < HOUR) {
+ return 'minute';
+ } else if (absDelta < DAY) {
+ return 'hour';
+ }
+
+ return 'day';
+};
+
+const getUnitDelay = units => {
+ switch (units) {
+ case 'second':
+ return SECOND;
+ case 'minute':
+ return MINUTE;
+ case 'hour':
+ return HOUR;
+ case 'day':
+ return DAY;
+ default:
+ return MAX_DELAY;
+ }
+};
+
+export const timeAgoString = (intl, date, now, year) => {
+ const delta = now - date.getTime();
+
+ let relativeTime;
+
+ if (delta < 10 * SECOND) {
+ relativeTime = intl.formatMessage(messages.just_now);
+ } else if (delta < 7 * DAY) {
+ if (delta < MINUTE) {
+ relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
+ } else if (delta < HOUR) {
+ relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
+ } else if (delta < DAY) {
+ relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
+ } else {
+ relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
+ }
+ } else if (date.getFullYear() === year) {
+ relativeTime = intl.formatDate(date, shortDateFormatOptions);
+ } else {
+ relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
+ }
+
+ return relativeTime;
+};
+
+const timeRemainingString = (intl, date, now) => {
+ const delta = date.getTime() - now;
+
+ let relativeTime;
+
+ if (delta < 10 * SECOND) {
+ relativeTime = intl.formatMessage(messages.moments_remaining);
+ } else if (delta < MINUTE) {
+ relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
+ } else if (delta < HOUR) {
+ relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
+ } else if (delta < DAY) {
+ relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
+ } else {
+ relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
+ }
+
+ return relativeTime;
+};
+
+export default @injectIntl
+class RelativeTimestamp extends Component {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ timestamp: PropTypes.string.isRequired,
+ year: PropTypes.number.isRequired,
+ futureDate: PropTypes.bool,
+ };
+
+ state = {
+ now: this.props.intl.now(),
+ };
+
+ static defaultProps = {
+ year: (new Date()).getFullYear(),
+ };
+
+ shouldComponentUpdate (nextProps, nextState) {
+ // As of right now the locale doesn't change without a new page load,
+ // but we might as well check in case that ever changes.
+ return this.props.timestamp !== nextProps.timestamp ||
+ this.props.intl.locale !== nextProps.intl.locale ||
+ this.state.now !== nextState.now;
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (this.props.timestamp !== nextProps.timestamp) {
+ this.setState({ now: this.props.intl.now() });
+ }
+ }
+
+ componentDidMount () {
+ this._scheduleNextUpdate(this.props, this.state);
+ }
+
+ componentWillUpdate (nextProps, nextState) {
+ this._scheduleNextUpdate(nextProps, nextState);
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this._timer);
+ }
+
+ _scheduleNextUpdate (props, state) {
+ clearTimeout(this._timer);
+
+ const { timestamp } = props;
+ const delta = (new Date(timestamp)).getTime() - state.now;
+ const unitDelay = getUnitDelay(selectUnits(delta));
+ const unitRemainder = Math.abs(delta % unitDelay);
+ const updateInterval = 1000 * 10;
+ const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
+
+ this._timer = setTimeout(() => {
+ this.setState({ now: this.props.intl.now() });
+ }, delay);
+ }
+
+ render () {
+ const { timestamp, intl, year, futureDate } = this.props;
+
+ const date = new Date(timestamp);
+ const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year);
+
+ return (
+
+ {relativeTime}
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/scrollable_list/index.js b/app/javascript/gabsocial/components/scrollable_list/index.js
index 6204c4c4..1c1a0fd0 100644
--- a/app/javascript/gabsocial/components/scrollable_list/index.js
+++ b/app/javascript/gabsocial/components/scrollable_list/index.js
@@ -1,245 +1 @@
-import React, { PureComponent } from 'react';
-import { throttle } from 'lodash';
-import { List as ImmutableList } from 'immutable';
-import ColumnIndicator from '../column_indicator';
-import IntersectionObserverArticleContainer from '../../containers/intersection_observer_article_container';
-import LoadMore from '../load_more';
-import IntersectionObserverWrapper from '../../features/ui/util/intersection_observer_wrapper';
-
-import './index.scss';
-
-const MOUSE_IDLE_DELAY = 300;
-
-export default class ScrollableList extends PureComponent {
-
- static contextTypes = {
- router: PropTypes.object,
- };
-
- static propTypes = {
- scrollKey: PropTypes.string.isRequired,
- onLoadMore: PropTypes.func,
- isLoading: PropTypes.bool,
- showLoading: PropTypes.bool,
- hasMore: PropTypes.bool,
- emptyMessage: PropTypes.node,
- children: PropTypes.node,
- onScrollToTop: PropTypes.func,
- onScroll: PropTypes.func,
- };
-
- state = {
- cachedMediaWidth: 250, // Default media/card width using default Gab Social theme
- };
-
- intersectionObserverWrapper = new IntersectionObserverWrapper();
-
- mouseIdleTimer = null;
- mouseMovedRecently = false;
- lastScrollWasSynthetic = false;
- scrollToTopOnMouseIdle = false;
-
- setScrollTop = newScrollTop => {
- if (this.documentElement.scrollTop !== newScrollTop) {
- this.lastScrollWasSynthetic = true;
- this.documentElement.scrollTop = newScrollTop;
- }
- };
-
- 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);
-
- // Only set if we just started moving and are scrolled to the top.
- if (!this.mouseMovedRecently && this.documentElement.scrollTop === 0) {
- this.scrollToTopOnMouseIdle = true;
- }
-
- // Save setting this flag for last, so we can do the comparison above.
- this.mouseMovedRecently = true;
- }, MOUSE_IDLE_DELAY / 2);
-
- handleMouseIdle = () => {
- if (this.scrollToTopOnMouseIdle) {
- this.setScrollTop(0);
- }
-
- this.mouseMovedRecently = false;
- this.scrollToTopOnMouseIdle = false;
- }
-
- componentDidMount () {
- this.window = window;
- this.documentElement = document.scrollingElement || document.documentElement;
-
- this.attachScrollListener();
- this.attachIntersectionObserver();
- // Handle initial scroll posiiton
- this.handleScroll();
- }
-
- getScrollPosition = () => {
- if (this.documentElement && (this.documentElement.scrollTop > 0 || this.mouseMovedRecently)) {
- return { height: this.documentElement.scrollHeight, top: this.documentElement.scrollTop };
- }
-
- return null;
- }
-
- updateScrollBottom = (snapshot) => {
- const newScrollTop = this.documentElement.scrollHeight - snapshot;
-
- this.setScrollTop(newScrollTop);
- }
-
- componentDidUpdate (prevProps, prevState, snapshot) {
- // 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.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 } = this.documentElement;
- const { innerHeight } = this.window;
- const offset = scrollHeight - scrollTop - innerHeight;
-
- if (400 > 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;
- }
-
- return null;
- }
-
- cacheMediaWidth = (width) => {
- if (width && this.state.cachedMediaWidth !== width) {
- this.setState({ cachedMediaWidth: width });
- }
- }
-
- componentWillUnmount () {
- this.clearMouseIdleTimer();
- this.detachScrollListener();
- this.detachIntersectionObserver();
- }
-
- attachIntersectionObserver () {
- this.intersectionObserverWrapper.connect();
- }
-
- detachIntersectionObserver () {
- this.intersectionObserverWrapper.disconnect();
- }
-
- getFirstChildKey (props) {
- const { children } = props;
- let firstChild = children;
-
- if (children instanceof ImmutableList) {
- firstChild = children.get(0);
- } else if (Array.isArray(children)) {
- firstChild = children[0];
- }
-
- return firstChild && firstChild.key;
- }
-
- handleLoadMore = e => {
- e.preventDefault();
- this.props.onLoadMore();
- }
-
- render () {
- const { children, scrollKey, showLoading, isLoading, hasMore, emptyMessage, onLoadMore } = this.props;
- const childrenCount = React.Children.count(children);
-
- const trackScroll = true; //placeholder
-
- const loadMore = (hasMore && onLoadMore) ?
: null;
-
- if (showLoading) {
- return (
);
- } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
- return (
-
-
- {React.Children.map(this.props.children, (child, index) => (
-
- {React.cloneElement(child, {
- getScrollPosition: this.getScrollPosition,
- updateScrollBottom: this.updateScrollBottom,
- cachedMediaWidth: this.state.cachedMediaWidth,
- cacheMediaWidth: this.cacheMediaWidth,
- })}
-
- ))}
-
- {loadMore}
-
-
- );
- }
-
- return (
);
- }
-
-}
+export { default } from './scrollable_list';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/scrollable_list/scrollable_list.js b/app/javascript/gabsocial/components/scrollable_list/scrollable_list.js
new file mode 100644
index 00000000..33dd5d0e
--- /dev/null
+++ b/app/javascript/gabsocial/components/scrollable_list/scrollable_list.js
@@ -0,0 +1,244 @@
+import { throttle } from 'lodash';
+import { List as ImmutableList } from 'immutable';
+import ColumnIndicator from '../column_indicator';
+import IntersectionObserverArticleContainer from '../../containers/intersection_observer_article_container';
+import LoadMore from '../load_more';
+import IntersectionObserverWrapper from '../../features/ui/util/intersection_observer_wrapper';
+
+import './scrollable_list.scss';
+
+const MOUSE_IDLE_DELAY = 300;
+
+export default class ScrollableList extends PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ scrollKey: PropTypes.string.isRequired,
+ onLoadMore: PropTypes.func,
+ isLoading: PropTypes.bool,
+ showLoading: PropTypes.bool,
+ hasMore: PropTypes.bool,
+ emptyMessage: PropTypes.node,
+ children: PropTypes.node,
+ onScrollToTop: PropTypes.func,
+ onScroll: PropTypes.func,
+ };
+
+ state = {
+ cachedMediaWidth: 250, // Default media/card width using default Gab Social theme
+ };
+
+ intersectionObserverWrapper = new IntersectionObserverWrapper();
+
+ mouseIdleTimer = null;
+ mouseMovedRecently = false;
+ lastScrollWasSynthetic = false;
+ scrollToTopOnMouseIdle = false;
+
+ setScrollTop = newScrollTop => {
+ if (this.documentElement.scrollTop !== newScrollTop) {
+ this.lastScrollWasSynthetic = true;
+ this.documentElement.scrollTop = newScrollTop;
+ }
+ };
+
+ 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);
+
+ // Only set if we just started moving and are scrolled to the top.
+ if (!this.mouseMovedRecently && this.documentElement.scrollTop === 0) {
+ this.scrollToTopOnMouseIdle = true;
+ }
+
+ // Save setting this flag for last, so we can do the comparison above.
+ this.mouseMovedRecently = true;
+ }, MOUSE_IDLE_DELAY / 2);
+
+ handleMouseIdle = () => {
+ if (this.scrollToTopOnMouseIdle) {
+ this.setScrollTop(0);
+ }
+
+ this.mouseMovedRecently = false;
+ this.scrollToTopOnMouseIdle = false;
+ }
+
+ componentDidMount () {
+ this.window = window;
+ this.documentElement = document.scrollingElement || document.documentElement;
+
+ this.attachScrollListener();
+ this.attachIntersectionObserver();
+ // Handle initial scroll posiiton
+ this.handleScroll();
+ }
+
+ getScrollPosition = () => {
+ if (this.documentElement && (this.documentElement.scrollTop > 0 || this.mouseMovedRecently)) {
+ return { height: this.documentElement.scrollHeight, top: this.documentElement.scrollTop };
+ }
+
+ return null;
+ }
+
+ updateScrollBottom = (snapshot) => {
+ const newScrollTop = this.documentElement.scrollHeight - snapshot;
+
+ this.setScrollTop(newScrollTop);
+ }
+
+ componentDidUpdate (prevProps, prevState, snapshot) {
+ // 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.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 } = this.documentElement;
+ const { innerHeight } = this.window;
+ const offset = scrollHeight - scrollTop - innerHeight;
+
+ if (400 > 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;
+ }
+
+ return null;
+ }
+
+ cacheMediaWidth = (width) => {
+ if (width && this.state.cachedMediaWidth !== width) {
+ this.setState({ cachedMediaWidth: width });
+ }
+ }
+
+ componentWillUnmount () {
+ this.clearMouseIdleTimer();
+ this.detachScrollListener();
+ this.detachIntersectionObserver();
+ }
+
+ attachIntersectionObserver () {
+ this.intersectionObserverWrapper.connect();
+ }
+
+ detachIntersectionObserver () {
+ this.intersectionObserverWrapper.disconnect();
+ }
+
+ getFirstChildKey (props) {
+ const { children } = props;
+ let firstChild = children;
+
+ if (children instanceof ImmutableList) {
+ firstChild = children.get(0);
+ } else if (Array.isArray(children)) {
+ firstChild = children[0];
+ }
+
+ return firstChild && firstChild.key;
+ }
+
+ handleLoadMore = e => {
+ e.preventDefault();
+ this.props.onLoadMore();
+ }
+
+ render () {
+ const { children, scrollKey, showLoading, isLoading, hasMore, emptyMessage, onLoadMore } = this.props;
+ const childrenCount = React.Children.count(children);
+
+ const trackScroll = true; //placeholder
+
+ const loadMore = (hasMore && onLoadMore) ?
: null;
+
+ if (showLoading) {
+ return (
);
+ } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
+ return (
+
+
+ {React.Children.map(this.props.children, (child, index) => (
+
+ {React.cloneElement(child, {
+ getScrollPosition: this.getScrollPosition,
+ updateScrollBottom: this.updateScrollBottom,
+ cachedMediaWidth: this.state.cachedMediaWidth,
+ cacheMediaWidth: this.cacheMediaWidth,
+ })}
+
+ ))}
+
+ {loadMore}
+
+
+ );
+ }
+
+ return (
);
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/scrollable_list/index.scss b/app/javascript/gabsocial/components/scrollable_list/scrollable_list.scss
similarity index 100%
rename from app/javascript/gabsocial/components/scrollable_list/index.scss
rename to app/javascript/gabsocial/components/scrollable_list/scrollable_list.scss
diff --git a/app/javascript/gabsocial/components/setting_toggle/index.js b/app/javascript/gabsocial/components/setting_toggle/index.js
new file mode 100644
index 00000000..d6d85c2b
--- /dev/null
+++ b/app/javascript/gabsocial/components/setting_toggle/index.js
@@ -0,0 +1 @@
+export { default } from './setting_toggle';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/notifications/components/setting_toggle.js b/app/javascript/gabsocial/components/setting_toggle/setting_toggle.js
similarity index 96%
rename from app/javascript/gabsocial/features/notifications/components/setting_toggle.js
rename to app/javascript/gabsocial/components/setting_toggle/setting_toggle.js
index 3fd5ed5a..e2b2788f 100644
--- a/app/javascript/gabsocial/features/notifications/components/setting_toggle.js
+++ b/app/javascript/gabsocial/components/setting_toggle/setting_toggle.js
@@ -1,6 +1,8 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
+import './setting_toggle.scss';
+
export default class SettingToggle extends PureComponent {
static propTypes = {
diff --git a/app/javascript/gabsocial/components/setting_toggle/setting_toggle.scss b/app/javascript/gabsocial/components/setting_toggle/setting_toggle.scss
new file mode 100644
index 00000000..5f14927b
--- /dev/null
+++ b/app/javascript/gabsocial/components/setting_toggle/setting_toggle.scss
@@ -0,0 +1,12 @@
+.setting-toggle {
+ display: block;
+ line-height: 24px;
+
+ &__label {
+ color: $darker-text-color;
+ display: inline-block;
+ margin-bottom: 14px;
+ margin-left: 8px;
+ vertical-align: middle;
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/status/index.js b/app/javascript/gabsocial/components/status/index.js
index 7d481182..a460ff60 100644
--- a/app/javascript/gabsocial/components/status/index.js
+++ b/app/javascript/gabsocial/components/status/index.js
@@ -1,483 +1 @@
-import { NavLink } from 'react-router-dom';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { HotKeys } from 'react-hotkeys';
-import classNames from 'classnames';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import Avatar from '../avatar';
-import AvatarOverlay from '../avatar_overlay';
-import RelativeTimestamp from '../relative_timestamp';
-import DisplayName from '../display_name';
-import StatusContent from '../status_content';
-import StatusActionBar from '../status_action_bar';
-import Card from '../../features/status/components/card';
-import { MediaGallery, Video } from '../../features/ui/util/async-components';
-import Icon from '../icon';
-import PollContainer from '../../containers/poll_container';
-import { displayMedia } from '../../initial_state';
-
-import './index.scss';
-
-// We use the component (and not the container) since we do not want
-// to use the progress bar to show download progress
-import Bundle from '../../features/ui/components/bundle';
-
-export const textForScreenReader = (intl, status, rebloggedByText = false) => {
- const displayName = status.getIn(['account', 'display_name']);
-
- const values = [
- displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
- status.get('spoiler_text') && status.get('hidden')
- ? status.get('spoiler_text')
- : status.get('search_index').slice(status.get('spoiler_text').length),
- intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
- status.getIn(['account', 'acct']),
- ];
-
- if (rebloggedByText) {
- values.push(rebloggedByText);
- }
-
- return values.join(', ');
-};
-
-export const defaultMediaVisibility = status => {
- if (!status) return undefined;
-
- if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
- status = status.get('reblog');
- }
-
- return (displayMedia !== 'hide_all' && !status.get('sensitive')) || displayMedia === 'show_all';
-};
-
-export default
-@injectIntl
-class Status extends ImmutablePureComponent {
-
- static contextTypes = {
- router: PropTypes.object,
- };
-
- static propTypes = {
- status: ImmutablePropTypes.map,
- account: ImmutablePropTypes.map,
- onClick: PropTypes.func,
- onReply: PropTypes.func,
- onFavourite: PropTypes.func,
- onReblog: PropTypes.func,
- onDelete: PropTypes.func,
- onDirect: PropTypes.func,
- onMention: PropTypes.func,
- onPin: PropTypes.func,
- onOpenMedia: PropTypes.func,
- onOpenVideo: PropTypes.func,
- onBlock: PropTypes.func,
- onEmbed: PropTypes.func,
- onHeightChange: PropTypes.func,
- onToggleHidden: PropTypes.func,
- muted: PropTypes.bool,
- hidden: PropTypes.bool,
- unread: PropTypes.bool,
- onMoveUp: PropTypes.func,
- onMoveDown: PropTypes.func,
- showThread: PropTypes.bool,
- getScrollPosition: PropTypes.func,
- updateScrollBottom: PropTypes.func,
- cacheMediaWidth: PropTypes.func,
- cachedMediaWidth: PropTypes.number,
- group: ImmutablePropTypes.map,
- };
-
- // Avoid checking props that are functions (and whose equality will always
- // evaluate to false. See react-immutable-pure-component for usage.
- updateOnProps = ['status', 'account', 'muted', 'hidden'];
-
- state = {
- showMedia: defaultMediaVisibility(this.props.status),
- statusId: undefined,
- };
-
- // Track height changes we know about to compensate scrolling
- componentDidMount() {
- 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();
- }
-
- return null;
- }
-
- static getDerivedStateFromProps(nextProps, prevState) {
- if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
- return {
- showMedia: defaultMediaVisibility(nextProps.status),
- statusId: nextProps.status.get('id'),
- };
- }
-
- return null;
- }
-
- // Compensate height changes
- componentDidUpdate(prevProps, prevState, snapshot) {
- const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
-
- if (doShowCard && !this.didShowCard) {
- this.didShowCard = true;
-
- if (snapshot !== null && this.props.updateScrollBottom) {
- if (this.node && this.node.offsetTop < snapshot.top) {
- this.props.updateScrollBottom(snapshot.height - snapshot.top);
- }
- }
- }
- }
-
- 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 });
- };
-
- handleClick = () => {
- if (this.props.onClick) {
- this.props.onClick();
- return;
- }
-
- if (!this.context.router) return;
-
- this.context.router.history.push(
- `/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`
- );
- };
-
- handleExpandClick = e => {
- if (e.button === 0) {
- if (!this.context.router) return;
-
- this.context.router.history.push(
- `/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`
- );
- }
- };
-
- handleExpandedToggle = () => {
- this.props.onToggleHidden(this._properStatus());
- };
-
- renderLoadingMediaGallery() {
- return
;
- }
-
- renderLoadingVideoPlayer() {
- return
;
- }
-
- handleOpenVideo = (media, startTime) => {
- this.props.onOpenVideo(media, startTime);
- };
-
- handleHotkeyReply = e => {
- e.preventDefault();
- this.props.onReply(this._properStatus(), this.context.router.history);
- };
-
- handleHotkeyFavourite = () => {
- this.props.onFavourite(this._properStatus());
- };
-
- handleHotkeyBoost = e => {
- this.props.onReblog(this._properStatus(), e);
- };
-
- handleHotkeyMention = e => {
- e.preventDefault();
- this.props.onMention(this._properStatus().get('account'), this.context.router.history);
- };
-
- handleHotkeyOpen = () => {
- this.context.router.history.push(
- `/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`
- );
- };
-
- handleHotkeyOpenProfile = () => {
- this.context.router.history.push(`/${this._properStatus().getIn(['account', 'acct'])}`);
- };
-
- handleHotkeyMoveUp = e => {
- this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
- };
-
- handleHotkeyMoveDown = e => {
- this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
- };
-
- handleHotkeyToggleHidden = () => {
- this.props.onToggleHidden(this._properStatus());
- };
-
- handleHotkeyToggleSensitive = () => {
- this.handleToggleMediaVisibility();
- };
-
- _properStatus() {
- const { status } = this.props;
-
- if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
- return status.get('reblog');
- }
-
- return status;
- }
-
- handleRef = c => {
- this.node = c;
- };
-
- render() {
- let media = null;
- let statusAvatar, prepend, rebloggedByText, reblogContent;
-
- const { intl, hidden, featured, unread, showThread, group } = this.props;
-
- let { status, account, ...other } = this.props;
-
- if (status === null) return null;
-
- if (hidden) {
- return (
-
- {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
- {status.get('content')}
-
- );
- }
-
- if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
- const minHandlers = this.props.muted
- ? {}
- : {
- moveUp: this.handleHotkeyMoveUp,
- moveDown: this.handleHotkeyMoveDown,
- };
-
- return (
-
-
-
-
-
- );
- }
-
- if (featured) {
- prepend = (
-
- );
- } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
- const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
-
- prepend = (
-
-
-
-
-
-
-
-
-
- ),
- }}
- />
-
- );
-
- rebloggedByText = intl.formatMessage(
- { id: 'status.reblogged_by', defaultMessage: '{name} reposted' },
- { name: status.getIn(['account', 'acct']) }
- );
-
- account = status.get('account');
- reblogContent = status.get('contentHtml');
- status = status.get('reblog');
- }
-
- if (status.get('poll')) {
- media =
;
- } else if (status.get('media_attachments').size > 0) {
- if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
- const video = status.getIn(['media_attachments', 0]);
-
- media = (
-
- {Component => (
-
- )}
-
- );
- } else {
- media = (
-
- {Component => (
-
- )}
-
- );
- }
- } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
- media = (
-
- );
- }
-
- if (account === undefined || account === null) {
- statusAvatar =
;
- } else {
- statusAvatar =
;
- }
-
- const handlers = this.props.muted
- ? {}
- : {
- reply: this.handleHotkeyReply,
- favourite: this.handleHotkeyFavourite,
- boost: this.handleHotkeyBoost,
- mention: this.handleHotkeyMention,
- open: this.handleHotkeyOpen,
- openProfile: this.handleHotkeyOpenProfile,
- moveUp: this.handleHotkeyMoveUp,
- moveDown: this.handleHotkeyMoveDown,
- toggleHidden: this.handleHotkeyToggleHidden,
- toggleSensitive: this.handleHotkeyToggleSensitive,
- };
-
- const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
-
- return (
-
-
- {prepend}
-
-
-
-
-
-
-
-
-
- {statusAvatar}
-
-
-
-
-
- {!group && status.get('group') && (
-
- Posted in{' '}
- {status.getIn(['group', 'title'])}
-
- )}
-
-
-
- {media}
-
- {showThread &&
- status.get('in_reply_to_id') &&
- status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
-
-
-
- )}
-
-
-
-
-
- );
- }
-
-}
+export { default } from './status';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/status/status.js b/app/javascript/gabsocial/components/status/status.js
new file mode 100644
index 00000000..94b035ea
--- /dev/null
+++ b/app/javascript/gabsocial/components/status/status.js
@@ -0,0 +1,482 @@
+import { NavLink } from 'react-router-dom';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { HotKeys } from 'react-hotkeys';
+import classNames from 'classnames';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Avatar from '../avatar';
+import AvatarOverlay from '../avatar_overlay';
+import RelativeTimestamp from '../relative_timestamp';
+import DisplayName from '../display_name';
+import StatusContent from '../status_content/status_content';
+import StatusActionBar from '../status_action_bar/status_action_bar';
+import Card from '../../features/status/components/card';
+import { MediaGallery, Video } from '../../features/ui/util/async-components';
+import Icon from '../icon';
+import Poll from '../../components/poll';
+import { displayMedia } from '../../initial_state';
+
+import './status.scss';
+
+// We use the component (and not the container) since we do not want
+// to use the progress bar to show download progress
+import Bundle from '../../features/ui/util/bundle';
+
+export const textForScreenReader = (intl, status, rebloggedByText = false) => {
+ const displayName = status.getIn(['account', 'display_name']);
+
+ const values = [
+ displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
+ status.get('spoiler_text') && status.get('hidden')
+ ? status.get('spoiler_text')
+ : status.get('search_index').slice(status.get('spoiler_text').length),
+ intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
+ status.getIn(['account', 'acct']),
+ ];
+
+ if (rebloggedByText) {
+ values.push(rebloggedByText);
+ }
+
+ return values.join(', ');
+};
+
+export const defaultMediaVisibility = status => {
+ if (!status) return undefined;
+
+ if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
+ status = status.get('reblog');
+ }
+
+ return (displayMedia !== 'hide_all' && !status.get('sensitive')) || displayMedia === 'show_all';
+};
+
+export default @injectIntl
+class Status extends ImmutablePureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ status: ImmutablePropTypes.map,
+ account: ImmutablePropTypes.map,
+ onClick: PropTypes.func,
+ onReply: PropTypes.func,
+ onFavourite: PropTypes.func,
+ onReblog: PropTypes.func,
+ onDelete: PropTypes.func,
+ onDirect: PropTypes.func,
+ onMention: PropTypes.func,
+ onPin: PropTypes.func,
+ onOpenMedia: PropTypes.func,
+ onOpenVideo: PropTypes.func,
+ onBlock: PropTypes.func,
+ onEmbed: PropTypes.func,
+ onHeightChange: PropTypes.func,
+ onToggleHidden: PropTypes.func,
+ muted: PropTypes.bool,
+ hidden: PropTypes.bool,
+ unread: PropTypes.bool,
+ onMoveUp: PropTypes.func,
+ onMoveDown: PropTypes.func,
+ showThread: PropTypes.bool,
+ getScrollPosition: PropTypes.func,
+ updateScrollBottom: PropTypes.func,
+ cacheMediaWidth: PropTypes.func,
+ cachedMediaWidth: PropTypes.number,
+ group: ImmutablePropTypes.map,
+ };
+
+ // Avoid checking props that are functions (and whose equality will always
+ // evaluate to false. See react-immutable-pure-component for usage.
+ updateOnProps = ['status', 'account', 'muted', 'hidden'];
+
+ state = {
+ showMedia: defaultMediaVisibility(this.props.status),
+ statusId: undefined,
+ };
+
+ // Track height changes we know about to compensate scrolling
+ componentDidMount() {
+ 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();
+ }
+
+ return null;
+ }
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
+ return {
+ showMedia: defaultMediaVisibility(nextProps.status),
+ statusId: nextProps.status.get('id'),
+ };
+ }
+
+ return null;
+ }
+
+ // Compensate height changes
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
+
+ if (doShowCard && !this.didShowCard) {
+ this.didShowCard = true;
+
+ if (snapshot !== null && this.props.updateScrollBottom) {
+ if (this.node && this.node.offsetTop < snapshot.top) {
+ this.props.updateScrollBottom(snapshot.height - snapshot.top);
+ }
+ }
+ }
+ }
+
+ 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 });
+ };
+
+ handleClick = () => {
+ if (this.props.onClick) {
+ this.props.onClick();
+ return;
+ }
+
+ if (!this.context.router) return;
+
+ this.context.router.history.push(
+ `/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`
+ );
+ };
+
+ handleExpandClick = e => {
+ if (e.button === 0) {
+ if (!this.context.router) return;
+
+ this.context.router.history.push(
+ `/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`
+ );
+ }
+ };
+
+ handleExpandedToggle = () => {
+ this.props.onToggleHidden(this._properStatus());
+ };
+
+ renderLoadingMediaGallery() {
+ return
;
+ }
+
+ renderLoadingVideoPlayer() {
+ return
;
+ }
+
+ handleOpenVideo = (media, startTime) => {
+ this.props.onOpenVideo(media, startTime);
+ };
+
+ handleHotkeyReply = e => {
+ e.preventDefault();
+ this.props.onReply(this._properStatus(), this.context.router.history);
+ };
+
+ handleHotkeyFavourite = () => {
+ this.props.onFavourite(this._properStatus());
+ };
+
+ handleHotkeyBoost = e => {
+ this.props.onReblog(this._properStatus(), e);
+ };
+
+ handleHotkeyMention = e => {
+ e.preventDefault();
+ this.props.onMention(this._properStatus().get('account'), this.context.router.history);
+ };
+
+ handleHotkeyOpen = () => {
+ this.context.router.history.push(
+ `/${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().get('id')}`
+ );
+ };
+
+ handleHotkeyOpenProfile = () => {
+ this.context.router.history.push(`/${this._properStatus().getIn(['account', 'acct'])}`);
+ };
+
+ handleHotkeyMoveUp = e => {
+ this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
+ };
+
+ handleHotkeyMoveDown = e => {
+ this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
+ };
+
+ handleHotkeyToggleHidden = () => {
+ this.props.onToggleHidden(this._properStatus());
+ };
+
+ handleHotkeyToggleSensitive = () => {
+ this.handleToggleMediaVisibility();
+ };
+
+ _properStatus() {
+ const { status } = this.props;
+
+ if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
+ return status.get('reblog');
+ }
+
+ return status;
+ }
+
+ handleRef = c => {
+ this.node = c;
+ };
+
+ render() {
+ let media = null;
+ let statusAvatar, prepend, rebloggedByText, reblogContent;
+
+ const { intl, hidden, featured, unread, showThread, group } = this.props;
+
+ let { status, account, ...other } = this.props;
+
+ if (status === null) return null;
+
+ if (hidden) {
+ return (
+
+ {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
+ {status.get('content')}
+
+ );
+ }
+
+ if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
+ const minHandlers = this.props.muted
+ ? {}
+ : {
+ moveUp: this.handleHotkeyMoveUp,
+ moveDown: this.handleHotkeyMoveDown,
+ };
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (featured) {
+ prepend = (
+
+ );
+ } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
+ const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
+
+ prepend = (
+
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+ );
+
+ rebloggedByText = intl.formatMessage(
+ { id: 'status.reblogged_by', defaultMessage: '{name} reposted' },
+ { name: status.getIn(['account', 'acct']) }
+ );
+
+ account = status.get('account');
+ reblogContent = status.get('contentHtml');
+ status = status.get('reblog');
+ }
+
+ if (status.get('poll')) {
+ media =
;
+ } else if (status.get('media_attachments').size > 0) {
+ if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+ const video = status.getIn(['media_attachments', 0]);
+
+ media = (
+
+ {Component => (
+
+ )}
+
+ );
+ } else {
+ media = (
+
+ {Component => (
+
+ )}
+
+ );
+ }
+ } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
+ media = (
+
+ );
+ }
+
+ if (account === undefined || account === null) {
+ statusAvatar =
;
+ } else {
+ statusAvatar =
;
+ }
+
+ const handlers = this.props.muted
+ ? {}
+ : {
+ reply: this.handleHotkeyReply,
+ favourite: this.handleHotkeyFavourite,
+ boost: this.handleHotkeyBoost,
+ mention: this.handleHotkeyMention,
+ open: this.handleHotkeyOpen,
+ openProfile: this.handleHotkeyOpenProfile,
+ moveUp: this.handleHotkeyMoveUp,
+ moveDown: this.handleHotkeyMoveDown,
+ toggleHidden: this.handleHotkeyToggleHidden,
+ toggleSensitive: this.handleHotkeyToggleSensitive,
+ };
+
+ const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
+
+ return (
+
+
+ {prepend}
+
+
+
+
+
+
+
+
+
+ {statusAvatar}
+
+
+
+
+
+ {!group && status.get('group') && (
+
+ Posted in{' '}
+ {status.getIn(['group', 'title'])}
+
+ )}
+
+
+
+ {media}
+
+ {showThread &&
+ status.get('in_reply_to_id') &&
+ status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
+
+
+
+ )}
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/status/index.scss b/app/javascript/gabsocial/components/status/status.scss
similarity index 100%
rename from app/javascript/gabsocial/components/status/index.scss
rename to app/javascript/gabsocial/components/status/status.scss
diff --git a/app/javascript/gabsocial/components/status_action_bar/index.js b/app/javascript/gabsocial/components/status_action_bar/index.js
index b7bb6674..188c4d30 100644
--- a/app/javascript/gabsocial/components/status_action_bar/index.js
+++ b/app/javascript/gabsocial/components/status_action_bar/index.js
@@ -1,323 +1 @@
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl } from 'react-intl';
-import { Link } from 'react-router-dom';
-import DropdownMenuContainer from '../../containers/dropdown_menu_container';
-import IconButton from '../icon_button';
-import { me, isStaff } from '../../initial_state';
-import { openModal } from '../../actions/modal';
-
-import './index.scss';
-
-const messages = defineMessages({
- delete: { id: 'status.delete', defaultMessage: 'Delete' },
- redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
- mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
- mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
- block: { id: 'account.block', defaultMessage: 'Block @{name}' },
- reply: { id: 'status.reply', defaultMessage: 'Reply' },
- share: { id: 'status.share', defaultMessage: 'Share' },
- more: { id: 'status.more', defaultMessage: 'More' },
- replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
- reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
- reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
- cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
- cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
- favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
- open: { id: 'status.open', defaultMessage: 'Expand this status' },
- report: { id: 'status.report', defaultMessage: 'Report @{name}' },
- muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
- unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
- pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
- unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
- embed: { id: 'status.embed', defaultMessage: 'Embed' },
- admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
- admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
- copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
- group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
- group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove status from group' },
-});
-
-class StatusActionBar extends ImmutablePureComponent {
-
- static contextTypes = {
- router: PropTypes.object,
- };
-
- static propTypes = {
- status: ImmutablePropTypes.map.isRequired,
- onOpenUnauthorizedModal: PropTypes.func.isRequired,
- onReply: PropTypes.func,
- onFavourite: PropTypes.func,
- onReblog: PropTypes.func,
- onDelete: PropTypes.func,
- onMention: PropTypes.func,
- onMute: PropTypes.func,
- onBlock: PropTypes.func,
- onReport: PropTypes.func,
- onEmbed: PropTypes.func,
- onMuteConversation: PropTypes.func,
- onPin: PropTypes.func,
- withDismiss: PropTypes.bool,
- withGroupAdmin: PropTypes.bool,
- intl: PropTypes.object.isRequired,
- };
-
- // Avoid checking props that are functions (and whose equality will always
- // evaluate to false. See react-immutable-pure-component for usage.
- updateOnProps = [
- 'status',
- 'withDismiss',
- ]
-
- handleReplyClick = () => {
- if (me) {
- this.props.onReply(this.props.status, this.context.router.history);
- } else {
- this.props.onOpenUnauthorizedModal();
- }
- }
-
- handleShareClick = () => {
- navigator.share({
- text: this.props.status.get('search_index'),
- url: this.props.status.get('url'),
- }).catch((e) => {
- if (e.name !== 'AbortError') console.error(e);
- });
- }
-
- handleFavouriteClick = () => {
- if (me) {
- this.props.onFavourite(this.props.status);
- } else {
- this.props.onOpenUnauthorizedModal();
- }
- }
-
- handleReblogClick = e => {
- if (me) {
- this.props.onReblog(this.props.status, e);
- } else {
- this.props.onOpenUnauthorizedModal();
- }
- }
-
- handleDeleteClick = () => {
- this.props.onDelete(this.props.status, this.context.router.history);
- }
-
- handleRedraftClick = () => {
- this.props.onDelete(this.props.status, this.context.router.history, true);
- }
-
- handlePinClick = () => {
- this.props.onPin(this.props.status);
- }
-
- handleMentionClick = () => {
- this.props.onMention(this.props.status.get('account'), this.context.router.history);
- }
-
- handleMuteClick = () => {
- this.props.onMute(this.props.status.get('account'));
- }
-
- handleBlockClick = () => {
- this.props.onBlock(this.props.status);
- }
-
- handleOpen = () => {
- this.context.router.history.push(`/${this.props.status.getIn(['account', 'acct'])}/posts/${this.props.status.get('id')}`);
- }
-
- handleEmbed = () => {
- this.props.onEmbed(this.props.status);
- }
-
- handleReport = () => {
- this.props.onReport(this.props.status);
- }
-
- handleConversationMuteClick = () => {
- this.props.onMuteConversation(this.props.status);
- }
-
- handleCopy = () => {
- const url = this.props.status.get('url');
- const textarea = document.createElement('textarea');
-
- textarea.textContent = url;
- textarea.style.position = 'fixed';
-
- document.body.appendChild(textarea);
-
- try {
- textarea.select();
- document.execCommand('copy');
- } catch (e) {
- //
- } finally {
- document.body.removeChild(textarea);
- }
- }
-
- handleGroupRemoveAccount = () => {
- const { status } = this.props;
-
- this.props.onGroupRemoveAccount(status.getIn(['group', 'id']), status.getIn(['account', 'id']));
- }
-
- handleGroupRemovePost = () => {
- const { status } = this.props;
-
- this.props.onGroupRemoveStatus(status.getIn(['group', 'id']), status.get('id'));
- }
-
- _makeMenu = (publicStatus) => {
- const { status, intl: { formatMessage }, withDismiss, withGroupAdmin } = this.props;
- const mutingConversation = status.get('muted');
-
- let menu = [];
-
- menu.push({ text: formatMessage(messages.open), action: this.handleOpen });
-
- if (publicStatus) {
- menu.push({ text: formatMessage(messages.copy), action: this.handleCopy });
- menu.push({ text: formatMessage(messages.embed), action: this.handleEmbed });
- }
-
- if (!me) {
- return menu;
- }
-
- menu.push(null);
-
- if (status.getIn(['account', 'id']) === me || withDismiss) {
- menu.push({ text: formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
- menu.push(null);
- }
-
- if (status.getIn(['account', 'id']) === me) {
- if (publicStatus) {
- menu.push({ text: formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
- } else {
- if (status.get('visibility') === 'private') {
- menu.push({ text: formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
- }
- }
-
- menu.push({ text: formatMessage(messages.delete), action: this.handleDeleteClick });
- menu.push({ text: formatMessage(messages.redraft), action: this.handleRedraftClick });
- } else {
- menu.push({ text: formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
- menu.push(null);
- menu.push({ text: formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
- menu.push({ text: formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
- menu.push({ text: formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
-
- if (isStaff) {
- menu.push(null);
- menu.push({ text: formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
- menu.push({ text: formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
- }
-
- if (withGroupAdmin) {
- menu.push(null);
- menu.push({ text: formatMessage(messages.group_remove_account), action: this.handleGroupRemoveAccount });
- menu.push({ text: formatMessage(messages.group_remove_post), action: this.handleGroupRemovePost });
- }
- }
-
- return menu;
- }
-
- render () {
- const { status, intl: { formatMessage } } = this.props;
-
- const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
-
- const replyCount = status.get('replies_count');
- const replyIcon = (status.get('in_reply_to_id', null) === null) ? 'reply' : 'reply-all';
- const replyTitle = (status.get('in_reply_to_id', null) === null) ? formatMessage(messages.reply) : formatMessage(messages.replyAll);
-
- const reblogCount = status.get('reblogs_count');
- const reblogTitle = !publicStatus ? formatMessage(messages.cannot_reblog) : formatMessage(messages.reblog);
- const reblogIcon = (status.get('visibility') === 'private') ? 'lock' : 'retweet';
-
- const favoriteCount = status.get('favourites_count');
-
- const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
-
- );
-
- const menu = this._makeMenu(publicStatus);
-
- return (
-
-
-
- {
- replyCount !== 0 &&
- {replyCount}
- }
-
-
-
-
- {reblogCount !== 0 && {reblogCount}}
-
-
-
-
- {favoriteCount !== 0 && {favoriteCount} }
-
-
- {shareButton}
-
-
-
-
-
- );
- }
-
-}
-
-const mapDispatchToProps = (dispatch) => ({
- onOpenUnauthorizedModal() {
- dispatch(openModal('UNAUTHORIZED'));
- },
-});
-
-export default injectIntl(
- connect(null, mapDispatchToProps, null, { forwardRef: true }
- )(StatusActionBar));
+export { default } from './status_action_bar';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/status_action_bar/status_action_bar.js b/app/javascript/gabsocial/components/status_action_bar/status_action_bar.js
new file mode 100644
index 00000000..9f279e39
--- /dev/null
+++ b/app/javascript/gabsocial/components/status_action_bar/status_action_bar.js
@@ -0,0 +1,321 @@
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { Link } from 'react-router-dom';
+import DropdownMenuContainer from '../../containers/dropdown_menu_container';
+import IconButton from '../icon_button';
+import { me, isStaff } from '../../initial_state';
+import { openModal } from '../../actions/modal';
+
+import './status_action_bar.scss';
+
+const messages = defineMessages({
+ delete: { id: 'status.delete', defaultMessage: 'Delete' },
+ redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+ mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
+ mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
+ block: { id: 'account.block', defaultMessage: 'Block @{name}' },
+ reply: { id: 'status.reply', defaultMessage: 'Reply' },
+ share: { id: 'status.share', defaultMessage: 'Share' },
+ more: { id: 'status.more', defaultMessage: 'More' },
+ replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
+ reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
+ reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
+ cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
+ cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
+ favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
+ open: { id: 'status.open', defaultMessage: 'Expand this status' },
+ report: { id: 'status.report', defaultMessage: 'Report @{name}' },
+ muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
+ unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+ pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
+ unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
+ embed: { id: 'status.embed', defaultMessage: 'Embed' },
+ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+ admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
+ copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
+ group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
+ group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove status from group' },
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ onOpenUnauthorizedModal() {
+ dispatch(openModal('UNAUTHORIZED'));
+ },
+});
+
+export default @connect(null, mapDispatchToProps)
+@injectIntl
+class StatusActionBar extends ImmutablePureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ status: ImmutablePropTypes.map.isRequired,
+ onOpenUnauthorizedModal: PropTypes.func.isRequired,
+ onReply: PropTypes.func,
+ onFavourite: PropTypes.func,
+ onReblog: PropTypes.func,
+ onDelete: PropTypes.func,
+ onMention: PropTypes.func,
+ onMute: PropTypes.func,
+ onBlock: PropTypes.func,
+ onReport: PropTypes.func,
+ onEmbed: PropTypes.func,
+ onMuteConversation: PropTypes.func,
+ onPin: PropTypes.func,
+ withDismiss: PropTypes.bool,
+ withGroupAdmin: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ };
+
+ // Avoid checking props that are functions (and whose equality will always
+ // evaluate to false. See react-immutable-pure-component for usage.
+ updateOnProps = [
+ 'status',
+ 'withDismiss',
+ ]
+
+ handleReplyClick = () => {
+ if (me) {
+ this.props.onReply(this.props.status, this.context.router.history);
+ } else {
+ this.props.onOpenUnauthorizedModal();
+ }
+ }
+
+ handleShareClick = () => {
+ navigator.share({
+ text: this.props.status.get('search_index'),
+ url: this.props.status.get('url'),
+ }).catch((e) => {
+ if (e.name !== 'AbortError') console.error(e);
+ });
+ }
+
+ handleFavouriteClick = () => {
+ if (me) {
+ this.props.onFavourite(this.props.status);
+ } else {
+ this.props.onOpenUnauthorizedModal();
+ }
+ }
+
+ handleReblogClick = e => {
+ if (me) {
+ this.props.onReblog(this.props.status, e);
+ } else {
+ this.props.onOpenUnauthorizedModal();
+ }
+ }
+
+ handleDeleteClick = () => {
+ this.props.onDelete(this.props.status, this.context.router.history);
+ }
+
+ handleRedraftClick = () => {
+ this.props.onDelete(this.props.status, this.context.router.history, true);
+ }
+
+ handlePinClick = () => {
+ this.props.onPin(this.props.status);
+ }
+
+ handleMentionClick = () => {
+ this.props.onMention(this.props.status.get('account'), this.context.router.history);
+ }
+
+ handleMuteClick = () => {
+ this.props.onMute(this.props.status.get('account'));
+ }
+
+ handleBlockClick = () => {
+ this.props.onBlock(this.props.status);
+ }
+
+ handleOpen = () => {
+ this.context.router.history.push(`/${this.props.status.getIn(['account', 'acct'])}/posts/${this.props.status.get('id')}`);
+ }
+
+ handleEmbed = () => {
+ this.props.onEmbed(this.props.status);
+ }
+
+ handleReport = () => {
+ this.props.onReport(this.props.status);
+ }
+
+ handleConversationMuteClick = () => {
+ this.props.onMuteConversation(this.props.status);
+ }
+
+ handleCopy = () => {
+ const url = this.props.status.get('url');
+ const textarea = document.createElement('textarea');
+
+ textarea.textContent = url;
+ textarea.style.position = 'fixed';
+
+ document.body.appendChild(textarea);
+
+ try {
+ textarea.select();
+ document.execCommand('copy');
+ } catch (e) {
+ //
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }
+
+ handleGroupRemoveAccount = () => {
+ const { status } = this.props;
+
+ this.props.onGroupRemoveAccount(status.getIn(['group', 'id']), status.getIn(['account', 'id']));
+ }
+
+ handleGroupRemovePost = () => {
+ const { status } = this.props;
+
+ this.props.onGroupRemoveStatus(status.getIn(['group', 'id']), status.get('id'));
+ }
+
+ _makeMenu = (publicStatus) => {
+ const { status, intl: { formatMessage }, withDismiss, withGroupAdmin } = this.props;
+ const mutingConversation = status.get('muted');
+
+ let menu = [];
+
+ menu.push({ text: formatMessage(messages.open), action: this.handleOpen });
+
+ if (publicStatus) {
+ menu.push({ text: formatMessage(messages.copy), action: this.handleCopy });
+ menu.push({ text: formatMessage(messages.embed), action: this.handleEmbed });
+ }
+
+ if (!me) {
+ return menu;
+ }
+
+ menu.push(null);
+
+ if (status.getIn(['account', 'id']) === me || withDismiss) {
+ menu.push({ text: formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+ menu.push(null);
+ }
+
+ if (status.getIn(['account', 'id']) === me) {
+ if (publicStatus) {
+ menu.push({ text: formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+ } else {
+ if (status.get('visibility') === 'private') {
+ menu.push({ text: formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
+ }
+ }
+
+ menu.push({ text: formatMessage(messages.delete), action: this.handleDeleteClick });
+ menu.push({ text: formatMessage(messages.redraft), action: this.handleRedraftClick });
+ } else {
+ menu.push({ text: formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
+ menu.push(null);
+ menu.push({ text: formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
+ menu.push({ text: formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
+ menu.push({ text: formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
+
+ if (isStaff) {
+ menu.push(null);
+ menu.push({ text: formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+ menu.push({ text: formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+ }
+
+ if (withGroupAdmin) {
+ menu.push(null);
+ menu.push({ text: formatMessage(messages.group_remove_account), action: this.handleGroupRemoveAccount });
+ menu.push({ text: formatMessage(messages.group_remove_post), action: this.handleGroupRemovePost });
+ }
+ }
+
+ return menu;
+ }
+
+ render () {
+ const { status, intl: { formatMessage } } = this.props;
+
+ const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
+
+ const replyCount = status.get('replies_count');
+ const replyIcon = (status.get('in_reply_to_id', null) === null) ? 'reply' : 'reply-all';
+ const replyTitle = (status.get('in_reply_to_id', null) === null) ? formatMessage(messages.reply) : formatMessage(messages.replyAll);
+
+ const reblogCount = status.get('reblogs_count');
+ const reblogTitle = !publicStatus ? formatMessage(messages.cannot_reblog) : formatMessage(messages.reblog);
+ const reblogIcon = (status.get('visibility') === 'private') ? 'lock' : 'retweet';
+
+ const favoriteCount = status.get('favourites_count');
+
+ const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
+
+ );
+
+ const menu = this._makeMenu(publicStatus);
+
+ return (
+
+
+
+ {
+ replyCount !== 0 &&
+ {replyCount}
+ }
+
+
+
+
+ {reblogCount !== 0 && {reblogCount}}
+
+
+
+
+ {favoriteCount !== 0 && {favoriteCount} }
+
+
+ {shareButton}
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/status_action_bar/index.scss b/app/javascript/gabsocial/components/status_action_bar/status_action_bar.scss
similarity index 100%
rename from app/javascript/gabsocial/components/status_action_bar/index.scss
rename to app/javascript/gabsocial/components/status_action_bar/status_action_bar.scss
diff --git a/app/javascript/gabsocial/components/status_check_box/index.js b/app/javascript/gabsocial/components/status_check_box/index.js
new file mode 100644
index 00000000..f56ef7ac
--- /dev/null
+++ b/app/javascript/gabsocial/components/status_check_box/index.js
@@ -0,0 +1 @@
+export { default } from './status_check_box';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/report/components/status_check_box.js b/app/javascript/gabsocial/components/status_check_box/status_check_box.js
similarity index 72%
rename from app/javascript/gabsocial/features/report/components/status_check_box.js
rename to app/javascript/gabsocial/components/status_check_box/status_check_box.js
index bc870a32..2f738389 100644
--- a/app/javascript/gabsocial/features/report/components/status_check_box.js
+++ b/app/javascript/gabsocial/components/status_check_box/status_check_box.js
@@ -1,11 +1,27 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
+import { Set as ImmutableSet } from 'immutable';
import noop from 'lodash/noop';
-import StatusContent from '../../../components/status_content';
-import { MediaGallery, Video } from '../../ui/util/async-components';
-import Bundle from '../../ui/components/bundle';
+import StatusContent from '../status_content';
+import { MediaGallery, Video } from '../../features/ui/util/async-components';
+import Bundle from '../../features/ui/util/bundle';
+import { toggleStatusReport } from '../../actions/reports';
-export default class StatusCheckBox extends PureComponent {
+import './status_check_box.scss';
+
+const mapStateToProps = (state, { id }) => ({
+ status: state.getIn(['statuses', id]),
+ checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
+});
+
+const mapDispatchToProps = (dispatch, { id }) => ({
+ onToggle(e) {
+ dispatch(toggleStatusReport(id, e.target.checked));
+ },
+});
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+class StatusCheckBox extends PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
diff --git a/app/javascript/gabsocial/components/status_check_box/status_check_box.scss b/app/javascript/gabsocial/components/status_check_box/status_check_box.scss
new file mode 100644
index 00000000..377f24ea
--- /dev/null
+++ b/app/javascript/gabsocial/components/status_check_box/status_check_box.scss
@@ -0,0 +1,34 @@
+.status-check-box {
+ display: flex;
+ border-bottom: 1px solid $ui-secondary-color;
+
+ &__status {
+ margin: 10px 0 10px 10px;
+ flex: 1;
+
+ .media-gallery {
+ max-width: 250px;
+ }
+
+ .status__content {
+ padding: 0;
+ white-space: normal;
+ }
+
+ .video-player {
+ margin-top: 8px;
+ max-width: 250px;
+ }
+
+ .media-gallery__item-thumbnail {
+ cursor: default;
+ }
+ }
+
+ &__toggle {
+ flex: 0 0 auto;
+ padding: 10px;
+
+ @include flex(center, center);
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/status_content/index.js b/app/javascript/gabsocial/components/status_content/index.js
index 0e826b76..30bde49b 100644
--- a/app/javascript/gabsocial/components/status_content/index.js
+++ b/app/javascript/gabsocial/components/status_content/index.js
@@ -1,235 +1 @@
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage } from 'react-intl';
-import classnames from 'classnames';
-import { isRtl } from '../../utils/rtl';
-import Permalink from '../permalink';
-import Icon from '../icon';
-
-import './index.scss';
-
-const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
-
-export default class StatusContent extends PureComponent {
-
- static contextTypes = {
- router: PropTypes.object,
- };
-
- static propTypes = {
- status: ImmutablePropTypes.map.isRequired,
- reblogContent: PropTypes.string,
- expanded: PropTypes.bool,
- onExpandedToggle: PropTypes.func,
- onClick: PropTypes.func,
- collapsable: PropTypes.bool,
- };
-
- 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 () {
- const node = this.node;
-
- if (!node) return;
-
- const links = node.querySelectorAll('a');
-
- for (var i = 0; i < links.length; ++i) {
- let link = links[i];
- if (link.classList.contains('status-link')) {
- continue;
- }
- link.classList.add('status-link');
-
- let mention = this.props.status.get('mentions').find(item => link.href === `/${item.get('acct')}`);
-
- if (mention) {
- link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
- link.setAttribute('title', mention.get('acct'));
- } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
- link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
- } else {
- link.setAttribute('title', link.href);
- }
- }
-
- 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 });
- }
- }
-
- componentDidMount () {
- this._updateStatusLinks();
- }
-
- componentDidUpdate () {
- this._updateStatusLinks();
- }
-
- onMentionClick = (mention, e) => {
- if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.context.router.history.push(`/${mention.get('acct')}`);
- }
- }
-
- onHashtagClick = (hashtag, e) => {
- hashtag = hashtag.replace(/^#/, '').toLowerCase();
-
- if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.context.router.history.push(`/tags/${hashtag}`);
- }
- }
-
- handleMouseDown = (e) => {
- this.startXY = [e.clientX, e.clientY];
- }
-
- handleMouseUp = (e) => {
- if (!this.startXY) return;
-
- const [ startX, startY ] = this.startXY;
- const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
-
- if (e.target.localName === 'button' ||
- e.target.localName === 'a' ||
- (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
- return;
- }
-
- if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
- this.props.onClick();
- }
-
- this.startXY = null;
- }
-
- handleSpoilerClick = (e) => {
- e.preventDefault();
-
- if (this.props.onExpandedToggle) {
- // The parent manages the state
- this.props.onExpandedToggle();
- } else {
- this.setState({ hidden: !this.state.hidden });
- }
- }
-
- handleCollapsedClick = (e) => {
- e.preventDefault();
- this.setState({ collapsed: !this.state.collapsed });
- }
-
- setRef = (c) => {
- this.node = c;
- }
-
- getHtmlContent = () => {
- const { status, reblogContent } = this.props;
-
- const properContent = status.get('contentHtml');
-
- return reblogContent
- ? `${reblogContent}
${properContent}
`
- : properContent;
- }
-
- render () {
- const { status } = this.props;
-
- if (status.get('content').length === 0) return null;
-
- const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
-
- const content = { __html: this.getHtmlContent() };
- const spoilerContent = { __html: status.get('spoilerHtml') };
- const directionStyle = { direction: 'ltr' };
- 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,
- });
-
- if (isRtl(status.get('search_index'))) {
- directionStyle.direction = 'rtl';
- }
-
- const readMoreButton = (
-
-
-
-
- );
-
- if (status.get('spoiler_text').length > 0) {
- let mentionsPlaceholder = '';
-
- const mentionLinks = status.get('mentions').map(item => (
-
- @{item.get('username')}
-
- )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
-
- const toggleText = hidden ?
:
;
-
- if (hidden) {
- mentionsPlaceholder =
{mentionLinks}
;
- }
-
- return (
-
-
-
- {' '}
- {toggleText}
-
-
- {mentionsPlaceholder}
-
-
-
- );
- } else if (this.props.onClick) {
- const output = [
-
,
- ];
-
- if (this.state.collapsed) {
- output.push(readMoreButton);
- }
-
- return output;
- }
-
- return (
-
- );
- }
-
-}
+export { default } from './status_content';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/status_content/status_content.js b/app/javascript/gabsocial/components/status_content/status_content.js
new file mode 100644
index 00000000..9c522c23
--- /dev/null
+++ b/app/javascript/gabsocial/components/status_content/status_content.js
@@ -0,0 +1,235 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import classnames from 'classnames';
+import { isRtl } from '../../utils/rtl';
+import Permalink from '../permalink/permalink';
+import Icon from '../icon';
+
+import './status_content.scss';
+
+const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
+
+export default class StatusContent extends PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ status: ImmutablePropTypes.map.isRequired,
+ reblogContent: PropTypes.string,
+ expanded: PropTypes.bool,
+ onExpandedToggle: PropTypes.func,
+ onClick: PropTypes.func,
+ collapsable: PropTypes.bool,
+ };
+
+ 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 () {
+ const node = this.node;
+
+ if (!node) return;
+
+ const links = node.querySelectorAll('a');
+
+ for (var i = 0; i < links.length; ++i) {
+ let link = links[i];
+ if (link.classList.contains('status-link')) {
+ continue;
+ }
+ link.classList.add('status-link');
+
+ let mention = this.props.status.get('mentions').find(item => link.href === `/${item.get('acct')}`);
+
+ if (mention) {
+ link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
+ link.setAttribute('title', mention.get('acct'));
+ } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
+ link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
+ } else {
+ link.setAttribute('title', link.href);
+ }
+ }
+
+ 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 });
+ }
+ }
+
+ componentDidMount () {
+ this._updateStatusLinks();
+ }
+
+ componentDidUpdate () {
+ this._updateStatusLinks();
+ }
+
+ onMentionClick = (mention, e) => {
+ if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ this.context.router.history.push(`/${mention.get('acct')}`);
+ }
+ }
+
+ onHashtagClick = (hashtag, e) => {
+ hashtag = hashtag.replace(/^#/, '').toLowerCase();
+
+ if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ this.context.router.history.push(`/tags/${hashtag}`);
+ }
+ }
+
+ handleMouseDown = (e) => {
+ this.startXY = [e.clientX, e.clientY];
+ }
+
+ handleMouseUp = (e) => {
+ if (!this.startXY) return;
+
+ const [ startX, startY ] = this.startXY;
+ const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
+
+ if (e.target.localName === 'button' ||
+ e.target.localName === 'a' ||
+ (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
+ return;
+ }
+
+ if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
+ this.props.onClick();
+ }
+
+ this.startXY = null;
+ }
+
+ handleSpoilerClick = (e) => {
+ e.preventDefault();
+
+ if (this.props.onExpandedToggle) {
+ // The parent manages the state
+ this.props.onExpandedToggle();
+ } else {
+ this.setState({ hidden: !this.state.hidden });
+ }
+ }
+
+ handleCollapsedClick = (e) => {
+ e.preventDefault();
+ this.setState({ collapsed: !this.state.collapsed });
+ }
+
+ setRef = (c) => {
+ this.node = c;
+ }
+
+ getHtmlContent = () => {
+ const { status, reblogContent } = this.props;
+
+ const properContent = status.get('contentHtml');
+
+ return reblogContent
+ ? `${reblogContent}
${properContent}
`
+ : properContent;
+ }
+
+ render () {
+ const { status } = this.props;
+
+ if (status.get('content').length === 0) return null;
+
+ const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
+
+ const content = { __html: this.getHtmlContent() };
+ const spoilerContent = { __html: status.get('spoilerHtml') };
+ const directionStyle = { direction: 'ltr' };
+ 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,
+ });
+
+ if (isRtl(status.get('search_index'))) {
+ directionStyle.direction = 'rtl';
+ }
+
+ const readMoreButton = (
+
+
+
+
+ );
+
+ if (status.get('spoiler_text').length > 0) {
+ let mentionsPlaceholder = '';
+
+ const mentionLinks = status.get('mentions').map(item => (
+
+ @{item.get('username')}
+
+ )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
+
+ const toggleText = hidden ?
:
;
+
+ if (hidden) {
+ mentionsPlaceholder =
{mentionLinks}
;
+ }
+
+ return (
+
+
+
+ {' '}
+ {toggleText}
+
+
+ {mentionsPlaceholder}
+
+
+
+ );
+ } else if (this.props.onClick) {
+ const output = [
+
,
+ ];
+
+ if (this.state.collapsed) {
+ output.push(readMoreButton);
+ }
+
+ return output;
+ }
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/status_content/index.scss b/app/javascript/gabsocial/components/status_content/status_content.scss
similarity index 100%
rename from app/javascript/gabsocial/components/status_content/index.scss
rename to app/javascript/gabsocial/components/status_content/status_content.scss
diff --git a/app/javascript/gabsocial/components/status_list/index.js b/app/javascript/gabsocial/components/status_list/index.js
index c85faa74..d885f93e 100644
--- a/app/javascript/gabsocial/components/status_list/index.js
+++ b/app/javascript/gabsocial/components/status_list/index.js
@@ -1,157 +1 @@
-import { Fragment } from 'react';
-import { debounce } from 'lodash';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import LoadMore from '../load_more';
-import ScrollableList from '../scrollable_list';
-import TimelineQueueButtonHeader from '../timeline_queue_button_header';
-import ColumnIndicator from '../column_indicator';
-import StatusContainer from '../../containers/status_container';
-
-export default class StatusList extends ImmutablePureComponent {
-
- static propTypes = {
- scrollKey: PropTypes.string.isRequired,
- statusIds: ImmutablePropTypes.list.isRequired,
- featuredStatusIds: ImmutablePropTypes.list,
- onLoadMore: PropTypes.func,
- isLoading: PropTypes.bool,
- isPartial: PropTypes.bool,
- hasMore: PropTypes.bool,
- emptyMessage: PropTypes.node,
- timelineId: PropTypes.string,
- queuedItemSize: PropTypes.number,
- onDequeueTimeline: PropTypes.func,
- group: ImmutablePropTypes.map,
- withGroupAdmin: PropTypes.bool,
- onScrollToTop: PropTypes.func,
- onScroll: PropTypes.func,
- };
-
- componentDidMount() {
- this.handleDequeueTimeline();
- };
-
- getFeaturedStatusCount = () => {
- return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
- }
-
- getCurrentStatusIndex = (id, featured) => {
- if (featured) {
- return this.props.featuredStatusIds.indexOf(id);
- }
-
- return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
- }
-
- handleMoveUp = (id, featured) => {
- const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
- this._selectChild(elementIndex, true);
- }
-
- handleMoveDown = (id, featured) => {
- const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
- this._selectChild(elementIndex, false);
- }
-
- handleLoadOlder = debounce(() => {
- this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
- }, 300, { leading: true })
-
- _selectChild (index, align_top) {
- const container = this.node.node;
- const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
-
- if (element) {
- if (align_top && container.scrollTop > element.offsetTop) {
- element.scrollIntoView(true);
- } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
- element.scrollIntoView(false);
- }
- element.focus();
- }
- }
-
- handleDequeueTimeline = () => {
- const { onDequeueTimeline, timelineId } = this.props;
- if (!onDequeueTimeline || !timelineId) return;
-
- onDequeueTimeline(timelineId);
- }
-
- setRef = c => {
- this.node = c;
- }
-
- render () {
- const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, ...other } = this.props;
-
- if (isPartial) {
- return (
);
- }
-
- let scrollableContent = null;
- if (isLoading || statusIds.size > 0) {
- scrollableContent = statusIds.map((statusId, i) => {
- if (statusId === null) {
- return (
-
0 ? statusIds.get(i - 1) : null}
- onClick={onLoadMore}
- />
- );
- }
-
- return (
-
- );
- });
- }
-
- if (scrollableContent && featuredStatusIds) {
- scrollableContent = featuredStatusIds.map(statusId => (
-
- )).concat(scrollableContent);
- }
-
- return (
-
-
-
- {scrollableContent}
-
-
- );
- }
-
-}
+export { default } from './status_list';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/status_list/status_list.js b/app/javascript/gabsocial/components/status_list/status_list.js
new file mode 100644
index 00000000..c85faa74
--- /dev/null
+++ b/app/javascript/gabsocial/components/status_list/status_list.js
@@ -0,0 +1,157 @@
+import { Fragment } from 'react';
+import { debounce } from 'lodash';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import LoadMore from '../load_more';
+import ScrollableList from '../scrollable_list';
+import TimelineQueueButtonHeader from '../timeline_queue_button_header';
+import ColumnIndicator from '../column_indicator';
+import StatusContainer from '../../containers/status_container';
+
+export default class StatusList extends ImmutablePureComponent {
+
+ static propTypes = {
+ scrollKey: PropTypes.string.isRequired,
+ statusIds: ImmutablePropTypes.list.isRequired,
+ featuredStatusIds: ImmutablePropTypes.list,
+ onLoadMore: PropTypes.func,
+ isLoading: PropTypes.bool,
+ isPartial: PropTypes.bool,
+ hasMore: PropTypes.bool,
+ emptyMessage: PropTypes.node,
+ timelineId: PropTypes.string,
+ queuedItemSize: PropTypes.number,
+ onDequeueTimeline: PropTypes.func,
+ group: ImmutablePropTypes.map,
+ withGroupAdmin: PropTypes.bool,
+ onScrollToTop: PropTypes.func,
+ onScroll: PropTypes.func,
+ };
+
+ componentDidMount() {
+ this.handleDequeueTimeline();
+ };
+
+ getFeaturedStatusCount = () => {
+ return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
+ }
+
+ getCurrentStatusIndex = (id, featured) => {
+ if (featured) {
+ return this.props.featuredStatusIds.indexOf(id);
+ }
+
+ return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
+ }
+
+ handleMoveUp = (id, featured) => {
+ const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
+ this._selectChild(elementIndex, true);
+ }
+
+ handleMoveDown = (id, featured) => {
+ const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
+ this._selectChild(elementIndex, false);
+ }
+
+ handleLoadOlder = debounce(() => {
+ this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
+ }, 300, { leading: true })
+
+ _selectChild (index, align_top) {
+ const container = this.node.node;
+ const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
+
+ if (element) {
+ if (align_top && container.scrollTop > element.offsetTop) {
+ element.scrollIntoView(true);
+ } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
+ element.scrollIntoView(false);
+ }
+ element.focus();
+ }
+ }
+
+ handleDequeueTimeline = () => {
+ const { onDequeueTimeline, timelineId } = this.props;
+ if (!onDequeueTimeline || !timelineId) return;
+
+ onDequeueTimeline(timelineId);
+ }
+
+ setRef = c => {
+ this.node = c;
+ }
+
+ render () {
+ const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, ...other } = this.props;
+
+ if (isPartial) {
+ return ( );
+ }
+
+ let scrollableContent = null;
+ if (isLoading || statusIds.size > 0) {
+ scrollableContent = statusIds.map((statusId, i) => {
+ if (statusId === null) {
+ return (
+ 0 ? statusIds.get(i - 1) : null}
+ onClick={onLoadMore}
+ />
+ );
+ }
+
+ return (
+
+ );
+ });
+ }
+
+ if (scrollableContent && featuredStatusIds) {
+ scrollableContent = featuredStatusIds.map(statusId => (
+
+ )).concat(scrollableContent);
+ }
+
+ return (
+
+
+
+ {scrollableContent}
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/tabs_bar/index.js b/app/javascript/gabsocial/components/tabs_bar/index.js
new file mode 100644
index 00000000..17102cde
--- /dev/null
+++ b/app/javascript/gabsocial/components/tabs_bar/index.js
@@ -0,0 +1 @@
+export { default } from './tabs_bar';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/tabs_bar/tabs_bar.js b/app/javascript/gabsocial/components/tabs_bar/tabs_bar.js
new file mode 100644
index 00000000..1832f326
--- /dev/null
+++ b/app/javascript/gabsocial/components/tabs_bar/tabs_bar.js
@@ -0,0 +1,109 @@
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { NavLink, withRouter } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+import { me } from '../../initial_state';
+import NotificationsCounter from '../notification_counter';
+import SearchContainer from 'gabsocial/features/compose/containers/search_container';
+import Avatar from '../avatar';
+import ActionBar from 'gabsocial/features/compose/components/action_bar';
+import { openModal } from '../../actions/modal';
+
+import './tabs_bar.scss';
+
+const mapStateToProps = state => {
+ return {
+ account: state.getIn(['accounts', me]),
+ };
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ onOpenCompose() {
+ dispatch(openModal('COMPOSE'));
+ },
+});
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@withRouter
+class TabsBar extends ImmutablePureComponent {
+
+ static propTypes = {
+ history: PropTypes.object.isRequired,
+ onOpenCompose: PropTypes.func.isRequired,
+ account: ImmutablePropTypes.map.isRequired,
+ };
+
+ render () {
+ const { account, onOpenCompose } = this.props;
+
+ if (!account) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/tabs_bar/tabs_bar.scss b/app/javascript/gabsocial/components/tabs_bar/tabs_bar.scss
new file mode 100644
index 00000000..a1e0401c
--- /dev/null
+++ b/app/javascript/gabsocial/components/tabs_bar/tabs_bar.scss
@@ -0,0 +1,249 @@
+.tabs-bar {
+ display: flex;
+ box-sizing: border-box;
+ background: #000;
+ flex: 0 0 auto;
+ overflow-y: auto;
+ height: 50px;
+ position: sticky;
+ top: 0;
+ z-index: 4;
+
+ &__container {
+ display: flex;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 1200px;
+ padding: 0 15px;
+
+ @include margin-center;
+
+ // NOTE - might need to adjust this based on column sizing
+ @media screen and (max-width: $nav-breakpoint-4) {
+ padding: 0 20px;
+ }
+
+ }
+
+ &__split {
+ display: flex;
+ width: auto;
+
+ &--left {
+ margin-right: auto;
+ }
+
+ &--right {
+ margin-left: auto;
+ padding-top: 8px;
+
+ @media screen and (max-width: $nav-breakpoint-3) {
+ padding-top: 4px;
+ }
+ }
+ }
+
+ &__search-container {
+ display: block;
+ width: 251px;
+
+ @media screen and (max-width: $nav-breakpoint-2) {
+ display: none;
+ }
+
+ .search {
+ margin: 0;
+ }
+ }
+
+ &__profile {
+ position: relative;
+ overflow: hidden;
+ margin: 0 0 0 20px;
+
+ @include size(34px);
+
+ @media screen and (max-width: $nav-breakpoint-3) {
+ margin: 0;
+
+ @include size(42px);
+ }
+
+ .account__avatar {
+ background-size: 34px 34px;
+
+ @include size(34px);
+
+ @media screen and (max-width: $nav-breakpoint-3) {
+ background-size: 42px 42px;
+
+ @include size(42px);
+ }
+ }
+
+ .compose__action-bar {
+ display: block;
+
+ @include abs-position(0, 0, 0, -5px);
+
+ i {
+ display: none;
+ }
+ }
+ }
+
+ &__button-compose {
+ display: block;
+ margin-left: 20px;
+ border-radius: 4px;
+ background-color: $gab-brand-default !important;
+
+ @include background-image('/assets/images/sprite-main-navigation.png', 161px 152px, 18px 2px);
+ @include size(70px, 34px);
+
+ @media screen and (max-width: $nav-breakpoint-3) {
+ display: none;
+ }
+
+ &:hover {
+ background-color: lighten($gab-brand-default, 5%) !important;
+ background-position: 18px -98px;
+ box-shadow: inset 0px 0px 6px darken($gab-brand-default, 10%);
+ }
+
+ span {
+ display: none;
+ }
+ }
+
+ &__button {
+ margin-left: 12px;
+ height: 34px;
+ }
+
+ .flex {
+ display: flex;
+ }
+}
+
+.tabs-bar-item {
+ display: flex;
+ position: relative;
+ flex: 1 1 auto;
+ margin: 0 25px 0 0;
+ color: white;
+ text-decoration: none;
+ text-align: center;
+
+ @include background-image('/assets/images/sprite-main-navigation-links.png', auto 84px);
+
+ @media screen and (max-width: $nav-breakpoint-1) {
+ background-size: auto 120px;
+ margin: 4px 0 0 0;
+ padding: 0 !important;
+
+ @include size(46px, 42px);
+
+ &.active {
+ height: 38px;
+ border-bottom: 4px solid $gab-default-text-light;
+ }
+
+ &>span {
+ display: none;
+ }
+ }
+
+ // REMINDER - to add the remaining icons (globe / word balloon) from the sprite into this css as necessary
+ &.home {
+ padding: 16px 0 0 26px;
+ background-position: 0 18px;
+
+ &.active {
+ background-position: 0 -52px;
+ }
+
+ @media screen and (max-width: $nav-breakpoint-1) {
+ background-position: 11px 11px;
+
+ &.active {
+ background-position: 11px -89px;
+ }
+ }
+ }
+
+ &.notifications {
+ padding: 16px 0 0 22px;
+ background-position: -140px 18px;
+
+ &.active {
+ background-position: -140px -52px;
+ }
+
+ @media screen and (max-width: $nav-breakpoint-1) {
+ background-position: -186px 11px;
+
+ &.active {
+ background-position: -186px -89px;
+ }
+ }
+ }
+
+ &.groups {
+ padding: 16px 0 0 29px;
+ background-position: -280px 18px;
+
+ &.active {
+ background-position: -280px -52px;
+ }
+
+ @media screen and (max-width: $nav-breakpoint-1) {
+ background-position: -391px 11px;
+
+ &.active {
+ background-position: -391px -89px;
+ }
+ }
+ }
+
+ &.optional {
+ display: none;
+
+ @media screen and (max-width: $nav-breakpoint-2) {
+ display: flex;
+ background-position: -987px 11px;
+
+ &.active {
+ background-position: -987px -89px;
+ }
+ }
+ }
+
+ &.active {
+ color: $gab-text-highlight;
+ }
+
+ &--logo {
+ display: block;
+ margin-right: 35px;
+ border: none;
+
+ @include background-image('/assets/images/gab_logo.svg', 50px 30px, 0 10px);
+ @include size(50px);
+
+ // NOTE - Revisit right-margin of home button / positioning between 376px and 350px
+ // - want to keep the icons centered between logo and profile image while shrinking
+ @media screen and (max-width: $nav-breakpoint-4) {
+ display: none;
+ }
+
+ & span {
+ display: none !important;
+ }
+
+ &:hover {
+ background-color: #000 !important;
+ border: none !important;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/timeline_queue_button_header/index.js b/app/javascript/gabsocial/components/timeline_queue_button_header/index.js
index c8a22472..d0dfadbb 100644
--- a/app/javascript/gabsocial/components/timeline_queue_button_header/index.js
+++ b/app/javascript/gabsocial/components/timeline_queue_button_header/index.js
@@ -1,48 +1 @@
-import { FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import { shortNumberFormat } from '../../utils/numbers';
-
-import './index.scss';
-
-export default class TimelineQueueButtonHeader extends PureComponent {
-
- static propTypes = {
- onClick: PropTypes.func.isRequired,
- count: PropTypes.number,
- itemType: PropTypes.string,
- };
-
- static defaultProps = {
- count: 0,
- itemType: 'item',
- };
-
- render () {
- const { count, itemType, onClick } = this.props;
-
- const hasItems = (count > 0);
-
- const classes = classNames('timeline-queue-header', {
- 'timeline-queue-header--extended': hasItems,
- });
-
- return (
-
- );
- }
-
-}
\ No newline at end of file
+export { default } from './timeline_queue_button_header';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/timeline_queue_button_header/timeline_queue_button_header.js b/app/javascript/gabsocial/components/timeline_queue_button_header/timeline_queue_button_header.js
new file mode 100644
index 00000000..e2d95652
--- /dev/null
+++ b/app/javascript/gabsocial/components/timeline_queue_button_header/timeline_queue_button_header.js
@@ -0,0 +1,48 @@
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { shortNumberFormat } from '../../utils/numbers';
+
+import './timeline_queue_button_header.scss';
+
+export default class TimelineQueueButtonHeader extends PureComponent {
+
+ static propTypes = {
+ onClick: PropTypes.func.isRequired,
+ count: PropTypes.number,
+ itemType: PropTypes.string,
+ };
+
+ static defaultProps = {
+ count: 0,
+ itemType: 'item',
+ };
+
+ render () {
+ const { count, itemType, onClick } = this.props;
+
+ const hasItems = (count > 0);
+
+ const classes = classNames('timeline-queue-header', {
+ 'timeline-queue-header--extended': hasItems,
+ });
+
+ return (
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/timeline_queue_button_header/index.scss b/app/javascript/gabsocial/components/timeline_queue_button_header/timeline_queue_button_header.scss
similarity index 100%
rename from app/javascript/gabsocial/components/timeline_queue_button_header/index.scss
rename to app/javascript/gabsocial/components/timeline_queue_button_header/timeline_queue_button_header.scss
diff --git a/app/javascript/gabsocial/components/trending_item/index.js b/app/javascript/gabsocial/components/trending_item/index.js
index 71fef45e..7b819fe2 100644
--- a/app/javascript/gabsocial/components/trending_item/index.js
+++ b/app/javascript/gabsocial/components/trending_item/index.js
@@ -1,47 +1 @@
-import { Sparklines, SparklinesCurve } from 'react-sparklines';
-import { FormattedMessage } from 'react-intl';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import Permalink from '../permalink';
-import { shortNumberFormat } from '../../utils/numbers';
-
-import './index.scss';
-
-export default class TrendingItem extends ImmutablePureComponent {
-
- static propTypes = {
- hashtag: ImmutablePropTypes.map.isRequired,
- };
-
- render() {
- return (
-
-
-
- #{hashtag.get('name')}
-
-
-
{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))},
- }}
- />
-
-
-
- {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
-
-
-
- day.get('uses')).toArray()}>
-
-
-
-
- );
- }
-
-}
+export { default } from './trending_item';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/trending_item/trending_item.js b/app/javascript/gabsocial/components/trending_item/trending_item.js
new file mode 100644
index 00000000..55e85412
--- /dev/null
+++ b/app/javascript/gabsocial/components/trending_item/trending_item.js
@@ -0,0 +1,47 @@
+import { Sparklines, SparklinesCurve } from 'react-sparklines';
+import { FormattedMessage } from 'react-intl';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import Permalink from '../permalink/permalink';
+import { shortNumberFormat } from '../../utils/numbers';
+
+import './trending_item.scss';
+
+export default class TrendingItem extends ImmutablePureComponent {
+
+ static propTypes = {
+ hashtag: ImmutablePropTypes.map.isRequired,
+ };
+
+ render() {
+ return (
+
+
+
+ #{hashtag.get('name')}
+
+
+
{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))},
+ }}
+ />
+
+
+
+ {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
+
+
+
+ day.get('uses')).toArray()}>
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/gabsocial/components/trending_item/index.scss b/app/javascript/gabsocial/components/trending_item/trending_item.scss
similarity index 100%
rename from app/javascript/gabsocial/components/trending_item/index.scss
rename to app/javascript/gabsocial/components/trending_item/trending_item.scss
diff --git a/app/javascript/gabsocial/components/upload_area/index.js b/app/javascript/gabsocial/components/upload_area/index.js
new file mode 100644
index 00000000..c50c704a
--- /dev/null
+++ b/app/javascript/gabsocial/components/upload_area/index.js
@@ -0,0 +1 @@
+export { default } from './upload_area';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/ui/components/upload_area.js b/app/javascript/gabsocial/components/upload_area/upload_area.js
similarity index 83%
rename from app/javascript/gabsocial/features/ui/components/upload_area.js
rename to app/javascript/gabsocial/components/upload_area/upload_area.js
index bfacc1c3..ee810b20 100644
--- a/app/javascript/gabsocial/features/ui/components/upload_area.js
+++ b/app/javascript/gabsocial/components/upload_area/upload_area.js
@@ -1,7 +1,9 @@
-import Motion from '../../ui/util/optional_motion';
+import Motion from '../../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl';
+import './upload_area.scss';
+
export default class UploadArea extends PureComponent {
static propTypes = {
@@ -10,15 +12,15 @@ export default class UploadArea extends PureComponent {
};
handleKeyUp = (e) => {
+ if (!this.props.active) return;
+
const keyCode = e.keyCode;
- if (this.props.active) {
- switch(keyCode) {
- case 27:
- e.preventDefault();
- e.stopPropagation();
- this.props.onClose();
- break;
- }
+ switch(keyCode) {
+ case 27:
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.onClose();
+ break;
}
}
@@ -47,4 +49,4 @@ export default class UploadArea extends PureComponent {
);
}
-}
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/upload_area/upload_area.scss b/app/javascript/gabsocial/components/upload_area/upload_area.scss
new file mode 100644
index 00000000..1c7969c0
--- /dev/null
+++ b/app/javascript/gabsocial/components/upload_area/upload_area.scss
@@ -0,0 +1,42 @@
+.upload-area {
+ background: rgba($base-overlay-background, 0.8);
+ opacity: 0;
+ visibility: hidden;
+ z-index: 9999;
+
+ @include flex(center, center);
+ @include size(100%);
+ @include abs-position(0, auto, auto, 0);
+
+ * {
+ pointer-events: none;
+ }
+
+ &__drop {
+ display: flex;
+ box-sizing: border-box;
+ position: relative;
+ padding: 8px;
+
+ @include size(320px, 160px);
+ }
+
+ &__background {
+ z-index: -1;
+ border-radius: 4px;
+ background: $ui-base-color;
+ box-shadow: 0 0 5px rgba($base-shadow-color, 0.2);
+
+ @include abs-position(0, 0, 0, 0);
+ }
+
+ &__content {
+ flex: 1;
+ color: $secondary-text-color;
+ border: 2px dashed $ui-base-lighter-color;
+ border-radius: 4px;
+
+ @include flex(center, center);
+ @include text-sizing(18px, 500);
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/user_panel/index.js b/app/javascript/gabsocial/components/user_panel/index.js
new file mode 100644
index 00000000..0a8281b4
--- /dev/null
+++ b/app/javascript/gabsocial/components/user_panel/index.js
@@ -0,0 +1 @@
+export { default } from './user_panel';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/ui/components/user_panel.js b/app/javascript/gabsocial/components/user_panel/user_panel.js
similarity index 73%
rename from app/javascript/gabsocial/features/ui/components/user_panel.js
rename to app/javascript/gabsocial/components/user_panel/user_panel.js
index e7dbea30..f6fe6bf6 100644
--- a/app/javascript/gabsocial/features/ui/components/user_panel.js
+++ b/app/javascript/gabsocial/components/user_panel/user_panel.js
@@ -1,23 +1,40 @@
import { Link } from 'react-router-dom';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import { autoPlayGif, me } from '../../../initial_state';
-import { makeGetAccount } from '../../../selectors';
+import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Avatar from 'gabsocial/components/avatar';
-import { shortNumberFormat } from 'gabsocial/utils/numbers';
+import { autoPlayGif, me } from '../../initial_state';
+import { makeGetAccount } from '../../selectors';
+import Avatar from '../avatar';
+import { shortNumberFormat } from '../../utils/numbers';
+import './user_panel.scss';
+
+const messages = defineMessages({
+ gabs: { id:'account.posts', defaultMessage: 'Gabs' },
+ followers: { id: 'account.followers', defaultMessage: 'Followers' },
+ follows: { id: 'account.follows', defaultMessage: 'Follows' }
+});
+
+
+const mapStateToProps = state => {
+ const getAccount = makeGetAccount();
+
+ return {
+ account: getAccount(state, me),
+ };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
class UserPanel extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
- domain: PropTypes.string,
}
render() {
- const { account, intl, domain } = this.props;
+ const { account, intl } = this.props;
const displayNameHtml = { __html: account.get('display_name_html') };
- const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
return (
@@ -28,7 +45,7 @@ class UserPanel extends ImmutablePureComponent {
@@ -39,7 +56,7 @@ class UserPanel extends ImmutablePureComponent {
- @{acct}
+ @{account.get('acct')}
@@ -49,21 +66,21 @@ class UserPanel extends ImmutablePureComponent {
@@ -76,18 +93,3 @@ class UserPanel extends ImmutablePureComponent {
)
}
};
-
-
-const mapStateToProps = state => {
- const getAccount = makeGetAccount();
-
- return {
- account: getAccount(state, me),
- };
-};
-
-export default injectIntl(
- connect(mapStateToProps, null, null, {
- forwardRef: true,
- }
-)(UserPanel))
diff --git a/app/javascript/gabsocial/components/user_panel/user_panel.scss b/app/javascript/gabsocial/components/user_panel/user_panel.scss
new file mode 100644
index 00000000..43a3f15d
--- /dev/null
+++ b/app/javascript/gabsocial/components/user_panel/user_panel.scss
@@ -0,0 +1,125 @@
+.user-panel {
+ display: flex;
+ width: 265px;
+ flex-direction: column;
+ overflow-y: hidden;
+
+ // @include gab-container-standards();
+
+ &__header {
+ display: block;
+ background: lighten($gab-background-container, 4%);
+
+ @include size(100%, 112px);
+
+ body.theme-gabsocial-light & {
+ background: darken($gab-background-container-light, 4%);
+ }
+
+ img {
+ display: block;
+ margin: 0;
+ object-fit: cover;
+
+ @include size(100%);
+ }
+ }
+
+ &__profile {
+ display: flex;
+ align-items: flex-start;
+ padding: 0 10px;
+ margin-top: -53px;
+
+ .account__avatar {
+ display: block;
+ border: 6px solid $gab-background-base;
+ background-size: cover;
+
+ @include size(82px);
+
+ body.theme-gabsocial-light & {
+ border: 6px solid $gab-background-base-light;
+ }
+ }
+ }
+
+ &__meta {
+ display: block;
+ padding: 6px 20px 17px 20px;
+ }
+
+ &__account {
+ a {
+ text-decoration: none;
+ color: $primary-text-color;
+ }
+
+ &__name {
+ display: block;
+ color: #fff;
+
+ @include text-sizing(20px, 700, 24px);
+
+ body.theme-gabsocial-light & {
+ color: $gab-default-text-light;
+ }
+ }
+
+ &:hover & {
+ &__name {
+ text-decoration: underline;
+ }
+ }
+
+ &__username {
+ display: block;
+ font-size: 14px;
+ line-height: 16px;
+ color: $gab-secondary-text;
+ text-decoration: none !important;
+ }
+ }
+
+ &__stats-block {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 12px;
+ }
+
+ .user-panel-stats-item {
+ flex-wrap: wrap;
+
+ @include flex(left, start, column);
+
+ a {
+ text-decoration: none;
+
+ color: $primary-text-color;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+
+ &__value {
+ display: block;
+ width: 100%;
+ color: #fff;
+
+ @include text-sizing(20px, 800, 24px);
+
+ body.theme-gabsocial-light & {
+ color: $gab-default-text-light;
+ }
+ }
+
+ &__label {
+ display: block;
+ width: 100%;
+ color: $gab-secondary-text;
+
+ @include text-sizing(12px, 400, 14px);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/verified_icon/index.js b/app/javascript/gabsocial/components/verified_icon/index.js
index 5fc2d91b..f8d58dee 100644
--- a/app/javascript/gabsocial/components/verified_icon/index.js
+++ b/app/javascript/gabsocial/components/verified_icon/index.js
@@ -1,13 +1 @@
-import './index.scss';
-
-export default class VerifiedIcon extends PureComponent {
-
- render() {
- return (
-
- );
- }
-
-};
+export { default } from './verified_icon';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/verified_icon/verified_icon.js b/app/javascript/gabsocial/components/verified_icon/verified_icon.js
new file mode 100644
index 00000000..5ba1202e
--- /dev/null
+++ b/app/javascript/gabsocial/components/verified_icon/verified_icon.js
@@ -0,0 +1,13 @@
+import './verified_icon.scss';
+
+export default class VerifiedIcon extends PureComponent {
+
+ render() {
+ return (
+
+ );
+ }
+
+};
diff --git a/app/javascript/gabsocial/components/verified_icon/index.scss b/app/javascript/gabsocial/components/verified_icon/verified_icon.scss
similarity index 100%
rename from app/javascript/gabsocial/components/verified_icon/index.scss
rename to app/javascript/gabsocial/components/verified_icon/verified_icon.scss
diff --git a/app/javascript/gabsocial/components/zoomable_image/index.js b/app/javascript/gabsocial/components/zoomable_image/index.js
new file mode 100644
index 00000000..51d88f4f
--- /dev/null
+++ b/app/javascript/gabsocial/components/zoomable_image/index.js
@@ -0,0 +1 @@
+export { default } from './zoomable_image';
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/ui/components/zoomable_image.js b/app/javascript/gabsocial/components/zoomable_image/zoomable_image.js
similarity index 96%
rename from app/javascript/gabsocial/features/ui/components/zoomable_image.js
rename to app/javascript/gabsocial/components/zoomable_image/zoomable_image.js
index d58b470e..409f287e 100644
--- a/app/javascript/gabsocial/features/ui/components/zoomable_image.js
+++ b/app/javascript/gabsocial/components/zoomable_image/zoomable_image.js
@@ -1,3 +1,4 @@
+import './zoomable_image.scss';
const MIN_SCALE = 1;
const MAX_SCALE = 4;
@@ -126,11 +127,7 @@ export default class ZoomableImage extends PureComponent {
const overflow = scale === 1 ? 'hidden' : 'scroll';
return (
-