add self-destructing roars & `live`/`lifespan` bangtags
parent
2a6ccce070
commit
3862f48c34
|
@ -52,6 +52,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
delete_after: status_params[:delete_after],
|
||||
sharekey: status_params[:sharekey],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
|
@ -92,6 +93,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
:visibility,
|
||||
:sharekey,
|
||||
:scheduled_at,
|
||||
:delete_after,
|
||||
:content_type,
|
||||
media_ids: [],
|
||||
poll: [
|
||||
|
|
|
@ -53,6 +53,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_hide_public_profile,
|
||||
:setting_hide_public_outbox,
|
||||
:setting_max_public_history,
|
||||
:setting_roar_lifespan,
|
||||
|
||||
:setting_default_privacy,
|
||||
:setting_default_sensitive,
|
||||
|
|
|
@ -59,6 +59,9 @@ export default class StatusIcons extends React.PureComponent {
|
|||
aria-hidden='true'
|
||||
/>
|
||||
) : null}
|
||||
{status.get('delete_after') ? (
|
||||
<i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} aria-hidden='true' />
|
||||
) : null}
|
||||
{(
|
||||
<VisibilityIcon visibility={status.get('visibility')} />
|
||||
)}
|
||||
|
|
|
@ -17,6 +17,15 @@ import classNames from 'classnames';
|
|||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const dateFormatOptions = {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
export default class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -119,6 +128,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
let reblogIcon = 'repeat';
|
||||
let favouriteLink = '';
|
||||
let sharekeyLinks = '';
|
||||
let destructIcon = '';
|
||||
|
||||
if (this.props.measureHeight) {
|
||||
outerStyle.height = `${this.state.height}px`;
|
||||
|
@ -233,6 +243,14 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
if (status.get('delete_after')) {
|
||||
destructIcon = (
|
||||
<span>
|
||||
<i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} /> ·
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={outerStyle}>
|
||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })} data-status-by={status.getIn(['account', 'acct'])}>
|
||||
|
@ -254,7 +272,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
/>
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
{sharekeyLinks} {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
|
||||
{sharekeyLinks} {reblogLink} · {favouriteLink} · {destructIcon} <VisibilityIcon visibility={status.get('visibility')} />
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>
|
||||
|
|
|
@ -22,6 +22,22 @@ class Bangtags
|
|||
['media', 'stop'] => ['var', 'end'],
|
||||
['media', 'endall'] => ['var', 'endall'],
|
||||
['media', 'stopall'] => ['var', 'endall'],
|
||||
|
||||
['admin', 'end'] => ['var', 'end'],
|
||||
['admin', 'stop'] => ['var', 'end'],
|
||||
['admin', 'endall'] => ['var', 'endall'],
|
||||
['admin', 'stopall'] => ['var', 'endall'],
|
||||
|
||||
['parent', 'visibility'] => ['visibility', 'parent'],
|
||||
['parent', 'v'] => ['visibility', 'parent'],
|
||||
|
||||
['parent', 'live'] => ['live', 'parent'],
|
||||
['parent', 'lifespan'] => ['lifespan', 'parent'],
|
||||
['parent', 'delete_in'] => ['delete_in', 'parent'],
|
||||
|
||||
['all', 'live'] => ['live', 'all'],
|
||||
['all', 'lifespan'] => ['lifespan', 'all'],
|
||||
['all', 'delete_in'] => ['delete_in', 'all'],
|
||||
}
|
||||
|
||||
# sections of the final status text
|
||||
|
@ -525,6 +541,45 @@ class Bangtags
|
|||
status.local_only = true
|
||||
end
|
||||
end
|
||||
when 'live', 'lifespan', 'l', 'delete_in'
|
||||
chunk = nil
|
||||
next if cmd[1].nil?
|
||||
case cmd[1].downcase
|
||||
when 'parent'
|
||||
next unless @parent_status.present? && @parent_status.account_id == @account.id
|
||||
s = @parent_status
|
||||
i = cmd[2].to_i
|
||||
unit = cmd[3].present? ? cmd[3].downcase : 'minutes'
|
||||
when 'all'
|
||||
s = :all
|
||||
i = cmd[2].to_i
|
||||
unit = cmd[3].present? ? cmd[3].downcase : 'minutes'
|
||||
else
|
||||
s = @status
|
||||
i = cmd[1].to_i
|
||||
unit = cmd[2].present? ? cmd[2].downcase : 'minutes'
|
||||
end
|
||||
delete_after = case unit
|
||||
when 's', 'second', 'seconds'
|
||||
[60, i].max.seconds
|
||||
when 'm', 'minute', 'minutes'
|
||||
i.minutes
|
||||
when 'h', 'hour', 'hours'
|
||||
i.hours
|
||||
when 'd', 'day', 'days'
|
||||
i.days
|
||||
when 'w', 'week', 'weeks'
|
||||
i.weeks
|
||||
when 'm', 'month', 'months'
|
||||
i.months
|
||||
when 'y', 'year', 'years'
|
||||
i.years
|
||||
end
|
||||
if s == :all
|
||||
@account.statuses.find_each { |s| s.delete_after = delete_after }
|
||||
else
|
||||
s.delete_after = delete_after
|
||||
end
|
||||
when 'keysmash'
|
||||
keyboard = [
|
||||
'asdf', 'jkl;',
|
||||
|
|
|
@ -37,6 +37,7 @@ class UserSettingsDecorator
|
|||
user.settings['hide_public_outbox'] = hide_public_outbox_preference if change?('setting_hide_public_outbox')
|
||||
user.settings['larger_emoji'] = larger_emoji_preference if change?('setting_larger_emoji')
|
||||
user.settings['max_public_history'] = max_public_history_preference if change?('setting_max_public_history')
|
||||
user.settings['roar_lifespan'] = roar_lifespan_preference if change?('setting_roar_lifespan')
|
||||
|
||||
user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails')
|
||||
user.settings['interactions'] = merged_interactions if change?('interactions')
|
||||
|
@ -130,6 +131,10 @@ class UserSettingsDecorator
|
|||
settings['setting_max_public_history']
|
||||
end
|
||||
|
||||
def roar_lifespan_preference
|
||||
settings['setting_roar_lifespan']
|
||||
end
|
||||
|
||||
def merged_notification_emails
|
||||
user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h
|
||||
end
|
||||
|
|
|
@ -133,6 +133,7 @@ class Account < ApplicationRecord
|
|||
:defaults_to_local_only?,
|
||||
:always_local_only?,
|
||||
:max_public_history,
|
||||
:roar_lifespan,
|
||||
|
||||
:hides_public_profile?,
|
||||
:hides_public_outbox?,
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: destructing_statuses
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_id :bigint(8)
|
||||
# delete_after :datetime
|
||||
#
|
||||
|
||||
class DestructingStatus < ApplicationRecord
|
||||
belongs_to :status, inverse_of: :destructing_status
|
||||
|
||||
validate :validate_future_date
|
||||
|
||||
private
|
||||
|
||||
def validate_future_date
|
||||
errors.add(:delete_after, I18n.t('destructing_statuses.too_soon')) if delete_after.present? && delete_after < Time.now.utc + PostStatusService::MIN_DESTRUCT_OFFSET
|
||||
end
|
||||
end
|
|
@ -79,6 +79,7 @@ class Status < ApplicationRecord
|
|||
has_one :stream_entry, as: :activity, inverse_of: :status
|
||||
has_one :status_stat, inverse_of: :status
|
||||
has_one :poll, inverse_of: :status, dependent: :destroy
|
||||
has_one :destructing_status, inverse_of: :status, dependent: :destroy
|
||||
|
||||
validates :uri, uniqueness: true, presence: true, unless: :local?
|
||||
validates :text, presence: true, unless: -> { with_media? || reblog? }
|
||||
|
@ -266,6 +267,18 @@ class Status < ApplicationRecord
|
|||
@chat_tags = tags.only_chat
|
||||
end
|
||||
|
||||
def delete_after
|
||||
destructing_status&.delete_after
|
||||
end
|
||||
|
||||
def delete_after=(value)
|
||||
if destructing_status.nil?
|
||||
DestructingStatus.create!(status_id: id, delete_after: Time.now.utc + value)
|
||||
else
|
||||
destructing_status.delete_after = Time.now.utc + value
|
||||
end
|
||||
end
|
||||
|
||||
def mark_for_mass_destruction!
|
||||
@marked_for_mass_destruction = true
|
||||
end
|
||||
|
|
|
@ -125,6 +125,7 @@ class User < ApplicationRecord
|
|||
:hide_public_profile,
|
||||
:hide_public_outbox,
|
||||
:max_public_history,
|
||||
:roar_lifespan,
|
||||
|
||||
:auto_play_gif,
|
||||
:default_sensitive,
|
||||
|
@ -299,6 +300,10 @@ class User < ApplicationRecord
|
|||
@_max_public_history ||= (settings.max_public_history || 6)
|
||||
end
|
||||
|
||||
def roar_lifespan
|
||||
@_roar_lifespan ||= (settings.roar_lifespan || 0)
|
||||
end
|
||||
|
||||
def defaults_to_local_only?
|
||||
@defaults_to_local_only ||= (settings.default_local || false)
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
attribute :pinned, if: :pinnable?
|
||||
attribute :local_only if :local?
|
||||
attribute :sharekey, if: :owner?
|
||||
attribute :delete_after, if: :current_user?
|
||||
|
||||
attribute :content, unless: :source_requested?
|
||||
attribute :text, if: :source_requested?
|
||||
|
@ -135,6 +136,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
object.active_mentions.to_a.sort_by(&:id)
|
||||
end
|
||||
|
||||
def delete_after
|
||||
object.delete_after
|
||||
end
|
||||
|
||||
class ApplicationSerializer < ActiveModel::Serializer
|
||||
attributes :name, :website
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ class PostStatusService < BaseService
|
|||
include Redisable
|
||||
|
||||
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
||||
MIN_DESTRUCT_OFFSET = 30.seconds.freeze
|
||||
|
||||
VISIBILITY_RANK = {
|
||||
'public' => 0,
|
||||
'unlisted' => 1,
|
||||
|
@ -28,6 +30,7 @@ class PostStatusService < BaseService
|
|||
# @option [String] :spoiler_text
|
||||
# @option [String] :language
|
||||
# @option [String] :scheduled_at
|
||||
# @option [String] :delete_after
|
||||
# @option [Hash] :poll Optional poll to attach
|
||||
# @option [Enumerable] :media_ids Optional array of media IDs to attach
|
||||
# @option [Doorkeeper::Application] :application
|
||||
|
@ -134,6 +137,19 @@ class PostStatusService < BaseService
|
|||
|
||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||
@scheduled_at = nil if scheduled_in_the_past?
|
||||
|
||||
case @options[:delete_after].class
|
||||
when NilClass
|
||||
@delete_after = @account.user.setting_roar_lifespan.to_i.days
|
||||
when ActiveSupport::Duration
|
||||
@delete_after = @options[:delete_after]
|
||||
when Integer
|
||||
@delete_after = @options[:delete_after].minutes
|
||||
when Float
|
||||
@delete_after = @options[:delete_after].minutes
|
||||
end
|
||||
@delete_after = nil if @delete_after.present? && (@delete_after < MIN_DESTRUCT_OFFSET)
|
||||
|
||||
rescue ArgumentError
|
||||
raise ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
@ -179,6 +195,8 @@ class PostStatusService < BaseService
|
|||
end
|
||||
|
||||
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
|
||||
|
||||
@status.delete_after = @delete_after unless @delete_after.nil?
|
||||
end
|
||||
|
||||
def validate_media!
|
||||
|
@ -250,6 +268,7 @@ class PostStatusService < BaseService
|
|||
spoiler_text: @options[:spoiler_text] || '',
|
||||
visibility: @visibility,
|
||||
local_only: @local_only,
|
||||
delete_after: @delete_after,
|
||||
sharekey: @sharekey,
|
||||
language: language_from_option(@options[:language]) || @account.user_default_language&.presence || 'en',
|
||||
application: @options[:application],
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
.fields-group
|
||||
= f.input :setting_rawr_federated, as: :boolean, wrapper: :with_label
|
||||
|
||||
%hr/
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_hide_network, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_hide_interactions, as: :boolean, wrapper: :with_label
|
||||
|
@ -43,8 +45,11 @@
|
|||
= f.input :setting_show_application, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
|
||||
|
||||
%hr/
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_max_public_history, collection: [1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_max_public_history_#{item}")]) }, selected: current_user.max_public_history.to_i
|
||||
= f.input :setting_max_public_history, collection: [1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.max_public_history.to_i
|
||||
= f.input :setting_roar_lifespan, collection: [0, 1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.roar_lifespan.to_i
|
||||
= f.input :setting_hide_public_profile, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_hide_public_outbox, as: :boolean, wrapper: :with_label
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DestructStatusWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options unique: :until_executed
|
||||
|
||||
def perform(destructing_status_id)
|
||||
destructing_status = DestructingStatus.find(destructing_status_id)
|
||||
destructing_status.destroy!
|
||||
|
||||
RemoveStatusService.new.call(destructing_status.status)
|
||||
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
|
||||
true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Scheduler::DestructingStatusesScheduler
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options unique: :until_executed, retry: 0
|
||||
|
||||
def perform
|
||||
due_statuses.find_each do |destructing_status|
|
||||
DestructStatusWorker.perform_at(destructing_status.delete_after, destructing_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def due_statuses
|
||||
DestructingStatus.where('delete_after <= ?', Time.now.utc + PostStatusService::MIN_DESTRUCT_OFFSET)
|
||||
end
|
||||
end
|
|
@ -858,7 +858,9 @@ en:
|
|||
scheduled_statuses:
|
||||
over_daily_limit: You have exceeded the limit of %{limit} scheduled roars for that day
|
||||
over_total_limit: You have exceeded the limit of %{limit} scheduled roars
|
||||
too_soon: The scheduled date must be in the future
|
||||
too_soon: The scheduled timeframe must be at least 5 minutes into the future
|
||||
destructing_statuses:
|
||||
too_soon: The destruction timeframe must be at least 5 minutes into the future
|
||||
sessions:
|
||||
activity: Last activity
|
||||
browser: Browser
|
||||
|
|
|
@ -118,6 +118,7 @@ en:
|
|||
setting_default_content_type_x_bbcode_markdown: BBdown
|
||||
setting_default_language: Posting language
|
||||
setting_default_privacy: Post privacy
|
||||
setting_roar_lifespan: Auto-delete new roars after
|
||||
setting_default_local: Default to Monsterpit-only roars (in Glitch flavour)
|
||||
setting_always_local: Don't send your roars outside Monsterpit
|
||||
setting_rawr_federated: Show raw world timeline (may contain offensive content!)
|
||||
|
@ -156,19 +157,6 @@ en:
|
|||
setting_hide_public_profile: Hide your public profile from anonymous viewers
|
||||
setting_hide_public_outbox: Hide your public ActivityPub outbox (affects discoverability)
|
||||
setting_max_public_history: Limit history of roars on public profile to
|
||||
setting_max_public_history_1: 1 day
|
||||
setting_max_public_history_3: 3 days
|
||||
setting_max_public_history_6: 6 days
|
||||
setting_max_public_history_7: 1 week
|
||||
setting_max_public_history_14: 2 weeks
|
||||
setting_max_public_history_30: 6 weeks
|
||||
setting_max_public_history_60: 2 months
|
||||
setting_max_public_history_90: 3 months
|
||||
setting_max_public_history_180: 6 months
|
||||
setting_max_public_history_365: 1 year
|
||||
setting_max_public_history_730: 2 years
|
||||
setting_max_public_history_1095: 3 years
|
||||
setting_max_public_history_2190: 6 years
|
||||
setting_noindex: Opt-out of search engine indexing
|
||||
setting_reduce_motion: Reduce motion in animations
|
||||
setting_show_application: Disclose application used to send roars
|
||||
|
@ -180,6 +168,21 @@ en:
|
|||
username: Username
|
||||
username_or_email: Username or Email
|
||||
whole_word: Whole word
|
||||
lifespan:
|
||||
'0': No limit
|
||||
1: 1 day
|
||||
3: 3 days
|
||||
6: 6 days
|
||||
7: 1 week
|
||||
14: 2 weeks
|
||||
30: 6 weeks
|
||||
60: 2 months
|
||||
90: 3 months
|
||||
180: 6 months
|
||||
365: 1 year
|
||||
730: 2 years
|
||||
1095: 3 years
|
||||
2190: 6 years
|
||||
featured_tag:
|
||||
name: Hashtag
|
||||
interactions:
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
scheduled_statuses_scheduler:
|
||||
every: '5m'
|
||||
class: Scheduler::ScheduledStatusesScheduler
|
||||
destructing_statuses_scheduler:
|
||||
every: '1m'
|
||||
class: Scheduler::DestructingStatusesScheduler
|
||||
media_cleanup_scheduler:
|
||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||
class: Scheduler::MediaCleanupScheduler
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class CreateDestructingStatuses < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :destructing_statuses do |t|
|
||||
t.references :status, foreign_key: true
|
||||
t.datetime :delete_after
|
||||
end
|
||||
add_index :destructing_statuses, :delete_after
|
||||
end
|
||||
end
|
10
db/schema.rb
10
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_07_22_014444) do
|
||||
ActiveRecord::Schema.define(version: 2019_07_23_152514) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -266,6 +266,13 @@ ActiveRecord::Schema.define(version: 2019_07_22_014444) do
|
|||
t.index ["account_id"], name: "index_custom_filters_on_account_id"
|
||||
end
|
||||
|
||||
create_table "destructing_statuses", force: :cascade do |t|
|
||||
t.bigint "status_id"
|
||||
t.datetime "delete_after"
|
||||
t.index ["delete_after"], name: "index_destructing_statuses_on_delete_after"
|
||||
t.index ["status_id"], name: "index_destructing_statuses_on_status_id"
|
||||
end
|
||||
|
||||
create_table "domain_blocks", force: :cascade do |t|
|
||||
t.string "domain", default: "", null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
@ -817,6 +824,7 @@ ActiveRecord::Schema.define(version: 2019_07_22_014444) do
|
|||
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
||||
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
||||
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
||||
add_foreign_key "destructing_statuses", "statuses"
|
||||
add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
|
||||
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
|
||||
add_foreign_key "featured_tags", "accounts", on_delete: :cascade
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Fabricator(:destructing_status) do
|
||||
status nil
|
||||
delete_after "2019-07-23 10:25:14"
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DestructingStatus, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
Loading…
Reference in New Issue