This reverts committh-downstreamfa3b5195fd
, reversing changes made to1b1c4ff940
.
parent
0d1e55d566
commit
8aae9712d9
@ -1,19 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController
|
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
|
|
||||||
before_action :require_user!
|
|
||||||
|
|
||||||
def create
|
|
||||||
ReactService.new.call(current_account, @status, params[:id])
|
|
||||||
render json: @status, serializer: REST::StatusSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
UnreactWorker.perform_async(current_account.id, @status.id, params[:id])
|
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reactions_map: { @status.id => false })
|
|
||||||
rescue Mastodon::NotPermittedError
|
|
||||||
not_found
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,175 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
|
|
||||||
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
|
|
||||||
import { autoPlayGif, reduceMotion } from '../initial_state';
|
|
||||||
import { assetHost } from '../utils/config';
|
|
||||||
|
|
||||||
import { AnimatedNumber } from './animated_number';
|
|
||||||
|
|
||||||
export default class StatusReactions extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
statusId: PropTypes.string.isRequired,
|
|
||||||
reactions: ImmutablePropTypes.list.isRequired,
|
|
||||||
numVisible: PropTypes.number,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
canReact: PropTypes.bool.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
willEnter() {
|
|
||||||
return { scale: reduceMotion ? 1 : 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
willLeave() {
|
|
||||||
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { reactions, numVisible } = this.props;
|
|
||||||
let visibleReactions = reactions
|
|
||||||
.filter(x => x.get('count') > 0)
|
|
||||||
.sort((a, b) => b.get('count') - a.get('count'));
|
|
||||||
|
|
||||||
if (numVisible >= 0) {
|
|
||||||
visibleReactions = visibleReactions.filter((_, i) => i < numVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = visibleReactions.map(reaction => ({
|
|
||||||
key: reaction.get('name'),
|
|
||||||
data: reaction,
|
|
||||||
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
|
||||||
})).toArray();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
|
||||||
{items => (
|
|
||||||
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
|
||||||
{items.map(({ key, data, style }) => (
|
|
||||||
<Reaction
|
|
||||||
key={key}
|
|
||||||
statusId={this.props.statusId}
|
|
||||||
reaction={data}
|
|
||||||
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
canReact={this.props.canReact}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TransitionMotion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Reaction extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
statusId: PropTypes.string,
|
|
||||||
reaction: ImmutablePropTypes.map.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
canReact: PropTypes.bool.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
hovered: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
const { reaction, statusId, addReaction, removeReaction } = this.props;
|
|
||||||
|
|
||||||
if (reaction.get('me')) {
|
|
||||||
removeReaction(statusId, reaction.get('name'));
|
|
||||||
} else {
|
|
||||||
addReaction(statusId, reaction.get('name'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseEnter = () => this.setState({ hovered: true });
|
|
||||||
|
|
||||||
handleMouseLeave = () => this.setState({ hovered: false });
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { reaction } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
disabled={!this.props.canReact}
|
|
||||||
style={this.props.style}
|
|
||||||
>
|
|
||||||
<span className='reactions-bar__item__emoji'>
|
|
||||||
<Emoji
|
|
||||||
hovered={this.state.hovered}
|
|
||||||
emoji={reaction.get('name')}
|
|
||||||
url={reaction.get('url')}
|
|
||||||
staticUrl={reaction.get('static_url')}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span className='reactions-bar__item__count'>
|
|
||||||
<AnimatedNumber value={reaction.get('count')} />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Emoji extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
emoji: PropTypes.string.isRequired,
|
|
||||||
hovered: PropTypes.bool.isRequired,
|
|
||||||
url: PropTypes.string,
|
|
||||||
staticUrl: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { emoji, hovered, url, staticUrl } = this.props;
|
|
||||||
|
|
||||||
if (unicodeMapping[emoji]) {
|
|
||||||
const { filename, shortCode } = unicodeMapping[this.props.emoji];
|
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione'
|
|
||||||
alt={emoji}
|
|
||||||
title={title}
|
|
||||||
src={`${assetHost}/emoji/${filename}.svg`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const filename = (autoPlayGif || hovered) ? url : staticUrl;
|
|
||||||
const shortCode = `:${emoji}:`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione custom-emoji'
|
|
||||||
alt={shortCode}
|
|
||||||
title={shortCode}
|
|
||||||
src={filename}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 622 B |
Before Width: | Height: | Size: 744 B |
@ -1,26 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class ActivityPub::Activity::EmojiReact < ActivityPub::Activity
|
|
||||||
def perform
|
|
||||||
original_status = status_from_uri(object_uri)
|
|
||||||
name = @json['content']
|
|
||||||
return if original_status.nil? ||
|
|
||||||
!original_status.account.local? ||
|
|
||||||
delete_arrived_first?(@json['id'])
|
|
||||||
|
|
||||||
if /^:.*:$/.match?(name)
|
|
||||||
name.delete! ':'
|
|
||||||
custom_emoji = process_emoji_tags(name, @json['tag'])
|
|
||||||
|
|
||||||
return if custom_emoji.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
return if @account.reacted?(original_status, name, custom_emoji)
|
|
||||||
|
|
||||||
reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji)
|
|
||||||
|
|
||||||
LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction')
|
|
||||||
rescue ActiveRecord::RecordInvalid
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,37 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# == Schema Information
|
|
||||||
#
|
|
||||||
# Table name: status_reactions
|
|
||||||
#
|
|
||||||
# id :bigint(8) not null, primary key
|
|
||||||
# account_id :bigint(8) not null
|
|
||||||
# status_id :bigint(8) not null
|
|
||||||
# name :string default(""), not null
|
|
||||||
# custom_emoji_id :bigint(8)
|
|
||||||
# created_at :datetime not null
|
|
||||||
# updated_at :datetime not null
|
|
||||||
#
|
|
||||||
class StatusReaction < ApplicationRecord
|
|
||||||
belongs_to :account
|
|
||||||
belongs_to :status, inverse_of: :status_reactions
|
|
||||||
belongs_to :custom_emoji, optional: true
|
|
||||||
|
|
||||||
has_one :notification, as: :activity, dependent: :destroy
|
|
||||||
|
|
||||||
validates :name, presence: true
|
|
||||||
validates_with StatusReactionValidator
|
|
||||||
|
|
||||||
before_validation do
|
|
||||||
self.status = status.reblog if status&.reblog?
|
|
||||||
end
|
|
||||||
|
|
||||||
before_validation :set_custom_emoji
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Sets custom_emoji to nil when disabled
|
|
||||||
def set_custom_emoji
|
|
||||||
self.custom_emoji = CustomEmoji.find_by(disabled: false, shortcode: name, domain: custom_emoji.domain) if name.present? && custom_emoji.present?
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,39 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class ActivityPub::EmojiReactionSerializer < ActivityPub::Serializer
|
|
||||||
attributes :id, :type, :actor, :content
|
|
||||||
attribute :virtual_object, key: :object
|
|
||||||
attribute :custom_emoji, key: :tag, unless: -> { object.custom_emoji.nil? }
|
|
||||||
|
|
||||||
def id
|
|
||||||
[ActivityPub::TagManager.instance.uri_for(object.account), '#emoji_reactions/', object.id].join
|
|
||||||
end
|
|
||||||
|
|
||||||
def type
|
|
||||||
'EmojiReact'
|
|
||||||
end
|
|
||||||
|
|
||||||
def actor
|
|
||||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
|
||||||
end
|
|
||||||
|
|
||||||
def virtual_object
|
|
||||||
ActivityPub::TagManager.instance.uri_for(object.status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def content
|
|
||||||
if object.custom_emoji.nil?
|
|
||||||
object.name
|
|
||||||
else
|
|
||||||
":#{object.name}:"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
alias reaction content
|
|
||||||
|
|
||||||
# Akkoma (and possibly others) expect `tag` to be an array, so we can't just
|
|
||||||
# use the has_one shorthand because we need to wrap it into an array manually
|
|
||||||
def custom_emoji
|
|
||||||
[ActivityPub::EmojiSerializer.new(object.custom_emoji).serializable_hash]
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,19 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class ActivityPub::UndoEmojiReactionSerializer < ActivityPub::Serializer
|
|
||||||
attributes :id, :type, :actor
|
|
||||||
|
|
||||||
has_one :object, serializer: ActivityPub::EmojiReactionSerializer
|
|
||||||
|
|
||||||
def id
|
|
||||||
[ActivityPub::TagManager.instance.uri_for(object.account), '#emoji_reactions/', object.id, '/undo'].join
|
|
||||||
end
|
|
||||||
|
|
||||||
def type
|
|
||||||
'Undo'
|
|
||||||
end
|
|
||||||
|
|
||||||
def actor
|
|
||||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,46 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class ReactService < BaseService
|
|
||||||
include Authorization
|
|
||||||
include Payloadable
|
|
||||||
|
|
||||||
def call(account, status, emoji)
|
|
||||||
authorize_with account, status, :react?
|
|
||||||
|
|
||||||
name, domain = emoji.split('@')
|
|
||||||
return unless domain.nil? || status.local?
|
|
||||||
|
|
||||||
custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain)
|
|
||||||
reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji)
|
|
||||||
return reaction unless reaction.nil?
|
|
||||||
|
|
||||||
reaction = StatusReaction.create!(account: account, status: status, name: name, custom_emoji: custom_emoji)
|
|
||||||
|
|
||||||
Trends.statuses.register(status)
|
|
||||||
|
|
||||||
create_notification(reaction)
|
|
||||||
increment_statistics
|
|
||||||
|
|
||||||
reaction
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def create_notification(reaction)
|
|
||||||
status = reaction.status
|
|
||||||
|
|
||||||
if status.account.local?
|
|
||||||
LocalNotificationWorker.perform_async(status.account_id, reaction.id, 'StatusReaction', 'reaction')
|
|
||||||
elsif status.account.activitypub?
|
|
||||||
ActivityPub::DeliveryWorker.perform_async(build_json(reaction), reaction.account_id, status.account.inbox_url)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def increment_statistics
|
|
||||||
ActivityTracker.increment('activity:interactions')
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_json(reaction)
|
|
||||||
Oj.dump(serialize_payload(reaction, ActivityPub::EmojiReactionSerializer))
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class UnreactService < BaseService
|
|
||||||
include Payloadable
|
|
||||||
|
|
||||||
def call(account, status, emoji)
|
|
||||||
name, domain = emoji.split('@')
|
|
||||||
custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain)
|
|
||||||
reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji)
|
|
||||||
return if reaction.nil?
|
|
||||||
|
|
||||||
reaction.destroy!
|
|
||||||
create_notification(reaction) if !status.account.local? && status.account.activitypub?
|
|
||||||
reaction
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def create_notification(reaction)
|
|
||||||
status = reaction.status
|
|
||||||
ActivityPub::DeliveryWorker.perform_async(build_json(reaction), reaction.account_id, status.account.inbox_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_json(reaction)
|
|
||||||
Oj.dump(serialize_payload(reaction, ActivityPub::UndoEmojiReactionSerializer))
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,28 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class StatusReactionValidator < ActiveModel::Validator
|
|
||||||
SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze
|
|
||||||
|
|
||||||
LIMIT = [1, (ENV['MAX_REACTIONS'] || 1).to_i].max
|
|
||||||
|
|
||||||
def validate(reaction)
|
|
||||||
return if reaction.name.blank?
|
|
||||||
|
|
||||||
reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name)
|
|
||||||
reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if reaction.account.local? && new_reaction?(reaction) && limit_reached?(reaction)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def unicode_emoji?(name)
|
|
||||||
SUPPORTED_EMOJIS.include?(name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def new_reaction?(reaction)
|
|
||||||
!reaction.status.status_reactions.exists?(status: reaction.status, account: reaction.account, name: reaction.name, custom_emoji: reaction.custom_emoji)
|
|
||||||
end
|
|
||||||
|
|
||||||
def limit_reached?(reaction)
|
|
||||||
reaction.status.status_reactions.where(status: reaction.status, account: reaction.account).count >= LIMIT
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,13 +0,0 @@
|
|||||||
= content_for :heading do
|
|
||||||
= render 'application/mailer/heading', heading_title: t('notification_mailer.reaction.title'), heading_subtitle: t('notification_mailer.reaction.body', name: @account.pretty_acct), heading_image_url: frontend_asset_url('images/mailer-new/heading/reaction.png')
|
|
||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
|
||||||
%tr
|
|
||||||
%td.email-body-padding-td
|
|
||||||
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
|
||||||
%tr
|
|
||||||
%td.email-inner-card-td
|
|
||||||
= render 'status', status: @status, time_zone: @me.user_time_zone
|
|
||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
|
||||||
%tr
|
|
||||||
%td.email-padding-top-24
|
|
||||||
= render 'application/mailer/button', text: t('application_mailer.view_status'), url: web_url("@#{@status.account.pretty_acct}/#{@status.id}")
|
|
@ -1,5 +0,0 @@
|
|||||||
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
|
|
||||||
|
|
||||||
<%= raw t('notification_mailer.reaction.body', name: @account.pretty_acct) %>
|
|
||||||
|
|
||||||
<%= render 'status', status: @status %>
|
|
@ -1,11 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class UnreactWorker
|
|
||||||
include Sidekiq::Worker
|
|
||||||
|
|
||||||
def perform(account_id, status_id, emoji)
|
|
||||||
UnreactService.new.call(Account.find(account_id), Status.find(status_id), emoji)
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class CreateStatusReactions < ActiveRecord::Migration[6.1]
|
|
||||||
def change
|
|
||||||
create_table :status_reactions do |t|
|
|
||||||
t.references :account, null: false, foreign_key: { on_delete: :cascade }
|
|
||||||
t.references :status, null: false, foreign_key: { on_delete: :cascade }
|
|
||||||
t.string :name, null: false, default: ''
|
|
||||||
t.references :custom_emoji, null: true, foreign_key: { on_delete: :cascade }
|
|
||||||
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
add_index :status_reactions, [:account_id, :status_id, :name], unique: true, name: :index_status_reactions_on_account_id_and_status_id
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,49 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class MoveEmojiReactionSettings < ActiveRecord::Migration[6.1]
|
|
||||||
class User < ApplicationRecord; end
|
|
||||||
|
|
||||||
MAPPING = {
|
|
||||||
setting_visible_reactions: 'visible_reactions',
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
class LegacySetting < ApplicationRecord
|
|
||||||
self.table_name = 'settings'
|
|
||||||
|
|
||||||
def var
|
|
||||||
self[:var]&.to_sym
|
|
||||||
end
|
|
||||||
|
|
||||||
def value
|
|
||||||
YAML.safe_load(self[:value], permitted_classes: [ActiveSupport::HashWithIndifferentAccess]) if self[:value].present?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def up
|
|
||||||
User.find_in_batches do |users|
|
|
||||||
previous_settings_for_batch = LegacySetting.where(thing_type: 'User', thing_id: users.map(&:id)).group_by(&:thing_id)
|
|
||||||
|
|
||||||
users.each do |user|
|
|
||||||
previous_settings = previous_settings_for_batch[user.id]&.index_by(&:var) || {}
|
|
||||||
user_settings = Oj.load(user.settings || '{}')
|
|
||||||
user_settings.delete('theme')
|
|
||||||
|
|
||||||
MAPPING.each do |legacy_key, new_key|
|
|
||||||
value = previous_settings[legacy_key]&.value
|
|
||||||
|
|
||||||
next if value.blank?
|
|
||||||
|
|
||||||
if value.is_a?(Hash)
|
|
||||||
value.each do |nested_key, nested_value|
|
|
||||||
user_settings[MAPPING[legacy_key][nested_key.to_sym]] = nested_value
|
|
||||||
end
|
|
||||||
else
|
|
||||||
user_settings[new_key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
user.update_column('settings', Oj.dump(user_settings))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,8 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
Fabricator(:status_reaction) do
|
|
||||||
account
|
|
||||||
status
|
|
||||||
name '👍'
|
|
||||||
custom_emoji
|
|
||||||
end
|
|
@ -1,3 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
Loading…
Reference in new issue