diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index f301666db..624a9db68 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -39,11 +39,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def permitted_account_statuses - @account.statuses.permitted_for(@account, current_account) + @account.permitted_statuses(current_account) end def only_media_scope - Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) + Status.include_expired.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) end def pinned_scope @@ -53,18 +53,18 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def no_replies_scope - Status.without_replies + Status.include_expired.without_replies end def no_reblogs_scope - Status.without_reblogs + Status.include_expired.without_reblogs end def hashtag_scope tag = Tag.find_normalized(params[:tagged]) if tag - Status.tagged_with(tag.id) + Status.include_expired.tagged_with(tag.id) else Status.none end diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index f666a1d8d..9e5711c67 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -18,11 +18,11 @@ class Api::V1::BookmarksController < Api::BaseController end def cached_bookmarks - cache_collection(results.map(&:status), Status) + cache_collection(Status.where(id: results.pluck(:status_id)), Status) end def results - @_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id( + @_results ||= account_bookmarks.joins('INNER JOIN statuses ON statuses.deleted_at IS NULL AND statuses.id = bookmarks.status_id').to_a_paginated_by_id( limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id) ) diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 651a057d5..52f37555d 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -18,11 +18,11 @@ class Api::V1::FavouritesController < Api::BaseController end def cached_favourites - cache_collection(results.map(&:status), Status) + cache_collection(Status.where(id: results.pluck(:status_id)), Status) end def results - @_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id( + @_results ||= account_favourites.joins('INNER JOIN statuses ON statuses.deleted_at IS NULL AND statuses.id = favourites.status_id').to_a_paginated_by_id( limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id) ) diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb index 19963c002..523e8c025 100644 --- a/app/controllers/api/v1/statuses/bookmarks_controller.rb +++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb @@ -32,7 +32,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController private def set_status - @status = Status.find(params[:status_id]) + @status = Status.include_expired(current_account).find(params[:status_id]) authorize @status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 2b614a837..a7b8dedb2 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -65,7 +65,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController end def set_status - @status = Status.find(params[:status_id]) + @status = Status.include_expired(current_account).find(params[:status_id]) authorize @status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index 2e21ce6a0..c6e74cd8e 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -31,7 +31,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController private def set_status - @status = Status.find(params[:status_id]) + @status = Status.include_expired(current_account).find(params[:status_id]) authorize @status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 24db30fcc..72ba53887 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -61,7 +61,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def set_status - @status = Status.find(params[:status_id]) + @status = Status.include_expired(current_account).find(params[:status_id]) authorize @status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index d8f4db42f..309d86e2a 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -44,6 +44,8 @@ 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], + expires_action: status_params[:expires_action], application: doorkeeper_token.application, poll: status_params[:poll], idempotency: request.headers['Idempotency-Key'], @@ -54,7 +56,7 @@ class Api::V1::StatusesController < Api::BaseController end def destroy - @status = Status.where(account_id: current_user.account).find(params[:id]) + @status = Status.include_expired.where(account_id: current_account.id).find(params[:id]) authorize @status, :destroy? @status.discard @@ -67,7 +69,7 @@ class Api::V1::StatusesController < Api::BaseController private def set_status - @status = Status.find(params[:id]) + @status = Status.include_expired(current_account).find(params[:id]) authorize @status, :show? rescue Mastodon::NotPermittedError not_found @@ -88,6 +90,8 @@ class Api::V1::StatusesController < Api::BaseController :visibility, :scheduled_at, :quote_id, + :expires_at, + :expires_action, media_ids: [], poll: [ :multiple, diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 05e431b19..56dd7b728 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -40,7 +40,7 @@ module CacheConcern klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) unless uncached_ids.empty? - uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) + uncached = klass.unscoped.where(id: uncached_ids).with_includes.index_by(&:id) uncached.each_value do |item| Rails.cache.write(item, item) diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index b7c85482d..a62fb2227 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -84,6 +84,15 @@ const messages = defineMessages({ direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); +const dateFormatOptions = { + hour12: false, + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', +}; + export default @connect(mapStateToProps) @injectIntl class Status extends ImmutablePureComponent { @@ -654,19 +663,22 @@ class Status extends ImmutablePureComponent { ); } + const expires_at = status.get('expires_at') + const expires_date = expires_at && new Date(expires_at) + const expired = expires_date && expires_date.getTime() < intl.now() + return ( -
+
{prepend} -
+
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 2f5ccd583..2a9b62c09 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -60,6 +60,7 @@ class StatusActionBar extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + expired: PropTypes.bool, relationship: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, @@ -84,6 +85,10 @@ class StatusActionBar extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; + static defaultProps = { + expired: false, + }; + // Avoid checking props that are functions (and whose equality will always // evaluate to false. See react-immutable-pure-component for usage. updateOnProps = [ @@ -236,7 +241,7 @@ class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, relationship, intl, withDismiss, scrollKey } = this.props; + const { status, relationship, intl, withDismiss, scrollKey, expired } = this.props; const anonymousAccess = !me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -248,20 +253,20 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); - if (publicStatus) { + if (publicStatus && !expired) { menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } menu.push(null); - if (writtenByMe && publicStatus) { + if (writtenByMe && publicStatus && !expired) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); } menu.push(null); - if (writtenByMe || withDismiss) { + if ((writtenByMe || withDismiss) && !expired) { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push(null); } @@ -332,17 +337,17 @@ class StatusActionBar extends ImmutablePureComponent { } const shareButton = ('share' in navigator) && publicStatus && ( - + ); return (
- - - - + + + + {shareButton} - +
+
); let replyIcon; @@ -290,10 +296,10 @@ class ActionBar extends React.PureComponent { return (
-
-
+
+
-
+
{shareButton}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index a55b5a120..eb73c2a4a 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -48,6 +48,15 @@ const mapStateToProps = (state, props) => { }; }; +const dateFormatOptions = { + hour12: false, + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', +}; + export default @connect(mapStateToProps) @injectIntl class DetailedStatus extends ImmutablePureComponent { @@ -376,9 +385,13 @@ class DetailedStatus extends ImmutablePureComponent { ); } + const expires_at = status.get('expires_at'); + const expires_date = expires_at && new Date(expires_at); + const expired = expires_date && expires_date.getTime() < intl.now(); + return (
-
+
@@ -392,7 +405,15 @@ class DetailedStatus extends ImmutablePureComponent {
- {visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} + + {status.get('expires_at') && + +