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],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
visibility: status_params[:visibility],
|
visibility: status_params[:visibility],
|
||||||
scheduled_at: status_params[:scheduled_at],
|
scheduled_at: status_params[:scheduled_at],
|
||||||
|
expires_at: status_params[:expires_at],
|
||||||
application: doorkeeper_token.application,
|
application: doorkeeper_token.application,
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
idempotency: request.headers['Idempotency-Key'],
|
||||||
@ -79,6 +80,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
text: status_params[:status],
|
text: status_params[:status],
|
||||||
markdown: markdown,
|
markdown: markdown,
|
||||||
media_ids: status_params[:media_ids],
|
media_ids: status_params[:media_ids],
|
||||||
|
expires_at: status_params[:expires_at],
|
||||||
sensitive: status_params[:sensitive],
|
sensitive: status_params[:sensitive],
|
||||||
spoiler_text: status_params[:spoiler_text],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
visibility: status_params[:visibility],
|
visibility: status_params[:visibility],
|
||||||
@ -117,6 +119,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
:spoiler_text,
|
:spoiler_text,
|
||||||
:visibility,
|
:visibility,
|
||||||
:scheduled_at,
|
:scheduled_at,
|
||||||
|
:expires_at,
|
||||||
:group_id,
|
:group_id,
|
||||||
media_ids: [],
|
media_ids: [],
|
||||||
poll: [
|
poll: [
|
||||||
|
@ -15,6 +15,14 @@ import { updateTimeline, dequeueTimeline } from './timelines';
|
|||||||
// import { showAlert, showAlertForError } from './alerts';
|
// import { showAlert, showAlertForError } from './alerts';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
import { openModal, closeModal } from './modal';
|
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 { me } from '../initial_state';
|
||||||
import { makeGetStatus } from '../selectors'
|
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_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_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY = 'COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY'
|
||||||
|
|
||||||
export const COMPOSE_CLEAR = 'COMPOSE_CLEAR'
|
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);
|
let scheduled_at = getState().getIn(['compose', 'scheduled_at'], null);
|
||||||
if (scheduled_at !== null) scheduled_at = moment.utc(scheduled_at).toDate();
|
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) {
|
if (isMobile(window.innerWidth) && router && isStandalone) {
|
||||||
router.history.goBack()
|
router.history.goBack()
|
||||||
}
|
}
|
||||||
@ -306,6 +334,7 @@ export function submitCompose(groupId, replyToId = null, router, isStandalone, a
|
|||||||
api(getState)[method](endpoint, {
|
api(getState)[method](endpoint, {
|
||||||
status,
|
status,
|
||||||
markdown,
|
markdown,
|
||||||
|
expires_at,
|
||||||
scheduled_at,
|
scheduled_at,
|
||||||
autoJoinGroup,
|
autoJoinGroup,
|
||||||
in_reply_to_id: inReplyToId,
|
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) {
|
export function changeRichTextEditorControlsVisibility(open) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
|
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 ShopIcon from '../assets/shop_icon'
|
||||||
import SortIcon from '../assets/sort_icon'
|
import SortIcon from '../assets/sort_icon'
|
||||||
import StarIcon from '../assets/star_icon'
|
import StarIcon from '../assets/star_icon'
|
||||||
|
import StopWatchIcon from '../assets/stopwatch_icon'
|
||||||
import StrikethroughIcon from '../assets/strikethrough_icon'
|
import StrikethroughIcon from '../assets/strikethrough_icon'
|
||||||
import SubtractIcon from '../assets/subtract_icon'
|
import SubtractIcon from '../assets/subtract_icon'
|
||||||
import TextSizeIcon from '../assets/text_size_icon'
|
import TextSizeIcon from '../assets/text_size_icon'
|
||||||
@ -150,6 +151,7 @@ const ICONS = {
|
|||||||
'shop': ShopIcon,
|
'shop': ShopIcon,
|
||||||
'sort': SortIcon,
|
'sort': SortIcon,
|
||||||
'star': StarIcon,
|
'star': StarIcon,
|
||||||
|
'stopwatch': StopWatchIcon,
|
||||||
'strikethrough': StrikethroughIcon,
|
'strikethrough': StrikethroughIcon,
|
||||||
'subtract': SubtractIcon,
|
'subtract': SubtractIcon,
|
||||||
'text-size': TextSizeIcon,
|
'text-size': TextSizeIcon,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
POPOVER_SEARCH,
|
POPOVER_SEARCH,
|
||||||
POPOVER_SIDEBAR_MORE,
|
POPOVER_SIDEBAR_MORE,
|
||||||
POPOVER_STATUS_OPTIONS,
|
POPOVER_STATUS_OPTIONS,
|
||||||
|
POPOVER_STATUS_EXPIRATION_OPTIONS,
|
||||||
POPOVER_STATUS_VISIBILITY,
|
POPOVER_STATUS_VISIBILITY,
|
||||||
POPOVER_USER_INFO,
|
POPOVER_USER_INFO,
|
||||||
POPOVER_VIDEO_STATS,
|
POPOVER_VIDEO_STATS,
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
ProfileOptionsPopover,
|
ProfileOptionsPopover,
|
||||||
SearchPopover,
|
SearchPopover,
|
||||||
SidebarMorePopover,
|
SidebarMorePopover,
|
||||||
|
StatusExpirationOptionsPopover,
|
||||||
StatusOptionsPopover,
|
StatusOptionsPopover,
|
||||||
StatusVisibilityPopover,
|
StatusVisibilityPopover,
|
||||||
UserInfoPopover,
|
UserInfoPopover,
|
||||||
@ -51,6 +53,7 @@ POPOVER_COMPONENTS[POPOVER_PROFILE_OPTIONS] = ProfileOptionsPopover
|
|||||||
POPOVER_COMPONENTS[POPOVER_SEARCH] = SearchPopover
|
POPOVER_COMPONENTS[POPOVER_SEARCH] = SearchPopover
|
||||||
POPOVER_COMPONENTS[POPOVER_SIDEBAR_MORE] = SidebarMorePopover
|
POPOVER_COMPONENTS[POPOVER_SIDEBAR_MORE] = SidebarMorePopover
|
||||||
POPOVER_COMPONENTS[POPOVER_STATUS_OPTIONS] = StatusOptionsPopover
|
POPOVER_COMPONENTS[POPOVER_STATUS_OPTIONS] = StatusOptionsPopover
|
||||||
|
POPOVER_COMPONENTS[POPOVER_STATUS_EXPIRATION_OPTIONS] = StatusExpirationOptionsPopover
|
||||||
POPOVER_COMPONENTS[POPOVER_STATUS_VISIBILITY] = StatusVisibilityPopover
|
POPOVER_COMPONENTS[POPOVER_STATUS_VISIBILITY] = StatusVisibilityPopover
|
||||||
POPOVER_COMPONENTS[POPOVER_USER_INFO] = UserInfoPopover
|
POPOVER_COMPONENTS[POPOVER_USER_INFO] = UserInfoPopover
|
||||||
POPOVER_COMPONENTS[POPOVER_VIDEO_STATS] = VideoStatsPopover
|
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 ImmutablePropTypes from 'react-immutable-proptypes'
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
|
import moment from 'moment-mini'
|
||||||
import { openPopover } from '../actions/popover'
|
import { openPopover } from '../actions/popover'
|
||||||
import { openModal } from '../actions/modal'
|
import { openModal } from '../actions/modal'
|
||||||
import { me } from '../initial_state'
|
import { me } from '../initial_state'
|
||||||
@ -17,6 +18,7 @@ import Avatar from './avatar'
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
edited: { id: 'status.edited', defaultMessage: 'Edited' },
|
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_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for anyone on or off Gab' },
|
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for anyone on or off Gab' },
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
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)}`
|
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 (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
<div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}>
|
<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(' ')} />
|
<Icon id={visibilityIcon} size='12px' className={[_s.default, _s.fillSecondary].join(' ')} />
|
||||||
</span>
|
</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') &&
|
!!status.get('group') &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -30,6 +30,7 @@ export const POPOVER_PROFILE_OPTIONS = 'PROFILE_OPTIONS'
|
|||||||
export const POPOVER_SEARCH = 'SEARCH'
|
export const POPOVER_SEARCH = 'SEARCH'
|
||||||
export const POPOVER_SIDEBAR_MORE = 'SIDEBAR_MORE'
|
export const POPOVER_SIDEBAR_MORE = 'SIDEBAR_MORE'
|
||||||
export const POPOVER_STATUS_OPTIONS = 'STATUS_OPTIONS'
|
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_STATUS_VISIBILITY = 'STATUS_VISIBILITY'
|
||||||
export const POPOVER_USER_INFO = 'USER_INFO'
|
export const POPOVER_USER_INFO = 'USER_INFO'
|
||||||
export const POPOVER_VIDEO_STATS = 'VIDEO_STATS'
|
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 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 PollForm from './poll_form'
|
||||||
import SchedulePostButton from './schedule_post_button'
|
import SchedulePostButton from './schedule_post_button'
|
||||||
import SpoilerButton from './spoiler_button'
|
import SpoilerButton from './spoiler_button'
|
||||||
|
import ExpiresPostButton from './expires_post_button'
|
||||||
import RichTextEditorButton from './rich_text_editor_button'
|
import RichTextEditorButton from './rich_text_editor_button'
|
||||||
import StatusContainer from '../../../containers/status_container'
|
import StatusContainer from '../../../containers/status_container'
|
||||||
import StatusVisibilityButton from './status_visibility_button'
|
import StatusVisibilityButton from './status_visibility_button'
|
||||||
@ -484,10 +485,15 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
<StatusVisibilityButton />
|
<StatusVisibilityButton />
|
||||||
<SpoilerButton />
|
<SpoilerButton />
|
||||||
{
|
{
|
||||||
!hidePro &&
|
!hidePro && !edit &&
|
||||||
<SchedulePostButton />
|
<SchedulePostButton />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!hidePro && !edit &&
|
||||||
|
<ExpiresPostButton />
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!hidePro &&
|
!hidePro &&
|
||||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
<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) => ({
|
const mapStateToProps = (state) => ({
|
||||||
|
hasExpiresAt: !!state.getIn(['compose', 'expires_at']),
|
||||||
active: !!state.getIn(['compose', 'scheduled_at']) || state.getIn(['popover', 'popoverType']) === 'DATE_PICKER',
|
active: !!state.getIn(['compose', 'scheduled_at']) || state.getIn(['popover', 'popoverType']) === 'DATE_PICKER',
|
||||||
isPro: state.getIn(['accounts', me, 'is_pro']),
|
isPro: state.getIn(['accounts', me, 'is_pro']),
|
||||||
})
|
})
|
||||||
@ -34,12 +35,13 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export default
|
export default
|
||||||
@injectIntl
|
@injectIntl
|
||||||
@connect(mapStateToProps, mapDispatchToProps)
|
@connect(mapStateToProps, mapDispatchToProps)
|
||||||
class SchedulePostDropdown extends PureComponent {
|
class SchedulePostButton extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
active: PropTypes.bool.isRequired,
|
active: PropTypes.bool.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
isPro: PropTypes.bool,
|
isPro: PropTypes.bool,
|
||||||
|
hasExpiresAt: PropTypes.bool,
|
||||||
onOpenProUpgradeModal: PropTypes.func.isRequired,
|
onOpenProUpgradeModal: PropTypes.func.isRequired,
|
||||||
onOpenDatePickerPopover: PropTypes.func.isRequired,
|
onOpenDatePickerPopover: PropTypes.func.isRequired,
|
||||||
onCloseDatePickerPopover: PropTypes.func.isRequired,
|
onCloseDatePickerPopover: PropTypes.func.isRequired,
|
||||||
@ -62,9 +64,12 @@ class SchedulePostDropdown extends PureComponent {
|
|||||||
const {
|
const {
|
||||||
active,
|
active,
|
||||||
intl,
|
intl,
|
||||||
|
hasExpiresAt,
|
||||||
small,
|
small,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
|
if (hasExpiresAt) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposeExtraButton
|
<ComposeExtraButton
|
||||||
active={active}
|
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 StatusFeature() { return import(/* webpackChunkName: "features/status" */'../../status') }
|
||||||
export function SearchPopover() { return import(/* webpackChunkName: "components/search_popover" */'../../../components/popover/search_popover') }
|
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 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 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 StatusOptionsPopover() { return import(/* webpackChunkName: "components/status_options_popover" */'../../../components/popover/status_options_popover') }
|
||||||
export function StatusReposts() { return import(/* webpackChunkName: "features/status_reposts" */'../../status_reposts') }
|
export function StatusReposts() { return import(/* webpackChunkName: "features/status_reposts" */'../../status_reposts') }
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
COMPOSE_POLL_OPTION_REMOVE,
|
COMPOSE_POLL_OPTION_REMOVE,
|
||||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||||
COMPOSE_SCHEDULED_AT_CHANGE,
|
COMPOSE_SCHEDULED_AT_CHANGE,
|
||||||
|
COMPOSE_EXPIRES_AT_CHANGE,
|
||||||
COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
|
COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
|
||||||
COMPOSE_CLEAR,
|
COMPOSE_CLEAR,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
@ -77,6 +78,7 @@ const initialState = ImmutableMap({
|
|||||||
idempotencyKey: null,
|
idempotencyKey: null,
|
||||||
tagHistory: ImmutableList(),
|
tagHistory: ImmutableList(),
|
||||||
scheduled_at: null,
|
scheduled_at: null,
|
||||||
|
expires_at: null,
|
||||||
rte_controls_visible: false,
|
rte_controls_visible: false,
|
||||||
gif: null,
|
gif: null,
|
||||||
});
|
});
|
||||||
@ -114,6 +116,7 @@ function clearAll(state) {
|
|||||||
map.set('poll', null);
|
map.set('poll', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
map.set('scheduled_at', null);
|
map.set('scheduled_at', null);
|
||||||
|
map.set('expires_at', null);
|
||||||
map.set('rte_controls_visible', false);
|
map.set('rte_controls_visible', false);
|
||||||
map.set('gif', false);
|
map.set('gif', false);
|
||||||
});
|
});
|
||||||
@ -309,6 +312,7 @@ export default function compose(state = initialState, action) {
|
|||||||
map.set('poll', null);
|
map.set('poll', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
map.set('scheduled_at', null);
|
map.set('scheduled_at', null);
|
||||||
|
map.set('expires_at', null);
|
||||||
map.set('rte_controls_visible', false);
|
map.set('rte_controls_visible', false);
|
||||||
});
|
});
|
||||||
case COMPOSE_SUBMIT_REQUEST:
|
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));
|
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||||
case COMPOSE_SCHEDULED_AT_CHANGE:
|
case COMPOSE_SCHEDULED_AT_CHANGE:
|
||||||
return state.set('scheduled_at', action.date);
|
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:
|
case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('rte_controls_visible', action.open || !state.get('rte_controls_visible'));
|
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,
|
attributes :id, :created_at, :revised_at, :in_reply_to_id, :in_reply_to_account_id,
|
||||||
:sensitive, :spoiler_text, :visibility, :language,
|
:sensitive, :spoiler_text, :visibility, :language,
|
||||||
:uri, :url, :replies_count, :reblogs_count,
|
:uri, :url, :replies_count, :reblogs_count,
|
||||||
:favourites_count, :quote_of_id
|
:favourites_count, :quote_of_id, :expires_at
|
||||||
|
|
||||||
attribute :favourited, if: :current_user?
|
attribute :favourited, if: :current_user?
|
||||||
attribute :reblogged, if: :current_user?
|
attribute :reblogged, if: :current_user?
|
||||||
|
@ -16,6 +16,7 @@ class PostStatusService < BaseService
|
|||||||
# @option [String] :spoiler_text
|
# @option [String] :spoiler_text
|
||||||
# @option [String] :language
|
# @option [String] :language
|
||||||
# @option [String] :scheduled_at
|
# @option [String] :scheduled_at
|
||||||
|
# @option [String] :expires_at
|
||||||
# @option [Hash] :poll Optional poll to attach
|
# @option [Hash] :poll Optional poll to attach
|
||||||
# @option [Enumerable] :media_ids Optional array of media IDs to attach
|
# @option [Enumerable] :media_ids Optional array of media IDs to attach
|
||||||
# @option [Doorkeeper::Application] :application
|
# @option [Doorkeeper::Application] :application
|
||||||
@ -55,6 +56,7 @@ class PostStatusService < BaseService
|
|||||||
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
|
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
|
||||||
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
|
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
|
||||||
@visibility = :unlisted if @visibility == :public && @account.silenced?
|
@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 = @options[:scheduled_at]&.to_datetime
|
||||||
@scheduled_at = nil if scheduled_in_the_past?
|
@scheduled_at = nil if scheduled_in_the_past?
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
@ -98,6 +100,7 @@ class PostStatusService < BaseService
|
|||||||
DistributionWorker.perform_async(@status.id)
|
DistributionWorker.perform_async(@status.id)
|
||||||
# Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
|
# Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
|
||||||
# ActivityPub::DistributionWorker.perform_async(@status.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
|
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -177,6 +180,7 @@ class PostStatusService < BaseService
|
|||||||
{
|
{
|
||||||
text: @text,
|
text: @text,
|
||||||
markdown: @markdown,
|
markdown: @markdown,
|
||||||
|
expires_at: @expires_at,
|
||||||
group_id: @options[:group_id],
|
group_id: @options[:group_id],
|
||||||
quote_of_id: @options[:quote_of_id],
|
quote_of_id: @options[:quote_of_id],
|
||||||
media_attachments: @media || [],
|
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…
x
Reference in New Issue
Block a user