Add wide emoji support

Co-authored-by: Kei IWASAKI <laughk@users.noreply.github.com>
This commit is contained in:
noellabo 2023-02-20 13:41:47 +09:00
parent 2029200420
commit fa82ec15d5
26 changed files with 192 additions and 16 deletions

View File

@ -98,6 +98,8 @@ class Settings::PreferencesController < Settings::BaseController
:setting_content_emoji_reaction_size,
:setting_emoji_scale,
:setting_picker_emoji_size,
:setting_enable_wide_emoji,
:setting_enable_wide_emoji_reaction,
:setting_hide_bot_on_public_timeline,
:setting_confirm_follow_from_bot,
:setting_default_search_searchability,

View File

@ -123,12 +123,35 @@ module AccountsHelper
return if user.nil?
":root {
css = []
css << <<-EOS
:root {
--content-font-size: #{h(user.setting_content_font_size)}px;
--info-font-size: #{h(user.setting_info_font_size)}px;
--content-emoji-reaction-size: #{h(user.setting_content_emoji_reaction_size)}px;
--emoji-scale: #{h(user.setting_emoji_scale)};
}"
}
EOS
css << <<-EOS if user.setting_enable_wide_emoji
img.emojione.custom-emoji:not(.reaction) {
width: unset !important;
max-width: min(100%, 10em);
}
EOS
css << <<-EOS if user.setting_enable_wide_emoji_reaction
span.reactions-bar__item__emoji {
width: unset !important;
}
span.reactions-bar__item__emoji img.emojione.custom-emoji {
width: unset !important;
max-width: 8em;
}
EOS
css.join("\n")
end
def svg_logo

View File

@ -139,7 +139,7 @@ module ApplicationHelper
elsif animate
image_tag(reaction['url'], class: 'emojione', alt: ":#{reaction['name']}:")
else
image_tag(reaction['static_url'], class: 'emojione custom-emoji', alt: ":#{reaction['name']}", 'data-original' => full_asset_url(reaction['url']), 'data-static' => full_asset_url(reaction['static_url']))
image_tag(reaction['static_url'], class: 'emojione custom-emoji reaction', alt: ":#{reaction['name']}", 'data-original' => full_asset_url(reaction['url']), 'data-static' => full_asset_url(reaction['static_url']))
end
end

View File

