diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 96930bb4..ce965466 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -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: [
diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js
index ac562ac7..7870005a 100644
--- a/app/javascript/gabsocial/actions/compose.js
+++ b/app/javascript/gabsocial/actions/compose.js
@@ -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,
diff --git a/app/javascript/gabsocial/assets/stopwatch_icon.js b/app/javascript/gabsocial/assets/stopwatch_icon.js
new file mode 100644
index 00000000..87d2f51f
--- /dev/null
+++ b/app/javascript/gabsocial/assets/stopwatch_icon.js
@@ -0,0 +1,25 @@
+const StopwatchIcon = ({
+ className = '',
+ size = '16px',
+ title = '',
+}) => (
+
+)
+
+export default StopwatchIcon
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/icon.js b/app/javascript/gabsocial/components/icon.js
index 80a2c522..fa0dff93 100644
--- a/app/javascript/gabsocial/components/icon.js
+++ b/app/javascript/gabsocial/components/icon.js
@@ -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,
diff --git a/app/javascript/gabsocial/components/popover/popover_root.js b/app/javascript/gabsocial/components/popover/popover_root.js
index 301a9201..08d8de46 100644
--- a/app/javascript/gabsocial/components/popover/popover_root.js
+++ b/app/javascript/gabsocial/components/popover/popover_root.js
@@ -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
diff --git a/app/javascript/gabsocial/components/popover/status_expiration_options_popover.js b/app/javascript/gabsocial/components/popover/status_expiration_options_popover.js
new file mode 100644
index 00000000..1a1b82c9
--- /dev/null
+++ b/app/javascript/gabsocial/components/popover/status_expiration_options_popover.js
@@ -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 (
+
+