@@ -7,7 +7,7 @@ class Api::V1::CustomEmojisController < Api::BaseController | |||||
def index | def index | ||||
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do | render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do | ||||
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer) | |||||
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false).includes(:category), each_serializer: REST::CustomEmojiSerializer) | |||||
end | end | ||||
end | end | ||||
end | end |
@@ -6,7 +6,7 @@ import Overlay from 'react-overlays/lib/Overlay'; | |||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
import detectPassiveEvents from 'detect-passive-events'; | import detectPassiveEvents from 'detect-passive-events'; | ||||
import { buildCustomEmojis } from '../../emoji/emoji'; | |||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; | |||||
const messages = defineMessages({ | const messages = defineMessages({ | ||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, | emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, | ||||
@@ -31,19 +31,6 @@ let EmojiPicker, Emoji; // load asynchronously | |||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; | const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; | ||||
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; | const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; | ||||
const categoriesSort = [ | |||||
'recent', | |||||
'custom', | |||||
'people', | |||||
'nature', | |||||
'foods', | |||||
'activity', | |||||
'places', | |||||
'objects', | |||||
'symbols', | |||||
'flags', | |||||
]; | |||||
class ModifierPickerMenu extends React.PureComponent { | class ModifierPickerMenu extends React.PureComponent { | ||||
static propTypes = { | static propTypes = { | ||||
@@ -241,8 +228,23 @@ class EmojiPickerMenu extends React.PureComponent { | |||||
} | } | ||||
const title = intl.formatMessage(messages.emoji); | const title = intl.formatMessage(messages.emoji); | ||||
const { modifierOpen } = this.state; | const { modifierOpen } = this.state; | ||||
const categoriesSort = [ | |||||
'recent', | |||||
'people', | |||||
'nature', | |||||
'foods', | |||||
'activity', | |||||
'places', | |||||
'objects', | |||||
'symbols', | |||||
'flags', | |||||
]; | |||||
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); | |||||
return ( | return ( | ||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> | <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> | ||||
<EmojiPicker | <EmojiPicker | ||||
@@ -92,8 +92,11 @@ export const buildCustomEmojis = (customEmojis) => { | |||||
keywords: [name], | keywords: [name], | ||||
imageUrl: url, | imageUrl: url, | ||||
custom: true, | custom: true, | ||||
customCategory: emoji.get('category'), | |||||
}); | }); | ||||
}); | }); | ||||
return emojis; | return emojis; | ||||
}; | }; | ||||
export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set()); |
@@ -16,6 +16,7 @@ | |||||
# uri :string | # uri :string | ||||
# image_remote_url :string | # image_remote_url :string | ||||
# visible_in_picker :boolean default(TRUE), not null | # visible_in_picker :boolean default(TRUE), not null | ||||
# category_id :bigint(8) | |||||
# | # | ||||
class CustomEmoji < ApplicationRecord | class CustomEmoji < ApplicationRecord | ||||
@@ -27,6 +28,7 @@ class CustomEmoji < ApplicationRecord | |||||
:(#{SHORTCODE_RE_FRAGMENT}): | :(#{SHORTCODE_RE_FRAGMENT}): | ||||
(?=[^[:alnum:]:]|$)/x | (?=[^[:alnum:]:]|$)/x | ||||
belongs_to :category, class_name: 'CustomEmojiCategory', optional: true | |||||
has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode | has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode | ||||
has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } } | has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } } | ||||
@@ -0,0 +1,15 @@ | |||||
# frozen_string_literal: true | |||||
# == Schema Information | |||||
# | |||||
# Table name: custom_emoji_categories | |||||
# | |||||
# id :bigint(8) not null, primary key | |||||
# name :string | |||||
# created_at :datetime not null | |||||
# updated_at :datetime not null | |||||
# | |||||
class CustomEmojiCategory < ApplicationRecord | |||||
has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category | |||||
end |
@@ -5,6 +5,8 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer | |||||
attributes :shortcode, :url, :static_url, :visible_in_picker | attributes :shortcode, :url, :static_url, :visible_in_picker | ||||
attribute :category, if: :category_loaded? | |||||
def url | def url | ||||
full_asset_url(object.image.url) | full_asset_url(object.image.url) | ||||
end | end | ||||
@@ -12,4 +14,12 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer | |||||
def static_url | def static_url | ||||
full_asset_url(object.image.url(:static)) | full_asset_url(object.image.url(:static)) | ||||
end | end | ||||
def category | |||||
object.category.name | |||||
end | |||||
def category_loaded? | |||||
object.association(:category).loaded? && object.category.present? | |||||
end | |||||
end | end |
@@ -0,0 +1,9 @@ | |||||
class CreateCustomEmojiCategories < ActiveRecord::Migration[5.2] | |||||
def change | |||||
create_table :custom_emoji_categories do |t| | |||||
t.string :name, index: { unique: true } | |||||
t.timestamps | |||||
end | |||||
end | |||||
end |
@@ -0,0 +1,5 @@ | |||||
class AddCategoryIdToCustomEmojis < ActiveRecord::Migration[5.2] | |||||
def change | |||||
add_column :custom_emojis, :category_id, :bigint | |||||
end | |||||
end |
@@ -10,7 +10,7 @@ | |||||
# | # | ||||
# It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||
ActiveRecord::Schema.define(version: 2019_05_29_143559) do | |||||
ActiveRecord::Schema.define(version: 2019_06_27_222826) do | |||||
# These are extensions that must be enabled in order to support this database | # These are extensions that must be enabled in order to support this database | ||||
enable_extension "plpgsql" | enable_extension "plpgsql" | ||||
@@ -208,6 +208,13 @@ ActiveRecord::Schema.define(version: 2019_05_29_143559) do | |||||
t.index ["uri"], name: "index_conversations_on_uri", unique: true | t.index ["uri"], name: "index_conversations_on_uri", unique: true | ||||
end | end | ||||
create_table "custom_emoji_categories", force: :cascade do |t| | |||||
t.string "name" | |||||
t.datetime "created_at", null: false | |||||
t.datetime "updated_at", null: false | |||||
t.index ["name"], name: "index_custom_emoji_categories_on_name", unique: true | |||||
end | |||||
create_table "custom_emojis", force: :cascade do |t| | create_table "custom_emojis", force: :cascade do |t| | ||||
t.string "shortcode", default: "", null: false | t.string "shortcode", default: "", null: false | ||||
t.string "domain" | t.string "domain" | ||||
@@ -221,6 +228,7 @@ ActiveRecord::Schema.define(version: 2019_05_29_143559) do | |||||
t.string "uri" | t.string "uri" | ||||
t.string "image_remote_url" | t.string "image_remote_url" | ||||
t.boolean "visible_in_picker", default: true, null: false | t.boolean "visible_in_picker", default: true, null: false | ||||
t.bigint "category_id" | |||||
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true | t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true | ||||
end | end | ||||
@@ -15,6 +15,7 @@ module Mastodon | |||||
option :suffix | option :suffix | ||||
option :overwrite, type: :boolean | option :overwrite, type: :boolean | ||||
option :unlisted, type: :boolean | option :unlisted, type: :boolean | ||||
option :category | |||||
desc 'import PATH', 'Import emoji from a TAR GZIP archive at PATH' | desc 'import PATH', 'Import emoji from a TAR GZIP archive at PATH' | ||||
long_desc <<-LONG_DESC | long_desc <<-LONG_DESC | ||||
Imports custom emoji from a TAR GZIP archive specified by PATH. | Imports custom emoji from a TAR GZIP archive specified by PATH. | ||||
@@ -22,6 +23,9 @@ module Mastodon | |||||
Existing emoji will be skipped unless the --overwrite option | Existing emoji will be skipped unless the --overwrite option | ||||
is provided, in which case they will be overwritten. | is provided, in which case they will be overwritten. | ||||
You can specifiy a --category under which the emojis will be | |||||
grouped together. | |||||
With the --prefix option, a prefix can be added to all | With the --prefix option, a prefix can be added to all | ||||
generated shortcodes. Likewise, the --suffix option controls | generated shortcodes. Likewise, the --suffix option controls | ||||
the suffix of all shortcodes. | the suffix of all shortcodes. | ||||
@@ -33,6 +37,7 @@ module Mastodon | |||||
imported = 0 | imported = 0 | ||||
skipped = 0 | skipped = 0 | ||||
failed = 0 | failed = 0 | ||||
category = options[:category] ? CustomEmojiCategory.find_or_create_by(name: options[:category]) : nil | |||||
Gem::Package::TarReader.new(Zlib::GzipReader.open(path)) do |tar| | Gem::Package::TarReader.new(Zlib::GzipReader.open(path)) do |tar| | ||||
tar.each do |entry| | tar.each do |entry| | ||||
@@ -50,6 +55,7 @@ module Mastodon | |||||
custom_emoji.image = StringIO.new(entry.read) | custom_emoji.image = StringIO.new(entry.read) | ||||
custom_emoji.image_file_name = File.basename(entry.full_name) | custom_emoji.image_file_name = File.basename(entry.full_name) | ||||
custom_emoji.visible_in_picker = !options[:unlisted] | custom_emoji.visible_in_picker = !options[:unlisted] | ||||
custom_emoji.category = category | |||||
if custom_emoji.save | if custom_emoji.save | ||||
imported += 1 | imported += 1 | ||||
@@ -0,0 +1,3 @@ | |||||
Fabricator(:custom_emoji_category) do | |||||
name "MyString" | |||||
end |
@@ -0,0 +1,5 @@ | |||||
require 'rails_helper' | |||||
RSpec.describe CustomEmojiCategory, type: :model do | |||||
pending "add some examples to (or delete) #{__FILE__}" | |||||
end |
@@ -3384,8 +3384,8 @@ elliptic@^6.0.0: | |||||
minimalistic-crypto-utils "^1.0.0" | minimalistic-crypto-utils "^1.0.0" | ||||
emoji-mart@Gargron/emoji-mart#build: | emoji-mart@Gargron/emoji-mart#build: | ||||
version "2.6.2" | |||||
resolved "https://codeload.github.com/Gargron/emoji-mart/tar.gz/ff00dc470b5b2d9f145a6d6e977a54de5df2b4c9" | |||||
version "2.6.3" | |||||
resolved "https://codeload.github.com/Gargron/emoji-mart/tar.gz/934f314fd8322276765066e8a2a6be5bac61b1cf" | |||||
emoji-regex@^7.0.1, emoji-regex@^7.0.2: | emoji-regex@^7.0.1, emoji-regex@^7.0.2: | ||||
version "7.0.3" | version "7.0.3" | ||||