Added self-destructing/expiring statuses
• Added: - self-destructing/expiring statuses for GabPRO members only - ExpiringStatusWorker - stopwatch icon - expires_at redux values - expires_at button in composer - expires at selection popover • Updated: - Schedule status button to not show if expiring status active
This commit is contained in:
parent
5f4e7aad31
commit
043fc01cea
@ -63,6 +63,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
expires_at: status_params[:expires_at],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
idempotency: request.headers['Idempotency-Key'],
|
||||
@ -79,6 +80,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
text: status_params[:status],
|
||||
markdown: markdown,
|
||||
media_ids: status_params[:media_ids],
|
||||
expires_at: status_params[:expires_at],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
@ -117,6 +119,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
:spoiler_text,
|
||||
:visibility,
|
||||
:scheduled_at,
|
||||
:expires_at,
|
||||
:group_id,
|
||||
media_ids: [],
|
||||
poll: [
|
||||
|
@ -15,6 +15,14 @@ import { updateTimeline, dequeueTimeline } from './timelines';
|
||||
// import { showAlert, showAlertForError } from './alerts';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { openModal, closeModal } from './modal';
|
||||
import {
|
||||
STATUS_EXPIRATION_OPTION_5_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_60_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_6_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_24_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_3_DAYS,
|
||||
STATUS_EXPIRATION_OPTION_7_DAYS,
|
||||
} from '../constants'
|
||||
import { me } from '../initial_state';
|
||||
import { makeGetStatus } from '../selectors'
|
||||
|
||||
@ -67,6 +75,8 @@ export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
||||
|
||||
export const COMPOSE_SCHEDULED_AT_CHANGE = 'COMPOSE_SCHEDULED_AT_CHANGE';
|
||||
|
||||
export const COMPOSE_EXPIRES_AT_CHANGE = 'COMPOSE_EXPIRES_AT_CHANGE'
|
||||
|
||||
export const COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY = 'COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY'
|
||||
|
||||
export const COMPOSE_CLEAR = 'COMPOSE_CLEAR'
|
||||
@ -299,6 +309,24 @@ export function submitCompose(groupId, replyToId = null, router, isStandalone, a
|
||||
let scheduled_at = getState().getIn(['compose', 'scheduled_at'], null);
|
||||
if (scheduled_at !== null) scheduled_at = moment.utc(scheduled_at).toDate();
|
||||
|
||||
let expires_at = getState().getIn(['compose', 'expires_at'], null);
|
||||
|
||||
if (expires_at) {
|
||||
if (expires_at === STATUS_EXPIRATION_OPTION_5_MINUTES) {
|
||||
expires_at = moment.utc().add('5', 'minute').toDate()
|
||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_60_MINUTES) {
|
||||
expires_at = moment.utc().add('60', 'minute').toDate()
|
||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_6_HOURS) {
|
||||
expires_at = moment.utc().add('6', 'hour').toDate()
|
||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_24_HOURS) {
|
||||
expires_at = moment.utc().add('24', 'hour').toDate()
|
||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_3_DAYS) {
|
||||
expires_at = moment.utc().add('3', 'day').toDate()
|
||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_7_DAYS) {
|
||||
expires_at = moment.utc().add('7', 'day').toDate()
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobile(window.innerWidth) && router && isStandalone) {
|
||||
router.history.goBack()
|
||||
}
|
||||
@ -306,6 +334,7 @@ export function submitCompose(groupId, replyToId = null, router, isStandalone, a
|
||||
api(getState)[method](endpoint, {
|
||||
status,
|
||||
markdown,
|
||||
expires_at,
|
||||
scheduled_at,
|
||||
autoJoinGroup,
|
||||
in_reply_to_id: inReplyToId,
|
||||
@ -715,6 +744,13 @@ export function changeScheduledAt(date) {
|
||||
};
|
||||
};
|
||||
|
||||
export function changeExpiresAt(value) {
|
||||
return {
|
||||
type: COMPOSE_EXPIRES_AT_CHANGE,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
export function changeRichTextEditorControlsVisibility(open) {
|
||||
return {
|
||||
type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
|
||||
|
25
app/javascript/gabsocial/assets/stopwatch_icon.js
Normal file
25
app/javascript/gabsocial/assets/stopwatch_icon.js
Normal file
@ -0,0 +1,25 @@
|
||||
const StopwatchIcon = ({
|
||||
className = '',
|
||||
size = '16px',
|
||||
title = '',
|
||||
}) => (
|
||||
<svg
|
||||
className={className}
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
x='0px'
|
||||
y='0px'
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox='0 0 48 48'
|
||||
xmlSpace='preserve'
|
||||
aria-label={title}
|
||||
>
|
||||
<g>
|
||||
<path d='M 18.01 0 L 18.01 4 L 22.02 4 L 22.02 6.01 C 23.42 5.8 24.62 5.8 26.02 6.01 L 26.02 4 L 30.02 4 L 30.02 0 Z M 18.01 0' />
|
||||
<path d='M 37.63 13.21 L 39.04 11.81 L 40.64 13.41 L 43.44 10.61 L 37.43 4.61 L 34.63 7.41 L 36.23 9.01 L 34.43 10.81 C 24.82 5 12.41 8.01 6.8 17.62 C 1.2 27.23 4.2 39.24 13.61 45.05 C 23.02 50.85 35.43 47.85 41.04 38.24 C 46.04 30.03 44.64 19.62 37.63 13.21 Z M 24.02 41.84 C 16.21 41.84 10 35.64 10 27.83 C 10 20.02 16.21 13.81 24.02 13.81 L 24.02 27.83 L 38.03 27.83 C 38.03 35.64 31.83 41.84 24.02 41.84 Z M 24.02 41.84' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default StopwatchIcon
|
@ -68,6 +68,7 @@ import ShareIcon from '../assets/share_icon'
|
||||
import ShopIcon from '../assets/shop_icon'
|
||||
import SortIcon from '../assets/sort_icon'
|
||||
import StarIcon from '../assets/star_icon'
|
||||
import StopWatchIcon from '../assets/stopwatch_icon'
|
||||
import StrikethroughIcon from '../assets/strikethrough_icon'
|
||||
import SubtractIcon from '../assets/subtract_icon'
|
||||
import TextSizeIcon from '../assets/text_size_icon'
|
||||
@ -150,6 +151,7 @@ const ICONS = {
|
||||
'shop': ShopIcon,
|
||||
'sort': SortIcon,
|
||||
'star': StarIcon,
|
||||
'stopwatch': StopWatchIcon,
|
||||
'strikethrough': StrikethroughIcon,
|
||||
'subtract': SubtractIcon,
|
||||
'text-size': TextSizeIcon,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
POPOVER_SEARCH,
|
||||
POPOVER_SIDEBAR_MORE,
|
||||
POPOVER_STATUS_OPTIONS,
|
||||
POPOVER_STATUS_EXPIRATION_OPTIONS,
|
||||
POPOVER_STATUS_VISIBILITY,
|
||||
POPOVER_USER_INFO,
|
||||
POPOVER_VIDEO_STATS,
|
||||
@ -24,6 +25,7 @@ import {
|
||||
ProfileOptionsPopover,
|
||||
SearchPopover,
|
||||
SidebarMorePopover,
|
||||
StatusExpirationOptionsPopover,
|
||||
StatusOptionsPopover,
|
||||
StatusVisibilityPopover,
|
||||
UserInfoPopover,
|
||||
@ -51,6 +53,7 @@ POPOVER_COMPONENTS[POPOVER_PROFILE_OPTIONS] = ProfileOptionsPopover
|
||||
POPOVER_COMPONENTS[POPOVER_SEARCH] = SearchPopover
|
||||
POPOVER_COMPONENTS[POPOVER_SIDEBAR_MORE] = SidebarMorePopover
|
||||
POPOVER_COMPONENTS[POPOVER_STATUS_OPTIONS] = StatusOptionsPopover
|
||||
POPOVER_COMPONENTS[POPOVER_STATUS_EXPIRATION_OPTIONS] = StatusExpirationOptionsPopover
|
||||
POPOVER_COMPONENTS[POPOVER_STATUS_VISIBILITY] = StatusVisibilityPopover
|
||||
POPOVER_COMPONENTS[POPOVER_USER_INFO] = UserInfoPopover
|
||||
POPOVER_COMPONENTS[POPOVER_VIDEO_STATS] = VideoStatsPopover
|
||||
|
@ -0,0 +1,126 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import { closePopover } from '../../actions/popover'
|
||||
import { changeExpiresAt } from '../../actions/compose'
|
||||
import {
|
||||
STATUS_EXPIRATION_OPTION_5_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_60_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_6_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_24_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_3_DAYS,
|
||||
STATUS_EXPIRATION_OPTION_7_DAYS,
|
||||
} from '../../constants'
|
||||
import PopoverLayout from './popover_layout'
|
||||
import List from '../list'
|
||||
|
||||
const messages = defineMessages({
|
||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
expiresAtValue: state.getIn(['compose', 'expires_at']),
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onChangeExpiresAt(expiresAt) {
|
||||
dispatch(changeExpiresAt(expiresAt))
|
||||
},
|
||||
onClosePopover() {
|
||||
dispatch(closePopover())
|
||||
},
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
class StatusExpirationOptionsPopover extends PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
expiresAtValue: PropTypes.string.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isXS: PropTypes.bool,
|
||||
onChangeExpiresAt: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
handleOnSetStatusExpiration = (expiresAt) => {
|
||||
this.props.onChangeExpiresAt(expiresAt)
|
||||
this.handleOnClosePopover()
|
||||
}
|
||||
|
||||
handleOnClosePopover = () => {
|
||||
this.props.onClosePopover()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
expiresAtValue,
|
||||
intl,
|
||||
isXS,
|
||||
} = this.props
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.minutes, { number: 5 }),
|
||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_5_MINUTES),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_5_MINUTES,
|
||||
},
|
||||
{
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.minutes, { number: 60 }),
|
||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_60_MINUTES),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_60_MINUTES,
|
||||
},
|
||||
{
|
||||
hideArrow: true,
|
||||
title: '6 hours',
|
||||
title: intl.formatMessage(messages.hours, { number: 6 }),
|
||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_6_HOURS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_6_HOURS,
|
||||
},
|
||||
{
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.hours, { number: 24 }),
|
||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_24_HOURS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_24_HOURS,
|
||||
},
|
||||
{
|
||||
hideArrow: true,
|
||||
title: '3 days',
|
||||
title: intl.formatMessage(messages.days, { number: 3 }),
|
||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_3_DAYS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_3_DAYS,
|
||||
},
|
||||
{
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.days, { number: 7 }),
|
||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_7_DAYS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_7_DAYS,
|
||||
},
|
||||
]
|
||||
|
||||
if (expiresAtValue) {
|
||||
listItems.unshift({
|
||||
hideArrow: true,
|
||||
title: 'Remove expiration',
|
||||
onClick: () => this.handleOnSetStatusExpiration(null),
|
||||
},)
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverLayout
|
||||
width={210}
|
||||
isXS={isXS}
|
||||
onClose={this.handleOnClosePopover}
|
||||
>
|
||||
<List
|
||||
scrollKey='group_list_sort_options'
|
||||
items={listItems}
|
||||
size={isXS ? 'large' : 'small'}
|
||||
/>
|
||||
</PopoverLayout>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -4,6 +4,7 @@ import { NavLink } from 'react-router-dom'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import classNames from 'classnames/bind'
|
||||
import moment from 'moment-mini'
|
||||
import { openPopover } from '../actions/popover'
|
||||
import { openModal } from '../actions/modal'
|
||||
import { me } from '../initial_state'
|
||||
@ -17,6 +18,7 @@ import Avatar from './avatar'
|
||||
|
||||
const messages = defineMessages({
|
||||
edited: { id: 'status.edited', defaultMessage: 'Edited' },
|
||||
expirationMessage: { id: 'status.expiration_message', defaultMessage: 'This status expires {time}' },
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for anyone on or off Gab' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
@ -101,6 +103,12 @@ class StatusHeader extends ImmutablePureComponent {
|
||||
visibilityText = `${intl.formatMessage(messages.public_short)} - ${intl.formatMessage(messages.public_long)}`
|
||||
}
|
||||
|
||||
const expirationDate = status.get('expires_at')
|
||||
let timeUntilExpiration
|
||||
if (!!expirationDate) {
|
||||
timeUntilExpiration = moment(expirationDate).fromNow()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}>
|
||||
@ -162,6 +170,18 @@ class StatusHeader extends ImmutablePureComponent {
|
||||
<Icon id={visibilityIcon} size='12px' className={[_s.default, _s.fillSecondary].join(' ')} />
|
||||
</span>
|
||||
|
||||
{
|
||||
!!status.get('expires_at') &&
|
||||
<Fragment>
|
||||
<DotTextSeperator />
|
||||
<span title={intl.formatMessage(messages.expirationMessage, {
|
||||
time: timeUntilExpiration,
|
||||
})} className={[_s.default, _s.displayInline, _s.ml5].join(' ')}>
|
||||
<Icon id='stopwatch' size='13px' className={[_s.default, _s.fillSecondary].join(' ')} />
|
||||
</span>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
{
|
||||
!!status.get('group') &&
|
||||
<Fragment>
|
||||
|
@ -30,6 +30,7 @@ export const POPOVER_PROFILE_OPTIONS = 'PROFILE_OPTIONS'
|
||||
export const POPOVER_SEARCH = 'SEARCH'
|
||||
export const POPOVER_SIDEBAR_MORE = 'SIDEBAR_MORE'
|
||||
export const POPOVER_STATUS_OPTIONS = 'STATUS_OPTIONS'
|
||||
export const POPOVER_STATUS_EXPIRATION_OPTIONS = 'STATUS_EXPIRATION_OPTIONS'
|
||||
export const POPOVER_STATUS_VISIBILITY = 'STATUS_VISIBILITY'
|
||||
export const POPOVER_USER_INFO = 'USER_INFO'
|
||||
export const POPOVER_VIDEO_STATS = 'VIDEO_STATS'
|
||||
@ -111,4 +112,11 @@ export const NOTIFICATION_FILTERS = [
|
||||
|
||||
export const GAB_COM_INTRODUCE_YOURSELF_GROUP_ID = '12'
|
||||
|
||||
export const MIN_ACCOUNT_CREATED_AT_ONBOARDING = 1594789200000 // 2020-07-15
|
||||
export const MIN_ACCOUNT_CREATED_AT_ONBOARDING = 1594789200000 // 2020-07-15
|
||||
|
||||
export const STATUS_EXPIRATION_OPTION_5_MINUTES = '5-minutes'
|
||||
export const STATUS_EXPIRATION_OPTION_60_MINUTES = '60-minutes'
|
||||
export const STATUS_EXPIRATION_OPTION_6_HOURS = '6-hours'
|
||||
export const STATUS_EXPIRATION_OPTION_24_HOURS = '24-hours'
|
||||
export const STATUS_EXPIRATION_OPTION_3_DAYS = '3-days'
|
||||
export const STATUS_EXPIRATION_OPTION_7_DAYS = '7-days'
|
@ -21,6 +21,7 @@ import PollButton from './poll_button'
|
||||
import PollForm from './poll_form'
|
||||
import SchedulePostButton from './schedule_post_button'
|
||||
import SpoilerButton from './spoiler_button'
|
||||
import ExpiresPostButton from './expires_post_button'
|
||||
import RichTextEditorButton from './rich_text_editor_button'
|
||||
import StatusContainer from '../../../containers/status_container'
|
||||
import StatusVisibilityButton from './status_visibility_button'
|
||||
@ -484,10 +485,15 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<StatusVisibilityButton />
|
||||
<SpoilerButton />
|
||||
{
|
||||
!hidePro &&
|
||||
!hidePro && !edit &&
|
||||
<SchedulePostButton />
|
||||
}
|
||||
|
||||
{
|
||||
!hidePro && !edit &&
|
||||
<ExpiresPostButton />
|
||||
}
|
||||
|
||||
{
|
||||
!hidePro &&
|
||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
||||
|
@ -0,0 +1,78 @@
|
||||
import { injectIntl, defineMessages } from 'react-intl'
|
||||
import { openModal } from '../../../actions/modal'
|
||||
import { openPopover } from '../../../actions/popover'
|
||||
import { me } from '../../../initial_state'
|
||||
import { POPOVER_STATUS_EXPIRATION_OPTIONS } from '../../../constants'
|
||||
import ComposeExtraButton from './compose_extra_button'
|
||||
|
||||
const messages = defineMessages({
|
||||
expires: { id: 'expiration.title', defaultMessage: 'Add status expiration' },
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
hasScheduledAt: !!state.getIn(['compose', 'scheduled_at']),
|
||||
active: !!state.getIn(['compose', 'expires_at']) || state.getIn(['popover', 'popoverType']) === POPOVER_STATUS_EXPIRATION_OPTIONS,
|
||||
isPro: state.getIn(['accounts', me, 'is_pro']),
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onOpenExpirationPopover(targetRef) {
|
||||
dispatch(openPopover(POPOVER_STATUS_EXPIRATION_OPTIONS, {
|
||||
targetRef,
|
||||
}))
|
||||
},
|
||||
onOpenProUpgradeModal() {
|
||||
dispatch(openModal('PRO_UPGRADE'))
|
||||
},
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
class ExpiresPostButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isPro: PropTypes.bool,
|
||||
hasScheduledAt: PropTypes.bool,
|
||||
onOpenProUpgradeModal: PropTypes.func.isRequired,
|
||||
onOpenExpirationPopover: PropTypes.func.isRequired,
|
||||
small: PropTypes.bool,
|
||||
}
|
||||
|
||||
handleToggle = () => {
|
||||
if (!this.props.isPro) {
|
||||
this.props.onOpenProUpgradeModal()
|
||||
} else {
|
||||
this.props.onOpenExpirationPopover(this.button)
|
||||
}
|
||||
}
|
||||
|
||||
setButton = (n) => {
|
||||
this.button = n
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
active,
|
||||
intl,
|
||||
hasScheduledAt,
|
||||
small,
|
||||
} = this.props
|
||||
|
||||
if (hasScheduledAt) return null
|
||||
|
||||
return (
|
||||
<ComposeExtraButton
|
||||
active={active}
|
||||
buttonRef={this.setButton}
|
||||
icon='stopwatch'
|
||||
onClick={this.handleToggle}
|
||||
small={small}
|
||||
title={intl.formatMessage(messages.expires)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ const messages = defineMessages({
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
hasExpiresAt: !!state.getIn(['compose', 'expires_at']),
|
||||
active: !!state.getIn(['compose', 'scheduled_at']) || state.getIn(['popover', 'popoverType']) === 'DATE_PICKER',
|
||||
isPro: state.getIn(['accounts', me, 'is_pro']),
|
||||
})
|
||||
@ -34,12 +35,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
class SchedulePostDropdown extends PureComponent {
|
||||
class SchedulePostButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isPro: PropTypes.bool,
|
||||
hasExpiresAt: PropTypes.bool,
|
||||
onOpenProUpgradeModal: PropTypes.func.isRequired,
|
||||
onOpenDatePickerPopover: PropTypes.func.isRequired,
|
||||
onCloseDatePickerPopover: PropTypes.func.isRequired,
|
||||
@ -62,9 +64,12 @@ class SchedulePostDropdown extends PureComponent {
|
||||
const {
|
||||
active,
|
||||
intl,
|
||||
hasExpiresAt,
|
||||
small,
|
||||
} = this.props
|
||||
|
||||
if (hasExpiresAt) return null
|
||||
|
||||
return (
|
||||
<ComposeExtraButton
|
||||
active={active}
|
||||
|
@ -71,6 +71,7 @@ export function Status() { return import(/* webpackChunkName: "components/status
|
||||
export function StatusFeature() { return import(/* webpackChunkName: "features/status" */'../../status') }
|
||||
export function SearchPopover() { return import(/* webpackChunkName: "components/search_popover" */'../../../components/popover/search_popover') }
|
||||
export function SidebarMorePopover() { return import(/* webpackChunkName: "components/sidebar_more_popover" */'../../../components/popover/sidebar_more_popover') }
|
||||
export function StatusExpirationOptionsPopover() { return import(/* webpackChunkName: "components/status_expiration_options_popover" */'../../../components/popover/status_expiration_options_popover') }
|
||||
export function StatusLikes() { return import(/* webpackChunkName: "features/status_likes" */'../../status_likes') }
|
||||
export function StatusOptionsPopover() { return import(/* webpackChunkName: "components/status_options_popover" */'../../../components/popover/status_options_popover') }
|
||||
export function StatusReposts() { return import(/* webpackChunkName: "features/status_reposts" */'../../status_reposts') }
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
COMPOSE_POLL_OPTION_REMOVE,
|
||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
COMPOSE_SCHEDULED_AT_CHANGE,
|
||||
COMPOSE_EXPIRES_AT_CHANGE,
|
||||
COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
|
||||
COMPOSE_CLEAR,
|
||||
} from '../actions/compose';
|
||||
@ -77,6 +78,7 @@ const initialState = ImmutableMap({
|
||||
idempotencyKey: null,
|
||||
tagHistory: ImmutableList(),
|
||||
scheduled_at: null,
|
||||
expires_at: null,
|
||||
rte_controls_visible: false,
|
||||
gif: null,
|
||||
});
|
||||
@ -114,6 +116,7 @@ function clearAll(state) {
|
||||
map.set('poll', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('scheduled_at', null);
|
||||
map.set('expires_at', null);
|
||||
map.set('rte_controls_visible', false);
|
||||
map.set('gif', false);
|
||||
});
|
||||
@ -309,6 +312,7 @@ export default function compose(state = initialState, action) {
|
||||
map.set('poll', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('scheduled_at', null);
|
||||
map.set('expires_at', null);
|
||||
map.set('rte_controls_visible', false);
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
@ -404,6 +408,8 @@ export default function compose(state = initialState, action) {
|
||||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||
case COMPOSE_SCHEDULED_AT_CHANGE:
|
||||
return state.set('scheduled_at', action.date);
|
||||
case COMPOSE_EXPIRES_AT_CHANGE:
|
||||
return state.set('expires_at', action.value);
|
||||
case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY:
|
||||
return state.withMutations(map => {
|
||||
map.set('rte_controls_visible', action.open || !state.get('rte_controls_visible'));
|
||||
|
@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
attributes :id, :created_at, :revised_at, :in_reply_to_id, :in_reply_to_account_id,
|
||||
:sensitive, :spoiler_text, :visibility, :language,
|
||||
:uri, :url, :replies_count, :reblogs_count,
|
||||
:favourites_count, :quote_of_id
|
||||
:favourites_count, :quote_of_id, :expires_at
|
||||
|
||||
attribute :favourited, if: :current_user?
|
||||
attribute :reblogged, if: :current_user?
|
||||
|
@ -16,6 +16,7 @@ class PostStatusService < BaseService
|
||||
# @option [String] :spoiler_text
|
||||
# @option [String] :language
|
||||
# @option [String] :scheduled_at
|
||||
# @option [String] :expires_at
|
||||
# @option [Hash] :poll Optional poll to attach
|
||||
# @option [Enumerable] :media_ids Optional array of media IDs to attach
|
||||
# @option [Doorkeeper::Application] :application
|
||||
@ -55,6 +56,7 @@ class PostStatusService < BaseService
|
||||
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
|
||||
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
|
||||
@visibility = :unlisted if @visibility == :public && @account.silenced?
|
||||
@expires_at = @options[:expires_at]&.to_datetime if @account.is_pro
|
||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||
@scheduled_at = nil if scheduled_in_the_past?
|
||||
rescue ArgumentError
|
||||
@ -98,6 +100,7 @@ class PostStatusService < BaseService
|
||||
DistributionWorker.perform_async(@status.id)
|
||||
# Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
|
||||
# ActivityPub::DistributionWorker.perform_async(@status.id)
|
||||
ExpiringStatusWorker.perform_at(@status.expires_at, @status.id) if @status.expires_at && @account.is_pro
|
||||
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
|
||||
end
|
||||
|
||||
@ -177,6 +180,7 @@ class PostStatusService < BaseService
|
||||
{
|
||||
text: @text,
|
||||
markdown: @markdown,
|
||||
expires_at: @expires_at,
|
||||
group_id: @options[:group_id],
|
||||
quote_of_id: @options[:quote_of_id],
|
||||
media_attachments: @media || [],
|
||||
|
14
app/workers/expiring_status_worker.rb
Normal file
14
app/workers/expiring_status_worker.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ExpiringStatusWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options unique: :until_executed
|
||||
|
||||
def perform(status_id)
|
||||
status = Status.find(status_id)
|
||||
RemovalWorker.perform_async(status.id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user