diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a5f497ddb..9bbc09860e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
### Improvements
- クライアント: アニメーションを減らす設定をメニューのアニメーションにも適用するように
+- クライアント: MFM関数構文のサジェストを実装
- ActivityPub: HTML -> MFMの変換を強化
### Bugfixes
diff --git a/src/client/components/autocomplete.vue b/src/client/components/autocomplete.vue
index 065ee6de2e..e2c1af3356 100644
--- a/src/client/components/autocomplete.vue
+++ b/src/client/components/autocomplete.vue
@@ -10,12 +10,12 @@
{{ $ts.selectUser }}
-
+
-
{{ hashtag }}
-
+
-
@@ -24,6 +24,11 @@
({{ emoji.aliasOf }})
+
+ -
+ {{ tag }}
+
+
@@ -106,6 +111,8 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion
+const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
+
export default defineComponent({
props: {
type: {
@@ -137,11 +144,6 @@ export default defineComponent({
type: Number,
required: true,
},
-
- showing: {
- type: Boolean,
- required: true
- },
},
emits: ['done', 'closed'],
@@ -154,18 +156,11 @@ export default defineComponent({
hashtags: [],
emojis: [],
items: [],
+ mfmTags: [],
select: -1,
}
},
- watch: {
- showing() {
- if (!this.showing) {
- this.$emit('closed');
- }
- }
- },
-
updated() {
this.setPosition();
this.items = (this.$refs.suggests as Element | undefined)?.children || [];
@@ -236,7 +231,9 @@ export default defineComponent({
}
}
- if (this.type == 'user') {
+ console.log(this.type);
+
+ if (this.type === 'user') {
if (this.q == null) {
this.users = [];
this.fetching = false;
@@ -262,7 +259,7 @@ export default defineComponent({
sessionStorage.setItem(cacheKey, JSON.stringify(users));
});
}
- } else if (this.type == 'hashtag') {
+ } else if (this.type === 'hashtag') {
if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
@@ -286,7 +283,7 @@ export default defineComponent({
});
}
}
- } else if (this.type == 'emoji') {
+ } else if (this.type === 'emoji') {
if (this.q == null || this.q == '') {
// 最近使った絵文字をサジェスト
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
@@ -314,6 +311,14 @@ export default defineComponent({
}
this.emojis = matched;
+ } else if (this.type === 'mfmTag') {
+ console.log(this.q);
+ if (this.q == null || this.q == '') {
+ this.mfmTags = MFM_TAGS;
+ return;
+ }
+
+ this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
}
},
@@ -490,5 +495,11 @@ export default defineComponent({
margin: 0 0 0 8px;
}
}
+
+ > .mfmTags > li {
+
+ .name {
+ }
+ }
}
diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts
index c4bcc4b724..e952ad3907 100644
--- a/src/client/scripts/autocomplete.ts
+++ b/src/client/scripts/autocomplete.ts
@@ -70,11 +70,13 @@ export class Autocomplete {
const mentionIndex = text.lastIndexOf('@');
const hashtagIndex = text.lastIndexOf('#');
const emojiIndex = text.lastIndexOf(':');
+ const mfmTagIndex = text.lastIndexOf('$');
const max = Math.max(
mentionIndex,
hashtagIndex,
- emojiIndex);
+ emojiIndex,
+ mfmTagIndex);
if (max == -1) {
this.close();
@@ -83,6 +85,7 @@ export class Autocomplete {
const isMention = mentionIndex != -1;
const isHashtag = hashtagIndex != -1;
+ const isMfmTag = mfmTagIndex != -1;
const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
let opened = false;
@@ -114,6 +117,14 @@ export class Autocomplete {
}
}
+ if (isMfmTag && !opened) {
+ const mfmTag = text.substr(mfmTagIndex + 1);
+ if (!mfmTag.includes(' ')) {
+ this.open('mfmTag', mfmTag);
+ opened = true;
+ }
+ }
+
if (!opened) {
this.close();
}
@@ -244,6 +255,22 @@ export class Autocomplete {
const pos = trimmedBefore.length + value.length;
this.textarea.setSelectionRange(pos, pos);
});
+ } else if (type == 'mfmTag') {
+ const source = this.text;
+
+ const before = source.substr(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
+ const after = source.substr(caret);
+
+ // 挿入
+ this.text = `${trimmedBefore}$[${value} ]${after}`;
+
+ // キャレットを戻す
+ this.vm.$nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + (value.length + 3);
+ this.textarea.setSelectionRange(pos, pos);
+ });
}
}
}