diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index bc0120c9ab..d46d536659 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -14,7 +14,8 @@
-
- {{ emoji.emoji }}
+
+ {{ emoji.emoji }}
({{ emoji.alias }})
@@ -169,22 +170,45 @@ export default Vue.extend({
}
} else if (this.type == 'emoji') {
const matched = [];
+ const max = 30;
+
+ const customEmojis = (this.os.getMetaSync() || { emojis: [] }).emojis;
+ customEmojis.some(x => {
+ if (x.name.startsWith(this.q)) matched.push({
+ name: x.name,
+ emoji: `:${x.name}:`,
+ url: x.url
+ });
+ return matched.length == max;
+ });
+ customEmojis.some(x => {
+ const alias = (x.aliases || []).find(a => a.startsWith(this.q));
+ if (alias) matched.push({
+ alias: x.name,
+ name: alias,
+ emoji: `:${x.name}:`,
+ url: x.url
+ });
+ return matched.length == max;
+ });
+
emjdb.some(x => {
if (x.name.indexOf(this.q) == 0 && !x.alias && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
- return matched.length == 30;
+ return matched.length == max;
});
- if (matched.length < 30) {
+ if (matched.length < max) {
emjdb.some(x => {
if (x.name.indexOf(this.q) == 0 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
- return matched.length == 30;
+ return matched.length == max;
});
}
- if (matched.length < 30) {
+ if (matched.length < max) {
emjdb.some(x => {
if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
- return matched.length == 30;
+ return matched.length == max;
});
}
+
this.emojis = matched;
}
},
@@ -340,6 +364,10 @@ export default Vue.extend({
margin 0 4px 0 0
width 24px
+ > img
+ width 24px
+ vertical-align bottom
+
.name
color var(--autocompleteItemText)
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts
index d36a0a3714..c45bb1022e 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts
@@ -3,7 +3,6 @@ import * as emojilib from 'emojilib';
import { length } from 'stringz';
import parse from '../../../../../mfm/parse';
import getAcct from '../../../../../misc/acct/render';
-import { url } from '../../../config';
import MkUrl from './url.vue';
import MkGoogle from './google.vue';
import { concat } from '../../../../../prelude/array';
@@ -186,6 +185,21 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'emoji': {
+ //#region カスタム絵文字
+ const customEmojis = (this.os.getMetaSync() || { emojis: [] }).emojis;
+ const customEmoji = customEmojis.find(e => e.name == token.emoji || (e.aliases || []).includes(token.emoji));
+ if (customEmoji) {
+ return [createElement('img', {
+ attrs: {
+ src: customEmoji.url,
+ alt: token.emoji,
+ title: token.emoji,
+ style: 'height: 2.5em; vertical-align: middle;'
+ }
+ })];
+ }
+ //#endregion
+
const emoji = emojilib.lib[token.emoji];
return [createElement('span', emoji ? emoji.char : token.content)];
}
diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts
index e2cc64d79f..1a5c5daedc 100644
--- a/src/client/app/common/views/directives/autocomplete.ts
+++ b/src/client/app/common/views/directives/autocomplete.ts
@@ -222,13 +222,15 @@ class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
const after = source.substr(caret);
+ if (value.startsWith(':')) value = value + ' ';
+
// 挿入
this.text = trimmedBefore + value + after;
// キャレットを戻す
this.vm.$nextTick(() => {
this.textarea.focus();
- const pos = trimmedBefore.length + 1;
+ const pos = trimmedBefore.length + (value.startsWith(':') ? value.length : 1);
this.textarea.setSelectionRange(pos, pos);
});
}
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index d50c4e15b2..9c351e9041 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -510,6 +510,14 @@ export default class MiOS extends EventEmitter {
return promise;
}
+ /**
+ * Misskeyのメタ情報を取得します
+ */
+ @autobind
+ public getMetaSync() {
+ return this.meta ? this.meta.data : null;
+ }
+
/**
* Misskeyのメタ情報を取得します
* @param force キャッシュを無視するか否か
diff --git a/src/models/meta.ts b/src/models/meta.ts
index 3c0347485c..7ecb4990d4 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -15,4 +15,24 @@ export type IMeta = {
disableLocalTimeline?: boolean;
hidedTags?: string[];
bannerUrl?: string;
+
+ /**
+ * カスタム絵文字定義
+ */
+ emojis?: {
+ /**
+ * 絵文字名 (例: thinking_ai)
+ */
+ name: string;
+
+ /**
+ * エイリアス
+ */
+ aliases?: string[];
+
+ /**
+ * 絵文字画像のURL
+ */
+ url: string;
+ }[];
};
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index f0ebfbe936..f45efd7fe5 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -17,6 +17,12 @@ export const meta = {
}
}),
+ emojis: $.arr($.obj()).optional.note({
+ desc: {
+ 'ja-JP': 'カスタム絵文字定義'
+ }
+ }),
+
disableRegistration: $.bool.optional.nullable.note({
desc: {
'ja-JP': '招待制か否か'
@@ -53,6 +59,10 @@ export default (params: any) => new Promise(async (res, rej) => {
set.broadcasts = ps.broadcasts;
}
+ if (ps.emojis) {
+ set.emojis = ps.emojis;
+ }
+
if (typeof ps.disableRegistration === 'boolean') {
set.disableRegistration = ps.disableRegistration;
}
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 0cd5842312..8bc0293832 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -50,6 +50,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined,
bannerUrl: meta.bannerUrl,
maxNoteTextLength: config.maxNoteTextLength,
+ emojis: meta.emojis,
features: {
registration: !meta.disableRegistration,