Merge pull request #1146 from ThibG/glitch-soc/merge-upstream
Merge upstream changesremotes/1727458204337373841/tmp_refs/heads/signup-info-prompt
commit
98c2d2aa46
|
@ -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
|
||||||
|
|
|
@ -11,7 +11,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 'flavours/glitch/util/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
@ -110,19 +110,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 = {
|
||||||
|
@ -320,8 +307,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
|
||||||
|
|
|
@ -112,6 +112,11 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
||||||
// React-router does this for us, but too late, feeling laggy.
|
// React-router does this for us, but too late, feeling laggy.
|
||||||
document.querySelector(currentLinkSelector).classList.remove('active');
|
document.querySelector(currentLinkSelector).classList.remove('active');
|
||||||
document.querySelector(nextLinkSelector).classList.add('active');
|
document.querySelector(nextLinkSelector).classList.add('active');
|
||||||
|
|
||||||
|
if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
|
||||||
|
this.context.router.history.push(getLink(this.pendingIndex));
|
||||||
|
this.pendingIndex = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAnimationEnd = () => {
|
handleAnimationEnd = () => {
|
||||||
|
@ -162,7 +167,6 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
||||||
const { shouldAnimate } = this.state;
|
const { shouldAnimate } = this.state;
|
||||||
|
|
||||||
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||||
this.pendingIndex = null;
|
|
||||||
|
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>;
|
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>;
|
||||||
|
|
|
@ -93,8 +93,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());
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestion
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
results: state.getIn(['search', 'results']),
|
results: state.getIn(['search', 'results']),
|
||||||
suggestions: state.getIn(['suggestions', 'items']),
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
searchTerm: state.getIn(['search', 'value']),
|
searchTerm: state.getIn(['search', 'searchTerm']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -110,6 +110,11 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
// React-router does this for us, but too late, feeling laggy.
|
// React-router does this for us, but too late, feeling laggy.
|
||||||
document.querySelector(currentLinkSelector).classList.remove('active');
|
document.querySelector(currentLinkSelector).classList.remove('active');
|
||||||
document.querySelector(nextLinkSelector).classList.add('active');
|
document.querySelector(nextLinkSelector).classList.add('active');
|
||||||
|
|
||||||
|
if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
|
||||||
|
this.context.router.history.push(getLink(this.pendingIndex));
|
||||||
|
this.pendingIndex = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAnimationEnd = () => {
|
handleAnimationEnd = () => {
|
||||||
|
@ -160,7 +165,6 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
const { shouldAnimate } = this.state;
|
const { shouldAnimate } = this.state;
|
||||||
|
|
||||||
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||||
this.pendingIndex = null;
|
|
||||||
|
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||||
|
|
|
@ -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
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.
|
# 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"
|
||||||
|
@ -218,6 +218,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"
|
||||||
|
@ -231,6 +238,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
|
|
@ -3388,8 +3388,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"
|
version "2.6.3"
|
||||||
resolved "https://codeload.github.com/Gargron/emoji-mart/tar.gz/ff00dc470b5b2d9f145a6d6e977a54de5df2b4c9"
|
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"
|
||||||
|
|
Loading…
Reference in New Issue