diff --git a/.env.production.sample b/.env.production.sample
index 0bf01bdc361d93..f06826b62a33a6 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -75,3 +75,8 @@ S3_ALIAS_HOST=files.example.com
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952
+
+# C3 link
+# -------
+C3_OFFICIAL_SITE_URL=
+C3_TOYBOX_URL=
diff --git a/app/controllers/settings/request_custom_emojis_controller.rb b/app/controllers/settings/request_custom_emojis_controller.rb
new file mode 100644
index 00000000000000..1e02cbe15522b8
--- /dev/null
+++ b/app/controllers/settings/request_custom_emojis_controller.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+class Settings::RequestCustomEmojisController < Settings::BaseController
+ include Authorization
+
+ def index
+ @is_admin = authorize?
+ @custom_emojis = RequestCustomEmoji.order(:state, :shortcode).page(params[:page])
+ @form = Form::RequestCustomEmojiBatch.new
+ end
+
+ def new
+ @custom_emoji = RequestCustomEmoji.new
+ end
+
+ def create
+ @custom_emoji = RequestCustomEmoji.new(resource_params)
+ @custom_emoji.account_id = current_account.id
+ if CustomEmoji.find_by(shortcode: @custom_emoji.shortcode, domain: nil)
+ @custom_emoji.errors.add(:shortcode, I18n.t('settings.request_custom_emoji.errors.already_exists'))
+ render :new
+ return
+ end
+
+ if @custom_emoji.save
+ redirect_to settings_request_custom_emojis_path, notice: I18n.t('settings.request_custom_emoji.created_msg')
+ else
+ render :new
+ end
+ end
+
+ def batch
+ @form = Form::RequestCustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
+ @form.save
+ rescue ActionController::ParameterMissing
+ flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ rescue Mastodon::NotPermittedError
+ flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
+ ensure
+ redirect_to settings_request_custom_emojis_path
+ end
+
+ private
+
+ def resource_params
+ params.require(:request_custom_emoji).permit(:shortcode, :image)
+ end
+
+ def form_custom_emoji_batch_params
+ params.require(:form_request_custom_emoji_batch).permit(:action, request_custom_emoji_ids: [])
+ end
+
+ def action_from_button
+ if params[:approve]
+ 'approve'
+ elsif params[:reject]
+ 'reject'
+ elsif params[:delete]
+ 'delete'
+ end
+ end
+
+ def authorize?
+ begin
+ authorize(:custom_emoji, :index?)
+ rescue Mastodon::NotPermittedError
+ return false
+ end
+ return true
+ end
+end
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index e6b361c0c9c956..f8c6684100030c 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -11,6 +11,18 @@ const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
return obj;
}, {});
+const rewrite = txt => {
+ let edit_txt = txt.replaceAll('
', ' ').replaceAll('
', ' ')
+ const e = document.createElement('div');
+ e.innerHTML = edit_txt;
+ return e.innerText;
+}
+
+const checkOnlyIconStatus = content => {
+ const trimContent = rewrite(content).trim();
+ return trimContent.match("^:[0-9a-zA-Z_]+:([ \r\t\s\n]+:[0-9a-zA-Z_]+:)*$");
+};
+
export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n');
@@ -91,9 +103,10 @@ export function normalizeStatus(status, normalOldStatus) {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus.emojis);
+ const toBigIcon = checkOnlyIconStatus(normalStatus.content);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
- normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
+ normalStatus.contentHtml = emojify(normalStatus.content, emojiMap, toBigIcon);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
}
diff --git a/app/javascript/mastodon/components/navigation_portal.jsx b/app/javascript/mastodon/components/navigation_portal.jsx
index a64bc7b1d26f0b..49b12a9fa076ca 100644
--- a/app/javascript/mastodon/components/navigation_portal.jsx
+++ b/app/javascript/mastodon/components/navigation_portal.jsx
@@ -4,15 +4,21 @@ import { Switch, Route, withRouter } from 'react-router-dom';
import AccountNavigation from 'mastodon/features/account/navigation';
import Trends from 'mastodon/features/getting_started/containers/trends_container';
-import { showTrends } from 'mastodon/initial_state';
+import { showTrends, mascot } from 'mastodon/initial_state';
+
+import elephantUIPlane from 'images/elephant_ui_plane.svg';
const DefaultNavigation = () => (
- showTrends ? (
- <>
-
-
- >
- ) : null
+ <>
+
+
+
+ {
+ showTrends ? (
+
+ ) : null
+ }
+ >
);
class NavigationPortal extends PureComponent {
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx
index 9222b2dc87703b..ebc2ed9187fca3 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.jsx
+++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx
@@ -28,6 +28,8 @@ import { countableText } from '../util/counter';
import CharacterCounter from './character_counter';
+import LiteracyCautionComponent from './literacy_caution'
+
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({
@@ -312,6 +314,7 @@ class ComposeForm extends ImmutablePureComponent {
/>
+
);
}
diff --git a/app/javascript/mastodon/features/compose/components/literacy_caution.jsx b/app/javascript/mastodon/features/compose/components/literacy_caution.jsx
new file mode 100644
index 00000000000000..08816bb74f0505
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/literacy_caution.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+ cautionMessage: { id: 'custom.caution_message', defaultMessage: 'CAUTION' },
+});
+
+class LiteracyCaution extends ImmutablePureComponent {
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ }
+
+ render() {
+ const { intl } = this.props;
+ return (
+
+ )
+ }
+}
+
+export default injectIntl(LiteracyCaution);
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index c11ef458c74eaa..285ba2c2d4928f 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -22,7 +22,7 @@ const emojiFilename = (filename) => {
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
};
-const emojifyTextNode = (node, customEmojis) => {
+const emojifyTextNode = (node, customEmojis, bigIcon) => {
const VS15 = 0xFE0E;
const VS16 = 0xFE0F;
@@ -72,9 +72,10 @@ const emojifyTextNode = (node, customEmojis) => {
// now got a replacee as ':shortcode:'
// if you want additional emoji handler, add statements below which set replacement and return true.
const filename = autoPlayGif ? custom_emoji.url : custom_emoji.static_url;
+ const bigIconClass = bigIcon ? " big_icon" : "" ;
replacement = document.createElement('img');
replacement.setAttribute('draggable', 'false');
- replacement.setAttribute('class', 'emojione custom-emoji');
+ replacement.setAttribute('class', `emojione custom-emoji${bigIconClass}`);
replacement.setAttribute('alt', shortcode);
replacement.setAttribute('title', shortcode);
replacement.setAttribute('src', filename);
@@ -111,28 +112,28 @@ const emojifyTextNode = (node, customEmojis) => {
node.parentElement.replaceChild(fragment, node);
};
-const emojifyNode = (node, customEmojis) => {
+const emojifyNode = (node, customEmojis, bigIcon) => {
for (const child of node.childNodes) {
switch(child.nodeType) {
case Node.TEXT_NODE:
- emojifyTextNode(child, customEmojis);
+ emojifyTextNode(child, customEmojis, bigIcon);
break;
case Node.ELEMENT_NODE:
if (!child.classList.contains('invisible'))
- emojifyNode(child, customEmojis);
+ emojifyNode(child, customEmojis, bigIcon);
break;
}
}
};
-const emojify = (str, customEmojis = {}) => {
+const emojify = (str, customEmojis = {}, bigIcon = null) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = str;
if (!Object.keys(customEmojis).length)
customEmojis = null;
- emojifyNode(wrapper, customEmojis);
+ emojifyNode(wrapper, customEmojis, bigIcon);
return wrapper.innerHTML;
};
diff --git a/app/javascript/mastodon/features/getting_started/index.jsx b/app/javascript/mastodon/features/getting_started/index.jsx
index f0cd70d7a14006..74c61e7e210eaf 100644
--- a/app/javascript/mastodon/features/getting_started/index.jsx
+++ b/app/javascript/mastodon/features/getting_started/index.jsx
@@ -21,6 +21,8 @@ import ColumnSubheading from '../ui/components/column_subheading';
import TrendsContainer from './containers/trends_container';
+import { c3_official_site_url, c3_toybox_url } from 'mastodon/initial_state';
+
const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
@@ -42,6 +44,10 @@ const messages = defineMessages({
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+
+ officialSite: {id: 'external_url.official_site', defaultMessage: 'C3 Official Site'},
+ toybox: {id: 'external_url.toybox', defaultMessage: 'ToyBox'},
+ c3: { id: 'navigation_bar.c3', defaultMessage: 'C3' },
});
const mapStateToProps = state => ({
@@ -125,6 +131,22 @@ class GettingStarted extends ImmutablePureComponent {
navItems.push();
}
+ if (c3_official_site_url || c3_toybox_url) {
+ navItems.push(
+ ,
+ )
+ if(c3_official_site_url) {
+ navItems.push(
+ ,
+ )
+ }
+ if(c3_toybox_url) {
+ navItems.push(
+
+ )
+ }
+ }
+
navItems.push(
,
,
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index b16eb5e1795131..94772a6cf21e1e 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
import { WordmarkLogo } from 'mastodon/components/logo';
import NavigationPortal from 'mastodon/components/navigation_portal';
-import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
+import { timelinePreview, trendsEnabled, c3_official_site_url, c3_toybox_url } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile';
import ColumnLink from './column_link';
@@ -32,6 +32,9 @@ const messages = defineMessages({
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
+
+ officialSite: {id: 'external_url.official_site', defaultMessage: 'C3 Official Site'},
+ toybox: {id: 'external_url.toybox', defaultMessage: 'ToyBox'},
});
class NavigationPanel extends Component {
@@ -113,6 +116,16 @@ class NavigationPanel extends Component {
+ {
+ (c3_official_site_url || c3_toybox_url) && (
+ <>
+ { c3_official_site_url && }
+ { c3_toybox_url && }
+
+ >
+ )
+ }
+
>
)}
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index 11cd2a16732781..a1dc2e580229e1 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -81,6 +81,8 @@
* @property {boolean=} use_pending_items
* @property {string} version
* @property {string} sso_redirect
+ * @property {string | null} c3_official_site_url
+ * @property {string | null} c3_toybox_url
*/
/**
@@ -140,6 +142,10 @@ export const unfollowModal = getMeta('unfollow_modal');
export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
+
+export const c3_official_site_url = getMeta('c3_official_site_url');
+export const c3_toybox_url = getMeta('c3_toybox_url');
+
export const languages = initialState?.languages;
export const criticalUpdatesPending = initialState?.critical_updates_pending;
// @ts-expect-error
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 10d9cf3a219088..e12ae5f9317677 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -248,6 +248,8 @@
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue",
+ "external_url.official_site": "C3 Official Site",
+ "external_url.toybox": "ToyBox",
"explore.search_results": "Search results",
"explore.suggested_follows": "People",
"explore.title": "Explore",
@@ -399,6 +401,7 @@
"navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.bookmarks": "Bookmarks",
+ "navigation_bar.c3": "c3",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.compose": "Compose new post",
"navigation_bar.direct": "Private mentions",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 0bf0a96a272dfd..9eaf0b5cdb7caf 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -248,6 +248,8 @@
"error.unexpected_crash.next_steps_addons": "それらを無効化してからリロードをお試しください。それでも解決しない場合、他のブラウザやアプリで Mastodon をお試しください。",
"errors.unexpected_crash.copy_stacktrace": "スタックトレースをクリップボードにコピー",
"errors.unexpected_crash.report_issue": "問題を報告",
+ "external_url.official_site": "C3 公式サイト",
+ "external_url.toybox": "ToyBox",
"explore.search_results": "検索結果",
"explore.suggested_follows": "ユーザー",
"explore.title": "エクスプローラー",
@@ -399,6 +401,7 @@
"navigation_bar.advanced_interface": "上級者向けUIに戻る",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.bookmarks": "ブックマーク",
+ "navigation_bar.c3": "c3",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.compose": "投稿の新規作成",
"navigation_bar.direct": "非公開の返信",
@@ -725,5 +728,6 @@
"video.mute": "ミュート",
"video.pause": "一時停止",
"video.play": "再生",
- "video.unmute": "ミュートを解除する"
+ "video.unmute": "ミュートを解除する",
+ "custom.caution_message": "このMastodonに公開、未収載でトゥートされた内容はすべてインターネット上の誰でも閲覧することができます。\n個人情報、著作権等のネットリテラシーには十分注意してトゥートしましょう。"
}
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 1b2969c2348fd4..f0e869521d7933 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -24,3 +24,18 @@
@import 'mastodon/rtl';
@import 'mastodon/accessibility';
@import 'mastodon/rich_text';
+
+
+// 非上級者向けUIのマスコット配置
+.drawer__inner__mastodon.navigation_icon {
+ background: none;
+}
+// 絵文字単体を大きく
+.emojione.custom-emoji.big_icon {
+ width: 40px;
+ height: 40px;
+}
+.detailed-status .emojione.custom-emoji.big_icon {
+ width: 50px;
+ height: 50px;
+}
diff --git a/app/javascript/styles/c3.scss b/app/javascript/styles/c3.scss
new file mode 100644
index 00000000000000..85f6c6b03a6353
--- /dev/null
+++ b/app/javascript/styles/c3.scss
@@ -0,0 +1,83 @@
+// Colors
+$ui-base-color: #202020;
+$ui-base-lighter-color: #88882b;
+$ui-primary-color: #ffff00;
+$ui-secondary-color: #6e6e00;
+$ui-highlight-color: #ffff00;
+$primary-text-color: #ffffff;
+
+// Import defaults
+@import 'application';
+
+.compose-form__publish-button-wrapper button.button {
+ color: $ui-base-color;
+}
+
+a.column-link.column-link--transparent:hover {
+ color: $ui-highlight-color;
+}
+
+h1.column-header>button {
+ color: $ui-highlight-color;
+}
+
+.admin-wrapper div.content h2 {
+ color: $ui-highlight-color;
+}
+
+.simple_form .block-button, .simple_form .button, .simple_form button {
+ color: $ui-base-color;
+}
+
+.admin-wrapper .sidebar ul .simple-navigation-active-leaf a {
+ color: $ui-base-color;
+}
+
+.dashboard__quick-access {
+ color: $ui-base-color;
+}
+
+.button {
+ color: $ui-base-color;
+}
+
+.button.logo-button {
+ color: $ui-base-color;
+}
+
+.simple_form .block-button.negative, .simple_form .button.negative, .simple_form button.negative {
+ color: $primary-text-color;
+}
+
+.getting-started__wrapper .column-link {
+ color: $ui-primary-color;
+}
+
+.getting-started__wrapper .column-link:hover {
+ background-color: $ui-highlight-color;
+ color: $ui-base-color;
+}
+
+.account__section-headline a.active, .account__section-headline button.active, .notification__filter-bar a.active, .notification__filter-bar button.active {
+ color: $primary-text-color;
+}
+
+.muted .status__content, .muted .status__content a, .muted .status__content p, .muted .status__display-name strong {
+ color: #abab95;
+}
+
+.account__section-headline a, .account__section-headline button, .notification__filter-bar a, .notification__filter-bar button {
+ color: $ui-secondary-color;
+}
+
+.account__section-headline a:hover, .account__section-headline button:hover, .notification__filter-bar a:hover, .notification__filter-bar button:hover {
+ color: $ui-primary-color;
+}
+
+.account__section-headline a, .account__section-headline button, .notification__filter-bar a, .notification__filter-bar button {
+ color: $ui-secondary-color;
+}
+
+.account__section-headline a:hover, .account__section-headline button:hover, .notification__filter-bar a:hover, .notification__filter-bar button:hover {
+ color: $ui-primary-color;
+}
diff --git a/app/javascript/styles/gruvbox.scss b/app/javascript/styles/gruvbox.scss
new file mode 100644
index 00000000000000..72b2378f9dfc8b
--- /dev/null
+++ b/app/javascript/styles/gruvbox.scss
@@ -0,0 +1,30 @@
+// Colors
+$ui-base-color: #282828;
+$ui-base-lighter-color: #665C54;
+$ui-primary-color: #D5C4A1;
+$ui-secondary-color: #689D6A;
+$ui-highlight-color: #8EC07C;
+$primary-text-color: #EBDBB2;
+
+// Import defaults
+@import 'application';
+
+// Columns width
+//.column {
+// flex-grow: 1;
+//}
+
+// Stream
+.activity-stream {
+ .status.light {
+ .status__content {
+ color: $primary-text-color;
+ }
+
+ .display-name {
+ strong {
+ color: $primary-text-color;
+ }
+ }
+ }
+}
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 0f02563b48d31e..630d94cfba7610 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -1,6 +1,21 @@
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
+$column-breakpoint: 700px;
+$small-breakpoint: 960px;
+
+@font-face {
+ font-family: 'Material Icons';
+ font-style: normal;
+ font-weight: 400;
+ src: url('~material-design-icons/iconfont/MaterialIcons-Regular.eot');
+ src: local('Material Icons'),
+ local('MaterialIcons-Regular'),
+ url('~material-design-icons/iconfont/MaterialIcons-Regular.woff2') format('woff2'),
+ url('~material-design-icons/iconfont/MaterialIcons-Regular.woff') format('woff'),
+ url('~material-design-icons/iconfont/MaterialIcons-Regular.ttf') format('ttf')
+}
+
.container {
box-sizing: border-box;
max-width: $maximum-width;
@@ -13,6 +28,875 @@ $fluid-breakpoint: $maximum-width + 20px;
}
}
+.rich-formatting {
+ font-family: $font-sans-serif, sans-serif;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.7;
+ word-wrap: break-word;
+ color: $darker-text-color;
+
+ a {
+ color: $highlight-text-color;
+ text-decoration: underline;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: none;
+ }
+ }
+
+ p,
+ li {
+ color: $darker-text-color;
+ }
+
+ p {
+ margin-top: 0;
+ margin-bottom: .85em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ strong {
+ font-weight: 700;
+ color: $secondary-text-color;
+ }
+
+ em {
+ font-style: italic;
+ color: $secondary-text-color;
+ }
+
+ code {
+ font-size: 0.85em;
+ background: darken($ui-base-color, 8%);
+ border-radius: 4px;
+ padding: 0.2em 0.3em;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-family: $font-display, sans-serif;
+ margin-top: 1.275em;
+ margin-bottom: .85em;
+ font-weight: 500;
+ color: $secondary-text-color;
+ }
+
+ h1 {
+ font-size: 2em;
+ }
+
+ h2 {
+ font-size: 1.75em;
+ }
+
+ h3 {
+ font-size: 1.5em;
+ }
+
+ h4 {
+ font-size: 1.25em;
+ }
+
+ h5,
+ h6 {
+ font-size: 1em;
+ }
+
+ ul {
+ list-style: disc;
+ }
+
+ ol {
+ list-style: decimal;
+ }
+
+ ul,
+ ol {
+ margin: 0;
+ padding: 0;
+ padding-left: 2em;
+ margin-bottom: 0.85em;
+
+ &[type='a'] {
+ list-style-type: lower-alpha;
+ }
+
+ &[type='i'] {
+ list-style-type: lower-roman;
+ }
+ }
+
+ hr {
+ width: 100%;
+ height: 0;
+ border: 0;
+ border-bottom: 1px solid lighten($ui-base-color, 4%);
+ margin: 1.7em 0;
+
+ &.spacer {
+ height: 1px;
+ border: 0;
+ }
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ break-inside: auto;
+ margin-top: 24px;
+ margin-bottom: 32px;
+
+ thead tr,
+ tbody tr {
+ border-bottom: 1px solid lighten($ui-base-color, 4%);
+ font-size: 1em;
+ line-height: 1.625;
+ font-weight: 400;
+ text-align: left;
+ color: $darker-text-color;
+ }
+
+ thead tr {
+ border-bottom-width: 2px;
+ line-height: 1.5;
+ font-weight: 500;
+ color: $dark-text-color;
+ }
+
+ th,
+ td {
+ padding: 8px;
+ align-self: start;
+ align-items: start;
+ word-break: break-all;
+
+ &.nowrap {
+ width: 25%;
+ position: relative;
+
+ &::before {
+ content: ' ';
+ visibility: hidden;
+ }
+
+ span {
+ position: absolute;
+ left: 8px;
+ right: 8px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+ }
+
+ & > :first-child {
+ margin-top: 0;
+ }
+
+ .mdi {
+ font-family: 'Material Icons';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 24px; /* Preferred icon size */
+ display: inline-block;
+ line-height: 1;
+ text-transform: none;
+ letter-spacing: normal;
+ word-wrap: normal;
+ white-space: nowrap;
+ direction: ltr;
+
+ /* Support for all WebKit browsers. */
+ -webkit-font-smoothing: antialiased;
+ /* Support for Safari and Chrome. */
+ text-rendering: optimizeLegibility;
+
+ /* Support for Firefox. */
+ -moz-osx-font-smoothing: grayscale;
+
+ /* Support for IE. */
+ font-feature-settings: 'liga';
+ }
+}
+
+.information-board {
+ background: darken($ui-base-color, 4%);
+ padding: 20px 0;
+
+ .container-alt {
+ position: relative;
+ padding-right: 280px + 15px;
+ }
+
+ &__sections {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ }
+
+ &__section {
+ flex: 1 0 0;
+ font-family: $font-sans-serif, sans-serif;
+ font-size: 16px;
+ line-height: 28px;
+ color: $primary-text-color;
+ text-align: right;
+ padding: 10px 15px;
+
+ span,
+ strong {
+ display: block;
+ }
+
+ span {
+ &:last-child {
+ color: $secondary-text-color;
+ }
+ }
+
+ strong {
+ font-family: $font-display, sans-serif;
+ font-weight: 500;
+ font-size: 32px;
+ line-height: 48px;
+ }
+
+ @media screen and (max-width: $column-breakpoint) {
+ text-align: center;
+ }
+ }
+
+ .panel {
+ position: absolute;
+ width: 280px;
+ box-sizing: border-box;
+ background: darken($ui-base-color, 8%);
+ padding: 20px;
+ padding-top: 10px;
+ border-radius: 4px 4px 0 0;
+ right: 0;
+ bottom: -40px;
+
+ .panel-header {
+ font-family: $font-display, sans-serif;
+ font-size: 14px;
+ line-height: 24px;
+ font-weight: 500;
+ color: $darker-text-color;
+ padding-bottom: 5px;
+ margin-bottom: 15px;
+ border-bottom: 1px solid lighten($ui-base-color, 4%);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+ a,
+ span {
+ font-weight: 400;
+ color: darken($darker-text-color, 10%);
+ }
+
+ a {
+ text-decoration: none;
+ }
+ }
+ }
+
+ .owner {
+ text-align: center;
+
+ .avatar {
+ width: 80px;
+ height: 80px;
+ margin: 0 auto;
+ margin-bottom: 15px;
+
+ img {
+ display: block;
+ width: 80px;
+ height: 80px;
+ border-radius: 48px;
+ }
+ }
+
+ .name {
+ font-size: 14px;
+
+ a {
+ display: block;
+ color: $primary-text-color;
+ text-decoration: none;
+
+ &:hover {
+ .display_name {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .username {
+ display: block;
+ color: $darker-text-color;
+ }
+ }
+ }
+}
+
+.landing-page {
+ p,
+ li {
+ font-family: $font-sans-serif, sans-serif;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 30px;
+ margin-bottom: 12px;
+ color: $darker-text-color;
+
+ a {
+ color: $highlight-text-color;
+ text-decoration: underline;
+ }
+ }
+
+ em {
+ display: inline;
+ margin: 0;
+ padding: 0;
+ font-weight: 700;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ color: lighten($darker-text-color, 10%);
+ }
+
+ h1 {
+ font-family: $font-display, sans-serif;
+ font-size: 26px;
+ line-height: 30px;
+ font-weight: 500;
+ margin-bottom: 20px;
+ color: $secondary-text-color;
+
+ small {
+ font-family: $font-sans-serif, sans-serif;
+ display: block;
+ font-size: 18px;
+ font-weight: 400;
+ color: lighten($darker-text-color, 10%);
+ }
+ }
+
+ h2 {
+ font-family: $font-display, sans-serif;
+ font-size: 22px;
+ line-height: 26px;
+ font-weight: 500;
+ margin-bottom: 20px;
+ color: $secondary-text-color;
+ }
+
+ h3 {
+ font-family: $font-display, sans-serif;
+ font-size: 18px;
+ line-height: 24px;
+ font-weight: 500;
+ margin-bottom: 20px;
+ color: $secondary-text-color;
+ }
+
+ h4 {
+ font-family: $font-display, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 500;
+ margin-bottom: 20px;
+ color: $secondary-text-color;
+ }
+
+ h5 {
+ font-family: $font-display, sans-serif;
+ font-size: 14px;
+ line-height: 24px;
+ font-weight: 500;
+ margin-bottom: 20px;
+ color: $secondary-text-color;
+ }
+
+ h6 {
+ font-family: $font-display, sans-serif;
+ font-size: 12px;
+ line-height: 24px;
+ font-weight: 500;
+ margin-bottom: 20px;
+ color: $secondary-text-color;
+ }
+
+ ul,
+ ol {
+ margin-left: 20px;
+
+ &[type='a'] {
+ list-style-type: lower-alpha;
+ }
+
+ &[type='i'] {
+ list-style-type: lower-roman;
+ }
+ }
+
+ ul {
+ list-style: disc;
+ }
+
+ ol {
+ list-style: decimal;
+ }
+
+ li > ol,
+ li > ul {
+ margin-top: 6px;
+ }
+
+ hr {
+ width: 100%;
+ height: 0;
+ border: 0;
+ border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
+ margin: 20px 0;
+
+ &.spacer {
+ height: 1px;
+ border: 0;
+ }
+ }
+
+ &__information,
+ &__forms {
+ padding: 20px;
+ }
+
+ &__call-to-action {
+ background: $ui-base-color;
+ border-radius: 4px;
+ padding: 25px 40px;
+ overflow: hidden;
+ box-sizing: border-box;
+
+ .row {
+ width: 100%;
+ display: flex;
+ flex-direction: row-reverse;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .row__information-board {
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-end;
+
+ .information-board__section {
+ flex: 1 0 auto;
+ padding: 0 10px;
+ }
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ width: 100%;
+ justify-content: space-between;
+ }
+ }
+
+ .row__mascot {
+ flex: 1;
+ margin: 10px -50px 0 0;
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ display: none;
+ }
+ }
+ }
+
+ &__logo {
+ margin-right: 20px;
+
+ img {
+ height: 50px;
+ width: auto;
+ mix-blend-mode: lighten;
+ }
+ }
+
+ &__information {
+ padding: 45px 40px;
+ margin-bottom: 10px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ strong {
+ font-weight: 500;
+ color: lighten($darker-text-color, 10%);
+ }
+
+ .account {
+ border-bottom: 0;
+ padding: 0;
+
+ &__display-name {
+ align-items: center;
+ display: flex;
+ margin-right: 5px;
+ }
+
+ div.account__display-name {
+ &:hover {
+ .display-name strong {
+ text-decoration: none;
+ }
+ }
+
+ .account__avatar {
+ cursor: default;
+ }
+ }
+
+ &__avatar-wrapper {
+ margin-left: 0;
+ flex: 0 0 auto;
+ }
+
+ .display-name {
+ font-size: 15px;
+
+ &__account {
+ font-size: 14px;
+ }
+ }
+ }
+
+ @media screen and (max-width: $small-breakpoint) {
+ .contact {
+ margin-top: 30px;
+ }
+ }
+
+ @media screen and (max-width: $column-breakpoint) {
+ padding: 25px 20px;
+ }
+ }
+
+ &__information,
+ &__forms,
+ #mastodon-timeline {
+ box-sizing: border-box;
+ background: $ui-base-color;
+ border-radius: 4px;
+ box-shadow: 0 0 6px rgba($black, 0.1);
+ }
+
+ &__mascot {
+ height: 104px;
+ position: relative;
+ left: -40px;
+ bottom: 25px;
+
+ img {
+ height: 190px;
+ width: auto;
+ }
+ }
+
+ &__short-description {
+ .row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ margin-bottom: 40px;
+ }
+
+ @media screen and (max-width: $column-breakpoint) {
+ .row {
+ margin-bottom: 20px;
+ }
+ }
+
+ p a {
+ color: $secondary-text-color;
+ }
+
+ h1 {
+ font-weight: 500;
+ color: $primary-text-color;
+ margin-bottom: 0;
+
+ small {
+ color: $darker-text-color;
+
+ span {
+ color: $secondary-text-color;
+ }
+ }
+ }
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__hero {
+ margin-bottom: 10px;
+
+ img {
+ display: block;
+ margin: 0;
+ max-width: 100%;
+ height: auto;
+ border-radius: 4px;
+ }
+ }
+
+ @media screen and (max-width: 840px) {
+ .information-board {
+ .container-alt {
+ padding-right: 20px;
+ }
+
+ .panel {
+ position: static;
+ margin-top: 20px;
+ width: 100%;
+ border-radius: 4px;
+
+ .panel-header {
+ text-align: center;
+ }
+ }
+ }
+ }
+
+ @media screen and (max-width: 675px) {
+ .header-wrapper {
+ padding-top: 0;
+
+ &.compact {
+ padding-bottom: 0;
+ }
+
+ &.compact .hero .heading {
+ text-align: initial;
+ }
+ }
+
+ .header .container-alt,
+ .features .container-alt {
+ display: block;
+ }
+ }
+
+ .cta {
+ margin: 20px;
+ }
+}
+
+.landing {
+ margin-bottom: 100px;
+
+ @media screen and (max-width: 738px) {
+ margin-bottom: 0;
+ }
+
+ &__brand {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 50px;
+
+ svg {
+ fill: $primary-text-color;
+ height: 52px;
+ }
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ padding: 0;
+ margin-bottom: 30px;
+ }
+ }
+
+ .directory {
+ margin-top: 30px;
+ background: transparent;
+ box-shadow: none;
+ border-radius: 0;
+ }
+
+ .hero-widget {
+ margin-top: 30px;
+ margin-bottom: 0;
+
+ h4 {
+ padding: 10px;
+ text-transform: uppercase;
+ font-weight: 700;
+ font-size: 13px;
+ color: $darker-text-color;
+ }
+
+ &__text {
+ border-radius: 0;
+ padding-bottom: 0;
+ }
+
+ &__footer {
+ background: $ui-base-color;
+ padding: 10px;
+ border-radius: 0 0 4px 4px;
+ display: flex;
+
+ &__column {
+ flex: 1 1 50%;
+ overflow-x: hidden;
+ }
+ }
+
+ .account {
+ padding: 10px 0;
+ border-bottom: 0;
+
+ .account__display-name {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ &__counters__wrapper {
+ display: flex;
+ }
+
+ &__counter {
+ padding: 10px;
+ width: 50%;
+
+ strong {
+ font-family: $font-display, sans-serif;
+ font-size: 15px;
+ font-weight: 700;
+ display: block;
+ }
+
+ span {
+ font-size: 14px;
+ color: $darker-text-color;
+ }
+ }
+ }
+
+ .simple_form .user_agreement .label_input > label {
+ font-weight: 400;
+ color: $darker-text-color;
+ }
+
+ .simple_form p.lead {
+ color: $darker-text-color;
+ font-size: 15px;
+ line-height: 20px;
+ font-weight: 400;
+ margin-bottom: 25px;
+ }
+
+ &__grid {
+ max-width: 960px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: minmax(0, 50%) minmax(0, 50%);
+ grid-gap: 30px;
+
+ @media screen and (max-width: 738px) {
+ grid-template-columns: minmax(0, 100%);
+ grid-gap: 10px;
+
+ &__column-login {
+ grid-row: 1;
+ display: flex;
+ flex-direction: column;
+
+ .box-widget {
+ order: 2;
+ flex: 0 0 auto;
+ }
+
+ .hero-widget {
+ margin-top: 0;
+ margin-bottom: 10px;
+ order: 1;
+ flex: 0 0 auto;
+ }
+ }
+
+ &__column-registration {
+ grid-row: 2;
+ }
+
+ .directory {
+ margin-top: 10px;
+ }
+ }
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ grid-gap: 0;
+
+ .hero-widget {
+ display: block;
+ margin-bottom: 0;
+ box-shadow: none;
+
+ &__img,
+ &__img img,
+ &__footer {
+ border-radius: 0;
+ }
+ }
+
+ .hero-widget,
+ .box-widget,
+ .directory__tag {
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+ }
+
+ .directory {
+ margin-top: 0;
+
+ &__tag {
+ margin-bottom: 0;
+
+ & > a,
+ & > div {
+ border-radius: 0;
+ box-shadow: none;
+ }
+
+ &:last-child {
+ border-bottom: 0;
+ }
+ }
+ }
+ }
+ }
+}
.brand {
position: relative;
text-decoration: none;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 5f773c1cf3f469..03da16bb5ccea3 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -705,6 +705,18 @@ body > [data-popper-placement] {
}
}
+.literacy-caution {
+ margin-top: 10px;
+ padding: 1rem;
+ border-radius: 4px;
+ background-color: $ui-base-color;
+}
+.literacy-caution a, .literacy-caution a:link, .literacy-caution a:visited, .literacy-caution a:hover, .literacy-caution a:active {
+ color: $primary-text-color;
+ text-decoration: none;
+}
+
+
.character-counter {
cursor: default;
font-family: $font-sans-serif, sans-serif;
diff --git a/app/javascript/styles/suumo.scss b/app/javascript/styles/suumo.scss
new file mode 100644
index 00000000000000..a653f1a38c14ba
--- /dev/null
+++ b/app/javascript/styles/suumo.scss
@@ -0,0 +1,3 @@
+@import 'suumo/variables';
+@import 'application';
+@import 'suumo/diff';
diff --git a/app/javascript/styles/suumo/diff.scss b/app/javascript/styles/suumo/diff.scss
new file mode 100644
index 00000000000000..f3cc08376c650f
--- /dev/null
+++ b/app/javascript/styles/suumo/diff.scss
@@ -0,0 +1,86 @@
+// components.scss
+.compose-form {
+ .compose-form__modifiers {
+ .compose-form__upload {
+ &-description {
+ input {
+ &::placeholder {
+ opacity: 1;
+ }
+ }
+ }
+ }
+ }
+}
+
+.rich-formatting a,
+.rich-formatting p a,
+.rich-formatting li a,
+.landing-page__short-description p a,
+.status__content a,
+.reply-indicator__content a {
+ color: lighten($ui-highlight-color, 12%);
+ text-decoration: underline;
+
+ &.mention {
+ text-decoration: none;
+ }
+
+ &.mention span {
+ text-decoration: underline;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: none;
+ }
+ }
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: none;
+ }
+
+ &.status__content__spoiler-link {
+ color: $secondary-text-color;
+ text-decoration: none;
+ }
+}
+
+.status__content__read-more-button {
+ text-decoration: underline;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: none;
+ }
+}
+
+.getting-started__footer a {
+ text-decoration: underline;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: none;
+ }
+}
+
+.nothing-here {
+ color: $darker-text-color;
+}
+
+.public-layout .public-account-header__tabs__tabs .counter.active::after {
+ border-bottom: 4px solid $ui-highlight-color;
+}
+
+.compose-form__publish-button-wrapper button.button::before {
+ content: "スーモ!";
+ font-size: 14px;
+}
+
+.compose-form__publish-button-wrapper button.button {
+ font-size: 0;
+}
diff --git a/app/javascript/styles/suumo/variables.scss b/app/javascript/styles/suumo/variables.scss
new file mode 100644
index 00000000000000..817e06b64b869c
--- /dev/null
+++ b/app/javascript/styles/suumo/variables.scss
@@ -0,0 +1,24 @@
+// Dependent colors
+$black: #0c1505;
+
+$classic-base-color: #18290a;
+$classic-primary-color: #9baec8;
+$classic-secondary-color: #d9e1e8;
+$classic-highlight-color: #62a527;
+
+$ui-base-color: $classic-base-color !default;
+$ui-primary-color: $classic-primary-color !default;
+$ui-secondary-color: $classic-secondary-color !default;
+
+// Differences
+$ui-highlight-color: #6eb92b;
+
+$darker-text-color: lighten($ui-primary-color, 20%) !default;
+$dark-text-color: lighten($ui-primary-color, 12%) !default;
+$secondary-text-color: lighten($ui-secondary-color, 6%) !default;
+$highlight-text-color: $classic-highlight-color !default;
+$action-button-color: #8d9ac2;
+
+$inverted-text-color: $black !default;
+$lighter-text-color: darken($ui-base-color, 6%) !default;
+$light-text-color: darken($ui-primary-color, 40%) !default;
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 35f0b5fee18a9a..69fd84be01353d 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -20,4 +20,10 @@ def set_autoreply_headers!
headers['X-Auto-Response-Suppress'] = 'All'
headers['Auto-Submitted'] = 'auto-generated'
end
+
+ def set_autoreply_headers!
+ headers['Precedence'] = 'list'
+ headers['X-Auto-Response-Suppress'] = 'All'
+ headers['Auto-Submitted'] = 'auto-generated'
+ end
end
diff --git a/app/models/form/request_custom_emoji_batch.rb b/app/models/form/request_custom_emoji_batch.rb
new file mode 100644
index 00000000000000..f8a8e773d8078c
--- /dev/null
+++ b/app/models/form/request_custom_emoji_batch.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class Form::RequestCustomEmojiBatch
+ include ActiveModel::Model
+ include Authorization
+ include AccountableConcern
+
+ attr_accessor :request_custom_emoji_ids, :action, :current_account
+
+ def save
+ case action
+ when 'approve'
+ approve!
+ when 'reject'
+ reject!
+ when 'delete'
+ delete!
+ end
+ end
+
+ private
+
+ def request_custom_emojis
+ @request_custom_emojis ||= RequestCustomEmoji.where(id: request_custom_emoji_ids)
+ end
+
+ def approve!
+ request_custom_emojis.each { |request_custom_emoji| authorize(request_custom_emoji, :update?) }
+
+ request_custom_emojis.each do |request_custom_emoji|
+ if request_custom_emoji.state != 0
+ next
+ end
+ request_custom_emoji.update(state: 1)
+ new_emoji = CustomEmoji.new(
+ shortcode: request_custom_emoji.shortcode,
+ image: request_custom_emoji.image
+ )
+ new_emoji.save
+ end
+ end
+
+ def reject!
+ request_custom_emojis.each { |request_custom_emoji| authorize(request_custom_emoji, :update?) }
+
+ request_custom_emojis.each do |request_custom_emoji|
+ if request_custom_emoji.state == 0
+ request_custom_emoji.update(state: 2)
+ end
+ end
+ end
+
+ def delete!
+ request_custom_emojis.each { |request_custom_emoji| authorize(request_custom_emoji, :destroy?) }
+
+ request_custom_emojis.each do |request_custom_emoji|
+ request_custom_emoji.destroy
+ end
+ end
+end
diff --git a/app/models/request_custom_emoji.rb b/app/models/request_custom_emoji.rb
new file mode 100644
index 00000000000000..d21da0a2a3f123
--- /dev/null
+++ b/app/models/request_custom_emoji.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: request_custom_emojis
+#
+# id :bigint(8) not null, primary key
+# state :integer default(0)
+# shortcode :string default(""), not null
+# image_file_name :string
+# image_content_type :string
+# image_file_size :integer
+# image_updated_at :datetime
+# created_at :datetime not null
+# updated_at :datetime not null
+# image_storage_schema_version :integer
+# account_id :bigint(8) not null
+#
+
+class RequestCustomEmoji < ApplicationRecord
+ include Attachmentable
+
+ belongs_to :account
+
+ has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set modify-date +set create-date' } }, validate_media_type: false
+
+ validates_attachment :image, content_type: { content_type: CustomEmoji::IMAGE_MIME_TYPES }, presence: true, size: { less_than: CustomEmoji::LIMIT }
+ validates :shortcode, uniqueness: true, format: { with: CustomEmoji::SHORTCODE_ONLY_RE }, length: { minimum: 2 }
+
+ scope :alphabetic, -> { order(shortcode: :asc) }
+
+ remotable_attachment :image, CustomEmoji::LIMIT
+
+ def object_type
+ :emoji
+ end
+
+ class << self
+ def from_text(text, domain = nil)
+ return [] if text.blank?
+
+ shortcodes = text.scan(CustomEmoji::SCAN_RE).map(&:first).uniq
+
+ return [] if shortcodes.empty?
+
+ EntityCache.instance.emoji(shortcodes, domain)
+ end
+
+ def search(shortcode)
+ where('"custom_emojis"."shortcode" ILIKE ?', "%#{shortcode}%")
+ end
+ end
+
+ private
+end
diff --git a/app/policies/request_custom_emoji_policy.rb b/app/policies/request_custom_emoji_policy.rb
new file mode 100644
index 00000000000000..913767e331792d
--- /dev/null
+++ b/app/policies/request_custom_emoji_policy.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RequestCustomEmojiPolicy < ApplicationPolicy
+ def update?
+ role.can?(:manage_custom_emojis)
+ end
+
+ def destroy?
+ role.can?(:manage_custom_emojis) || (record.account_id == current_account&.id)
+ end
+end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index d1c03a41318eb0..f381e5df28d30a 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -37,6 +37,14 @@ def meta
sso_redirect: sso_redirect,
}
+ if ENV['C3_OFFICIAL_SITE_URL'].present?
+ store[:c3_official_site_url] = ENV.fetch('C3_OFFICIAL_SITE_URL')
+ end
+
+ if ENV['C3_TOYBOX_URL'].present?
+ store[:c3_toybox_url] = ENV.fetch('C3_TOYBOX_URL')
+ end
+
if object.current_account
store[:me] = object.current_account.id.to_s
store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal
diff --git a/app/views/settings/request_custom_emojis/_custom_emoji.html.haml b/app/views/settings/request_custom_emojis/_custom_emoji.html.haml
new file mode 100644
index 00000000000000..459ed967f9b872
--- /dev/null
+++ b/app/views/settings/request_custom_emojis/_custom_emoji.html.haml
@@ -0,0 +1,25 @@
+.batch-table__row
+ %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+ = f.check_box :request_custom_emoji_ids, { multiple: true, include_hidden: false }, custom_emoji.id
+ .batch-table__row__content.batch-table__row__content--simple.batch-table__row__content--with-image
+ .batch-table__row__content__image
+ = custom_emoji_tag(custom_emoji)
+
+ .batch-table__row__content__text
+ %samp= ":#{custom_emoji.shortcode}:"
+
+ .batch-table__row__content__image
+ - if prefers_autoplay?
+ = image_tag custom_emoji.account.avatar_original_url, alt: custom_emoji.account.username, class: 'emojione'
+ - else
+ = image_tag custom_emoji.account.avatar_static_url, alt: custom_emoji.account.username, class: 'emojione custom-emoji', 'data-original' => custom_emoji.account.avatar_original_url, 'data-static' => custom_emoji.account.avatar_static_url
+
+ .batch-table__row__content__extra
+ - if custom_emoji.state == 0
+ = t('settings.request_custom_emoji.state.pending')
+ - elsif custom_emoji.state == 1
+ = t('settings.request_custom_emoji.state.approved')
+ - elsif custom_emoji.state == 2
+ = t('settings.request_custom_emoji.state.rejected')
+ - else
+ = 'error'
diff --git a/app/views/settings/request_custom_emojis/index.html.haml b/app/views/settings/request_custom_emojis/index.html.haml
new file mode 100644
index 00000000000000..b16cefd65437bf
--- /dev/null
+++ b/app/views/settings/request_custom_emojis/index.html.haml
@@ -0,0 +1,32 @@
+- content_for :page_title do
+ = t('settings.request_custom_emoji.title')
+
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :heading_actions do
+ = link_to t('settings.request_custom_emoji.upload'), new_settings_request_custom_emoji_path, class: 'button'
+
+= form_for(@form, url: batch_settings_request_custom_emojis_path) do |f|
+ = hidden_field_tag :page, params[:page] || 1
+
+ .batch-table.batch-table--simple
+ .batch-table__toolbar
+ %label.batch-table__toolbar__select.batch-checkbox-all
+ = check_box_tag :batch_checkbox_all, nil, false
+ .batch-table__toolbar__actions
+ - if @is_admin
+ = f.button safe_join([fa_icon('thumbs-up'), t('settings.request_custom_emoji.action.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+
+ = f.button safe_join([fa_icon('thumbs-down'), t('settings.request_custom_emoji.action.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+
+ = f.button safe_join([fa_icon('times'), t('admin.custom_emojis.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+
+ .batch-table__body
+ .batch-table__top_rid
+ - if @custom_emojis.empty?
+ = nothing_here 'nothing-here--under-tabs'
+ - else
+ = render partial: 'custom_emoji', collection: @custom_emojis, locals: { f: f }
+
+= paginate @custom_emojis
diff --git a/app/views/settings/request_custom_emojis/new.html.haml b/app/views/settings/request_custom_emojis/new.html.haml
new file mode 100644
index 00000000000000..e200cd577e1bb4
--- /dev/null
+++ b/app/views/settings/request_custom_emojis/new.html.haml
@@ -0,0 +1,13 @@
+- content_for :page_title do
+ = t('settings.request_custom_emoji.title')
+
+= simple_form_for @custom_emoji, url: settings_request_custom_emojis_path do |f|
+ = render 'shared/error_messages', object: @custom_emoji
+
+ .fields-group
+ = f.input :shortcode, wrapper: :with_label, label: t('admin.custom_emojis.shortcode'), hint: t('admin.custom_emojis.shortcode_hint')
+ .fields-group
+ = f.input :image, wrapper: :with_label, input_html: { accept: CustomEmoji::IMAGE_MIME_TYPES.join(' ') }, hint: t('admin.custom_emojis.image_hint', size: number_to_human_size(CustomEmoji::LIMIT))
+
+ .actions
+ = f.button :button, t('settings.request_custom_emoji.upload'), type: :submit
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 9d2abf0745eca9..b847e654692d16 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -3,6 +3,11 @@
require_relative '../../lib/mastodon/sidekiq_middleware'
Sidekiq.configure_server do |config|
+ if Rails.configuration.database_configuration.dig('production', 'adapter') == 'postgresql_makara'
+ STDERR.puts 'ERROR: Database replication is not currently supported in Sidekiq workers. Check your configuration.'
+ exit 1
+ end
+
config.redis = REDIS_SIDEKIQ_PARAMS
config.server_middleware do |chain|
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 6a72c1ca14d3a1..b2c9b7c4fc5825 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -1608,6 +1608,19 @@ ja:
preferences: ユーザー設定
profile: プロフィール
relationships: フォロー・フォロワー
+ request_custom_emoji:
+ action:
+ approve: 承認
+ reject: 不承認
+ errors:
+ already_exists: はすでにカスタム絵文字として存在します
+ created_msg: 絵文字の申請に成功しました!
+ state:
+ approved: 承認済
+ pending: 申請中
+ rejected: 不承認
+ title: カスタム絵文字の申請
+ upload: 申請
statuses_cleanup: 投稿の自動削除
strikes: モデレーションストライク
two_factor_authentication: 二要素認証
@@ -1701,6 +1714,9 @@ ja:
contrast: Mastodon (ハイコントラスト)
default: Mastodon (ダーク)
mastodon-light: Mastodon (ライト)
+ suumo: スーモ
+ gruvbox: gruvbox
+ c3: C3
time:
formats:
default: "%Y年%m月%d日 %H:%M"
diff --git a/config/navigation.rb b/config/navigation.rb
index e86c695a98db77..4cb72e247ef9fb 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -30,6 +30,7 @@
end
n.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? }
+ n.item :request_custom_emojis, safe_join([fa_icon('smile-o fw'), t('settings.request_custom_emoji.title')]), settings_request_custom_emojis_url
n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_path, if: -> { current_user.functional? }
n.item :trends, safe_join([fa_icon('fire fw'), t('admin.trends.title')]), admin_trends_statuses_path, if: -> { current_user.can?(:manage_taxonomies) } do |s|
diff --git a/config/routes/settings.rb b/config/routes/settings.rb
index 888fa9ecb55543..eb0e6e1bbe3805 100644
--- a/config/routes/settings.rb
+++ b/config/routes/settings.rb
@@ -71,4 +71,10 @@
resources :sessions, only: [:destroy]
resources :featured_tags, only: [:index, :create, :destroy]
resources :login_activities, only: [:index]
+
+ resources :request_custom_emojis, only: [:index, :new, :create] do
+ collection do
+ post :batch
+ end
+ end
end
diff --git a/config/themes.yml b/config/themes.yml
index 9c21c9459f3bcf..e01cc17c3cf882 100644
--- a/config/themes.yml
+++ b/config/themes.yml
@@ -1,3 +1,6 @@
default: styles/application.scss
contrast: styles/contrast.scss
mastodon-light: styles/mastodon-light.scss
+suumo: styles/suumo.scss
+gruvbox: styles/gruvbox.scss
+c3: styles/c3.scss
diff --git a/db/migrate/20221115101411_create_request_custom_emojis.rb b/db/migrate/20221115101411_create_request_custom_emojis.rb
new file mode 100644
index 00000000000000..d7227c6eca7228
--- /dev/null
+++ b/db/migrate/20221115101411_create_request_custom_emojis.rb
@@ -0,0 +1,17 @@
+class CreateRequestCustomEmojis < ActiveRecord::Migration[6.1]
+ def change
+ create_table :request_custom_emojis do |t|
+ t.integer :state, null: false, default: 0
+ t.string :shortcode, null: false, default: ''
+ t.string :image_file_name
+ t.string :image_content_type
+ t.integer :image_file_size
+ t.datetime :image_updated_at
+ t.integer :image_storage_schema_version
+ t.bigint :account_id
+
+ t.timestamps null: false
+ end
+ add_foreign_key :request_custom_emojis, :accounts, column: :account_id, primary_key: :id, on_update: :cascade, on_delete: :cascade, validate: false
+ end
+end
diff --git a/package.json b/package.json
index 35a236c8cc57ef..74a1f0b3ada5e2 100644
--- a/package.json
+++ b/package.json
@@ -88,6 +88,7 @@
"lodash": "^4.17.21",
"mark-loader": "^0.1.6",
"marky": "^1.2.5",
+ "material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^1.6.2",
"mkdirp": "^3.0.1",
"npmlog": "^7.0.1",
diff --git a/public/avatars/original/missing.png b/public/avatars/original/missing.png
index 781370782ecf61..aa21449ebcb99b 100644
Binary files a/public/avatars/original/missing.png and b/public/avatars/original/missing.png differ
diff --git a/public/robots.txt b/public/robots.txt
index 6672eeba1f22d7..64d64d64cb0487 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -6,3 +6,4 @@ Disallow: /
User-agent: *
Disallow: /media_proxy/
Disallow: /interact/
+Disallow: /web/
diff --git a/yarn.lock b/yarn.lock
index 70b791d8f82a94..5bef29bdf508cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8365,6 +8365,11 @@ mathml-tag-names@^2.1.3:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
+material-design-icons@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf"
+ integrity sha512-t19Z+QZBwSZulxptEu05kIm+UyfIdJY1JDwI+nx02j269m6W414whiQz9qfvQIiLrdx71RQv+T48nHhuQXOCIQ==
+
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"