@ -4,12 +4,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'mastodon/initial_state';
import { assetHost } from 'mastodon/utils/config';
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
import classNames from 'classnames';
export default class Emoji extends React.PureComponent {
static propTypes = {
emoji: PropTypes.string.isRequired,
emojiMap: ImmutablePropTypes.map.isRequired,
className: PropTypes.string,
hovered: PropTypes.bool.isRequired,
url: PropTypes.string,
static_url: PropTypes.string,
@ -21,11 +23,12 @@ export default class Emoji extends React.PureComponent {
if (unicodeMapping[emoji]) {
const { filename, shortCode } = unicodeMapping[emoji];
const title = shortCode ? `:${shortCode}:` : '';
const className = classNames('emojione', this.props.className);
return (
<img
draggable='false'
className='emojione'
className={className}
alt={emoji}
title={title}
src={`${assetHost}/emoji/${filename}.svg`}
@ -34,11 +37,12 @@ export default class Emoji extends React.PureComponent {
} else if (emojiMap.get(emoji)) {
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
const shortCode = `:${emoji}:`;
const className = classNames('emojione custom-emoji', this.props.className);
return (
<img
draggable='false'
className='emojione custom-emoji'
className={className}
alt={shortCode}
title={shortCode}
src={filename}
@ -47,11 +51,12 @@ export default class Emoji extends React.PureComponent {
} else if (url || static_url) {
const filename = (autoPlayGif || hovered) && url ? url : static_url;
const shortCode = `:${emoji}:`;
const className = classNames('emojione custom-emoji', this.props.className);
return (
<img
draggable='false'
className='emojione custom-emoji'
className={className}
alt={shortCode}
title={shortCode}
src={filename}

View File

@ -123,7 +123,7 @@ class EmojiReaction extends ImmutablePureComponent {
<Fragment>
<div className='reactions-bar__item-wrapper' ref={this.setTargetRef}>
<button className={classNames('reactions-bar__item', { active: myReaction })} disabled={disableReactions || status.get('emoji_reactioned') && !myReaction} onClick={this.handleClick} title={`:${shortCode}:`} style={this.props.style}>
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={this.props.emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} /></span>
<span className='reactions-bar__item__emoji'><Emoji className='reaction' hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={this.props.emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} /></span>
<span className='reactions-bar__item__count'><AnimatedNumber value={emojiReaction.get('count')} /></span>
</button>
</div>

View File

@ -61,7 +61,7 @@ class Reaction extends ImmutablePureComponent {
return (
<div className='account__emoji_reaction' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<Emoji hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} />
<Emoji className='reaction' hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} />
</div>
);
};

View File

@ -352,18 +352,20 @@ class Notification extends ImmutablePureComponent {
const { intl, unread, emojiMap } = this.props;
if (!notification.get('emoji_reaction')) {
return <Fragment></Fragment>
return <Fragment />;
}
const wide = notification.getIn(['emoji_reaction', 'width'], 1) / notification.getIn(['emoji_reaction', 'height'], 1) >= 1.4;
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-reaction focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.emoji_reaction, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<div className={classNames('notification__reaction-icon-wrapper', { wide })}>
<Emoji hovered={false} emoji={notification.getIn(['emoji_reaction', 'name'])} emojiMap={emojiMap} url={notification.getIn(['emoji_reaction', 'url'])} static_url={notification.getIn(['emoji_reaction', 'static_url'])} />
</div>
<span title={notification.get('created_at')}>
<span title={notification.get('created_at')} className={classNames('notification__reaction-message-wrapper', { wide })}>
<FormattedMessage id='notification.emoji_reaction' defaultMessage='{name} reactioned your post' values={{ name: link }} />
</span>
</div>

View File

@ -60,6 +60,8 @@ export const enableEmptyColumn = getMeta('enable_empty_column');
export const showReloadButton = getMeta('show_reload_button');
export const defaultColumnWidth = getMeta('default_column_width');
export const pickerEmojiSize = getMeta('picker_emoji_size');
export const enableWideEmoji = getMeta('enable_wide_emoji');
export const enableWideEmojiReaction = getMeta('enable_wide_emoji_reaction');
export const disablePost = getMeta('disable_post');
export const disableReactions = getMeta('disable_reactions');
export const disableFollow = getMeta('disable_follow');

View File

@ -1946,7 +1946,8 @@ a.account__display-name {
}
}
.notification__favourite-icon-wrapper {
.notification__favourite-icon-wrapper,
.notification__reaction-icon-wrapper {
left: -26px;
position: absolute;
@ -1955,6 +1956,11 @@ a.account__display-name {
}
}
.notification__reaction-message-wrapper.wide {
display: block;
margin-top: 22px;
}
.icon-button.star-icon.active {
color: $gold-star;
}

View File

@ -100,6 +100,8 @@ class UserSettingsDecorator
hide_link_preview
hide_photo_preview
hide_video_preview
enable_wide_emoji
enable_wide_emoji_reaction
).freeze
STRING_KEYS = %w(

View File

@ -18,13 +18,15 @@
# visible_in_picker :boolean default(TRUE), not null
# category_id :bigint(8)
# image_storage_schema_version :integer
# width :integer
# height :integer
#
class CustomEmoji < ApplicationRecord
include Attachmentable
LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 50.kilobytes).to_i
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 200.kilobytes).to_i].max
LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 256.kilobytes).to_i
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
@ -37,7 +39,7 @@ class CustomEmoji < ApplicationRecord
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_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile exif' } }, validate_media_type: false
has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile exif', file_geometry_parser: FastGeometryParser } }, processors: [:dimension_extractor], validate_media_type: false
before_validation :downcase_domain

View File

@ -140,6 +140,7 @@ class User < ApplicationRecord
:post_reference_modal, :add_reference_modal, :unselect_reference_modal, :delete_scheduled_status_modal,
:hexagon_avatar, :enable_empty_column,
:content_font_size, :info_font_size, :content_emoji_reaction_size, :emoji_scale, :picker_emoji_size,
:enable_wide_emoji, :enable_wide_emoji_reaction,
:hide_bot_on_public_timeline, :confirm_follow_from_bot,
:default_search_searchability, :default_expires_in, :default_expires_action,
:show_reload_button, :default_column_width,

View File

@ -78,6 +78,8 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:content_emoji_reaction_size] = object.current_account.user.setting_content_emoji_reaction_size
store[:emoji_scale] = object.current_account.user.setting_emoji_scale
store[:picker_emoji_size] = object.current_account.user.setting_picker_emoji_size
store[:enable_wide_emoji] = object.current_account.user.setting_enable_wide_emoji
store[:enable_wide_emoji_reaction] = object.current_account.user.setting_enable_wide_emoji_reaction
store[:hide_bot_on_public_timeline] = object.current_account.user.setting_hide_bot_on_public_timeline
store[:confirm_follow_from_bot] = object.current_account.user.setting_confirm_follow_from_bot
store[:show_reload_button] = object.current_account.user.setting_show_reload_button

View File

@ -6,6 +6,8 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer
attributes :shortcode, :url, :static_url, :visible_in_picker
attribute :category, if: :category_loaded?
attribute :width, if: :width?
attribute :height, if: :height?
def url
full_asset_url(object.image.url)
@ -22,4 +24,20 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer
def category_loaded?
object.association(:category).loaded? && object.category.present?
end
def width
object.width
end
def height
object.height
end
def width?
!object.width.nil?
end
def height?
!object.height.nil?
end
end

View File

@ -8,6 +8,8 @@ class REST::EmojiReactionSerializer < ActiveModel::Serializer
attribute :url, if: :custom_emoji?
attribute :static_url, if: :custom_emoji?
attribute :domain, if: :custom_emoji?
attribute :width, if: :width?
attribute :height, if: :height?
belongs_to :account, serializer: REST::AccountSerializer
@ -26,4 +28,20 @@ class REST::EmojiReactionSerializer < ActiveModel::Serializer
def domain
object.custom_emoji.domain
end
def width
object.custom_emoji.width
end
def height
object.custom_emoji.height
end
def width?
custom_emoji? && object.custom_emoji.width
end
def height?
custom_emoji? && object.custom_emoji.height
end
end

View File

@ -9,6 +9,8 @@ class REST::GroupedEmojiReactionSerializer < ActiveModel::Serializer
attribute :url, if: :custom_emoji?
attribute :static_url, if: :custom_emoji?
attribute :domain, if: :custom_emoji?
attribute :width, if: :width?
attribute :height, if: :height?
attribute :account_ids, if: :has_account_ids?
def count
@ -38,4 +40,20 @@ class REST::GroupedEmojiReactionSerializer < ActiveModel::Serializer
def domain
object.custom_emoji.domain
end
def width
object.custom_emoji.width
end
def height
object.custom_emoji.height
end
def width?
custom_emoji? && object.custom_emoji.width
end
def height?
custom_emoji? && object.custom_emoji.height
end
end

View File

@ -148,6 +148,8 @@ class REST::InstanceSerializer < ActiveModel::Serializer
:searchability,
:status_compact_mode,
:account_conversations,
:enable_wide_emoji,
:enable_wide_emoji_reaction,
]
capabilities << :profile_search unless Chewy.enabled?

View File

@ -33,6 +33,12 @@
= f.input :setting_content_emoji_reaction_size, as: :range, input_html: { min: 10, max: 48, list: 'emoji_reaction_size_label' }, wrapper: :with_label, false: true, fedibird_features: true
= f.input :setting_picker_emoji_size, as: :range, input_html: { min: 22, max: 48, list: 'picker_emoji_size_label' }, wrapper: :with_label, false: true, fedibird_features: true
.fields-group
= f.input :setting_enable_wide_emoji, as: :boolean, wrapper: :with_label, hint: true, fedibird_features: true
.fields-group
= f.input :setting_enable_wide_emoji_reaction, as: :boolean, wrapper: :with_label, hint: true, fedibird_features: true
.fields-group
= f.input :setting_theme_public, as: :boolean, wrapper: :with_label, hint: true, fedibird_features: true

View File

@ -12,6 +12,7 @@ require_relative '../lib/sanitize_ext/sanitize_config'
require_relative '../lib/redis/namespace_extensions'
require_relative '../lib/paperclip/url_generator_extensions'
require_relative '../lib/paperclip/attachment_extensions'
require_relative '../lib/paperclip/dimension_extractor'
require_relative '../lib/paperclip/lazy_thumbnail'
require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/transcoder'

View File

@ -91,6 +91,8 @@ en:
setting_enable_personal_timeline: Enable a personal home to display personal post
setting_enable_reaction: Enable the reaction display on the timeline and display the reaction button
setting_enable_status_reference: Enable the feature where a post references another post
setting_enable_wide_emoji: Displays wide emoji derived from Misskey in their original proportions (for content)
setting_enable_wide_emoji_reaction: Displays wide emoji derived from Misskey in their original proportions (for reaction)
setting_follow_button_to_list_adder: Change the behavior of the Follow / Subscribe button, open a dialog where you can select a list to follow / subscribe, or opt out of receiving at home
setting_hexagon_avatar: Display everyone's avatar icon as a hollowed out hexagon (joke feature)
setting_hide_bot_on_public_timeline: Disable Bot accounts from appearing on federation & hashtag & domain & group timelines (overridden by column setting)
@ -288,6 +290,8 @@ en:
setting_enable_personal_timeline: Enable personal timeline
setting_enable_reaction: Enable reaction
setting_enable_status_reference: Enable reference
setting_enable_wide_emoji: Enable wide emoji (for content)
setting_enable_wide_emoji_reaction: Enable wide emoji (for reaction)
setting_expand_spoilers: Always expand posts marked with content warnings
setting_follow_button_to_list_adder: Open list add dialog with follow button
setting_hexagon_avatar: Experience NFT Avatar

View File

@ -87,6 +87,8 @@ ja:
setting_enable_personal_timeline: 自分限定を表示する自分限定ホームを有効にします
setting_enable_reaction: タイムラインでリアクションの表示を有効にし、リアクションボタンを表示する
setting_enable_status_reference: 投稿が別の投稿を参照する機能を有効にします
setting_enable_wide_emoji: Misskey由来の横幅の広い絵文字を元の比率で表示します本文
setting_enable_wide_emoji_reaction: Misskey由来の横幅の広い絵文字を元の比率で表示しますリアクション
setting_follow_button_to_list_adder: フォロー・購読ボタンの動作を変更し、フォロー・購読するリストを選択したり、ホームで受け取らないよう設定するダイアログを開きます
setting_hexagon_avatar: 全員のアバターアイコンを6角形にくりぬいて表示しますジョーク機能
setting_hide_bot_on_public_timeline: 連合・ハッシュタグ・ドメイン・グループタイムライン上にBotアカウントが表示されないようにします※カラム設定を優先
@ -284,6 +286,8 @@ ja:
setting_enable_personal_timeline: 自分限定ホームを有効にする
setting_enable_reaction: リアクションを有効にする
setting_enable_status_reference: 参照を有効にする
setting_enable_wide_emoji: ワイド絵文字を有効にする(本文)
setting_enable_wide_emoji_reaction: ワイド絵文字を有効にする(リアクション)
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
setting_follow_button_to_list_adder: フォローボタンでリスト追加ダイアログを開く
setting_hexagon_avatar: NFTアイコンを体験する

View File

@ -73,6 +73,8 @@ defaults: &defaults
content_emoji_reaction_size: 16
emoji_scale: 1,
picker_emoji_size: 22,
enable_wide_emoji: true
enable_wide_emoji_reaction: true
notification_emails:
follow: false
reblog: false

View File

@ -0,0 +1,6 @@
class AddWidthHeightToCustomEmoji < ActiveRecord::Migration[6.1]
def change
add_column :custom_emojis, :width, :integer, null: true, default: nil
add_column :custom_emojis, :height, :integer, null: true, default: nil
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_02_15_062659) do
ActiveRecord::Schema.define(version: 2023_02_21_031206) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -371,6 +371,8 @@ ActiveRecord::Schema.define(version: 2023_02_15_062659) do
t.boolean "visible_in_picker", default: true, null: false
t.bigint "category_id"
t.integer "image_storage_schema_version"
t.integer "width"
t.integer "height"
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
end

View File

@ -7,6 +7,8 @@ require_relative 'cli_helper'
module Mastodon
class EmojiCLI < Thor
include CLIHelper
def self.exit_on_failure?
true
end
@ -132,6 +134,38 @@ module Mastodon
say('OK', :green)
end
option :local_only, type: :boolean
option :remote_only, type: :boolean
option :all, type: :boolean
option :concurrency, type: :numeric, default: 5, aliases: [:c]
option :verbose, type: :boolean, aliases: [:v]
desc 'fix-dimension', 'Fix dimension all custom emoji'
long_desc <<-LONG_DESC
Fix dimension all custom emoji.
With the --local-only option, only local emoji will be fixed.
With the --remote-only option, only remote emoji will be fixed.
With the --all option, fix dimension of all emojis.
LONG_DESC
def fix_dimension
scope = CustomEmoji
scope = scope.local if options[:local_only]
scope = scope.remote if options[:remote_only]
scope = scope.where(width: nil) unless options[:all]
processed, fixed = parallelize_with_progress(scope) do |emoji|
width, height = FastImage.size(emoji.image.url)
next if width.nil?
emoji.update!(width: width, height: height)
1
rescue
next
end
say("Checked #{processed} emojis, fixed #{fixed}", :green, true)
end
private
def color(green, _yellow, red)

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module Paperclip
class DimensionExtractor < Paperclip::Processor
def make
geometry = options.fetch(:file_geometry_parser).from_file(@file)
attachment.instance.width = geometry.width if attachment.instance.respond_to?(:width)
attachment.instance.height = geometry.height if attachment.instance.respond_to?(:height)
File.open(@file.path)
end
end
end