diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 1d670a344..cc91ded59 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -56,6 +56,7 @@ interface IAccount { showProfileHoverCard?: boolean, timestamp?: string | Date, timestampUrl?: string, + futureTimestamp?: boolean, withDate?: boolean, withRelationship?: boolean, showEdit?: boolean, @@ -75,6 +76,7 @@ const Account = ({ showProfileHoverCard = true, timestamp, timestampUrl, + futureTimestamp = false, withDate = false, withRelationship = true, showEdit = false, @@ -205,10 +207,10 @@ const Account = ({ {timestampUrl ? ( - + ) : ( - + )} ) : null} diff --git a/app/soapbox/components/ui/hstack/hstack.tsx b/app/soapbox/components/ui/hstack/hstack.tsx index 44dac93c7..803bbd7c1 100644 --- a/app/soapbox/components/ui/hstack/hstack.tsx +++ b/app/soapbox/components/ui/hstack/hstack.tsx @@ -4,6 +4,8 @@ import React from 'react'; const justifyContentOptions = { between: 'justify-between', center: 'justify-center', + start: 'justify-start', + end: 'justify-end', }; const alignItemsOptions = { @@ -29,7 +31,7 @@ interface IHStack { /** Extra class names on the
element. */ className?: string, /** Horizontal alignment of children. */ - justifyContent?: 'between' | 'center', + justifyContent?: 'between' | 'center' | 'start' | 'end', /** Size of the gap between elements. */ space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6, /** Whether to let the flexbox grow. */ diff --git a/app/soapbox/features/scheduled_statuses/builder.js b/app/soapbox/features/scheduled_statuses/builder.tsx similarity index 85% rename from app/soapbox/features/scheduled_statuses/builder.js rename to app/soapbox/features/scheduled_statuses/builder.tsx index 8e3417582..2927c8984 100644 --- a/app/soapbox/features/scheduled_statuses/builder.js +++ b/app/soapbox/features/scheduled_statuses/builder.tsx @@ -3,11 +3,13 @@ import { Map as ImmutableMap } from 'immutable'; import { normalizeStatus } from 'soapbox/normalizers/status'; import { calculateStatus } from 'soapbox/reducers/statuses'; import { makeGetAccount } from 'soapbox/selectors'; +import { RootState } from 'soapbox/store'; -export const buildStatus = (state, scheduledStatus) => { +export const buildStatus = (state: RootState, scheduledStatus: ImmutableMap) => { const getAccount = makeGetAccount(); - const me = state.get('me'); + const me = state.me as string; + const params = scheduledStatus.get('params'); const account = getAccount(state, me); diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status.js deleted file mode 100644 index b88ccc450..000000000 --- a/app/soapbox/features/scheduled_statuses/components/scheduled_status.js +++ /dev/null @@ -1,91 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; -import { Link, NavLink } from 'react-router-dom'; - -import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import RelativeTimestamp from 'soapbox/components/relative_timestamp'; -import StatusContent from 'soapbox/components/status_content'; -import StatusReplyMentions from 'soapbox/components/status_reply_mentions'; -import PollPreview from 'soapbox/features/ui/components/poll_preview'; -import { getDomain } from 'soapbox/utils/accounts'; - -import { buildStatus } from '../builder'; - -import ScheduledStatusActionBar from './scheduled_status_action_bar'; - -const mapStateToProps = (state, props) => { - const scheduledStatus = state.getIn(['scheduled_statuses', props.statusId]); - return { - status: buildStatus(state, scheduledStatus), - }; -}; - -export default @connect(mapStateToProps) -class ScheduledStatus extends ImmutablePureComponent { - - render() { - const { status, account, ...other } = this.props; - if (!status.get('account')) return null; - - const statusUrl = `/scheduled_statuses/${status.get('id')}`; - const favicon = status.getIn(['account', 'pleroma', 'favicon']); - const domain = getDomain(status.get('account')); - - return ( -
-
-
-
-
- - - - - {favicon && -
- - - -
} - -
-
- - - -
- - - -
-
- - - - - - {status.get('media_attachments').size > 0 && ( - - )} - - {status.get('poll') && } - - -
-
-
- ); - } - -} diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx new file mode 100644 index 000000000..0d8b14cd3 --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx @@ -0,0 +1,66 @@ +import classNames from 'classnames'; +import React from 'react'; + +import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; +import StatusContent from 'soapbox/components/status_content'; +import StatusReplyMentions from 'soapbox/components/status_reply_mentions'; +import { HStack } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import PollPreview from 'soapbox/features/ui/components/poll_preview'; +import { useAppSelector } from 'soapbox/hooks'; + +import { buildStatus } from '../builder'; + +import ScheduledStatusActionBar from './scheduled_status_action_bar'; + +import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; + +interface IScheduledStatus { + statusId: string, +} + +const ScheduledStatus: React.FC = ({ statusId, ...other }) => { + const status = useAppSelector((state) => buildStatus(state, state.scheduled_statuses.get(statusId))) as StatusEntity; + + if (!status) return null; + + const account = status.account as AccountEntity; + + return ( +
+
+
+ + + +
+ + + + + + {status.media_attachments.size > 0 && ( + + )} + + {status.poll && } + + +
+
+ ); +}; + +export default ScheduledStatus; diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js deleted file mode 100644 index 7ed5c4d08..000000000 --- a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js +++ /dev/null @@ -1,83 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { openModal } from 'soapbox/actions/modals'; -import { cancelScheduledStatus } from 'soapbox/actions/scheduled_statuses'; -import { getSettings } from 'soapbox/actions/settings'; -import IconButton from 'soapbox/components/icon_button'; -import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; - -const messages = defineMessages({ - cancel: { id: 'scheduled_status.cancel', defaultMessage: 'Cancel' }, - deleteConfirm: { id: 'confirmations.scheduled_status_delete.confirm', defaultMessage: 'Cancel' }, - deleteHeading: { id: 'confirmations.scheduled_status_delete.heading', defaultMessage: 'Cancel scheduled post' }, - deleteMessage: { id: 'confirmations.scheduled_status_delete.message', defaultMessage: 'Are you sure you want to cancel this scheduled post?' }, -}); - -const mapStateToProps = state => { - const me = state.get('me'); - return { - me, - }; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onCancelClick: (status) => { - dispatch((_, getState) => { - - const deleteModal = getSettings(getState()).get('deleteModal'); - if (!deleteModal) { - dispatch(cancelScheduledStatus(status.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - icon: require('@tabler/icons/icons/calendar-stats.svg'), - heading: intl.formatMessage(messages.deleteHeading), - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(cancelScheduledStatus(status.get('id'))), - })); - } - }); - }, -}); - -class ScheduledStatusActionBar extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.record.isRequired, - intl: PropTypes.object.isRequired, - me: SoapboxPropTypes.me, - onCancelClick: PropTypes.func.isRequired, - }; - - handleCancelClick = e => { - const { status, onCancelClick } = this.props; - - onCancelClick(status); - } - - render() { - const { intl } = this.props; - - return ( -
-
- -
-
- ); - } - -} - - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScheduledStatusActionBar)); diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.tsx b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.tsx new file mode 100644 index 000000000..7b44805d4 --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { openModal } from 'soapbox/actions/modals'; +import { cancelScheduledStatus } from 'soapbox/actions/scheduled_statuses'; +import { getSettings } from 'soapbox/actions/settings'; +import IconButton from 'soapbox/components/icon_button'; +import { HStack } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +import type { Status as StatusEntity } from 'soapbox/types/entities'; + +const messages = defineMessages({ + cancel: { id: 'scheduled_status.cancel', defaultMessage: 'Cancel' }, + deleteConfirm: { id: 'confirmations.scheduled_status_delete.confirm', defaultMessage: 'Cancel' }, + deleteHeading: { id: 'confirmations.scheduled_status_delete.heading', defaultMessage: 'Cancel scheduled post' }, + deleteMessage: { id: 'confirmations.scheduled_status_delete.message', defaultMessage: 'Are you sure you want to cancel this scheduled post?' }, +}); + +interface IScheduledStatusActionBar { + status: StatusEntity, +} + +const ScheduledStatusActionBar: React.FC = ({ status }) => { + const intl = useIntl(); + + const dispatch = useAppDispatch(); + + const handleCancelClick = () => { + dispatch((_, getState) => { + + const deleteModal = getSettings(getState()).get('deleteModal'); + if (!deleteModal) { + dispatch(cancelScheduledStatus(status.id)); + } else { + dispatch(openModal('CONFIRM', { + icon: require('@tabler/icons/icons/calendar-stats.svg'), + heading: intl.formatMessage(messages.deleteHeading), + message: intl.formatMessage(messages.deleteMessage), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => dispatch(cancelScheduledStatus(status.id)), + })); + } + }); + }; + + return ( + + + + ); +}; + +export default ScheduledStatusActionBar; diff --git a/app/soapbox/features/scheduled_statuses/index.js b/app/soapbox/features/scheduled_statuses/index.js deleted file mode 100644 index b8a63497e..000000000 --- a/app/soapbox/features/scheduled_statuses/index.js +++ /dev/null @@ -1,66 +0,0 @@ -import { debounce } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import ScrollableList from 'soapbox/components/scrollable_list'; - -import { fetchScheduledStatuses, expandScheduledStatuses } from '../../actions/scheduled_statuses'; -import Column from '../ui/components/column'; - -import ScheduledStatus from './components/scheduled_status'; - -const messages = defineMessages({ - heading: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled Posts' }, -}); - -const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'scheduled_statuses', 'items']), - isLoading: state.getIn(['status_lists', 'scheduled_statuses', 'isLoading'], true), - hasMore: !!state.getIn(['status_lists', 'scheduled_statuses', 'next']), -}); - -export default @connect(mapStateToProps) -@injectIntl -class ScheduledStatuses extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.orderedSet.isRequired, - intl: PropTypes.object.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - }; - - componentDidMount() { - const { dispatch } = this.props; - dispatch(fetchScheduledStatuses()); - } - - handleLoadMore = debounce(() => { - this.props.dispatch(expandScheduledStatuses()); - }, 300, { leading: true }) - - - render() { - const { intl, statusIds, hasMore, isLoading } = this.props; - const emptyMessage = ; - - return ( - - - {statusIds.map(id => )} - - - ); - } - -} diff --git a/app/soapbox/features/scheduled_statuses/index.tsx b/app/soapbox/features/scheduled_statuses/index.tsx new file mode 100644 index 000000000..7d387022d --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/index.tsx @@ -0,0 +1,51 @@ +import { debounce } from 'lodash'; +import React from 'react'; +import { useEffect } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { fetchScheduledStatuses, expandScheduledStatuses } from 'soapbox/actions/scheduled_statuses'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; + +import Column from '../ui/components/column'; + +import ScheduledStatus from './components/scheduled_status'; + +const messages = defineMessages({ + heading: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled Posts' }, +}); + +const handleLoadMore = debounce((dispatch) => { + dispatch(expandScheduledStatuses()); +}, 300, { leading: true }); + +const ScheduledStatuses = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const statusIds = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'items'])); + const isLoading = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'isLoading'])); + const hasMore = useAppSelector((state) => !!state.status_lists.getIn(['scheduled_statuses', 'next'])); + + useEffect(() => { + dispatch(fetchScheduledStatuses()); + }, []); + + const emptyMessage = ; + + return ( + + handleLoadMore(dispatch)} + emptyMessage={emptyMessage} + > + {statusIds.map((id: string) => )} + + + ); +}; + +export default ScheduledStatuses; \ No newline at end of file