Custom reaction (#4517)
* Custom reaction
* increase limit of reactions/delete
* リアクションの場合は OS標準の絵文字を使用 を迂回する
* カスタムリアクションを無効にする設定
* fix
* disableCustomReaction --> enableEmojiReaction
* Avoid MFM rendering
* 🎨
* 🎨
* Auto accept
* custom emoji reaction
* Improve usability
* Extract emojiRegex
* Fix
* Clean up
* 🎨
* 🎨
* toDbReaction で reaction は必須に
あとフォールバックは like に
* Clean up
* Make required
* 3eb08748fe (r266241728)
* Refactor
* Allow null
This commit is contained in:
parent
a5b12bac54
commit
2684541693
19 changed files with 278 additions and 44 deletions
|
@ -1238,6 +1238,7 @@ admin/views/instance.vue:
|
||||||
disable-local-timeline: "ローカルタイムラインを無効にする"
|
disable-local-timeline: "ローカルタイムラインを無効にする"
|
||||||
disable-global-timeline: "グローバルタイムラインを無効にする"
|
disable-global-timeline: "グローバルタイムラインを無効にする"
|
||||||
disabling-timelines-info: "これらのタイムラインを無効にしても、管理者およびモデレーターは引き続き利用できます。"
|
disabling-timelines-info: "これらのタイムラインを無効にしても、管理者およびモデレーターは引き続き利用できます。"
|
||||||
|
enable-emoji-reaction: "リアクションに絵文字を使えるようにする"
|
||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
|
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
|
||||||
<ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch>
|
<ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch>
|
||||||
<ui-info>{{ $t('disabling-timelines-info') }}</ui-info>
|
<ui-info>{{ $t('disabling-timelines-info') }}</ui-info>
|
||||||
|
<ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch>
|
||||||
</section>
|
</section>
|
||||||
<section class="fit-bottom">
|
<section class="fit-bottom">
|
||||||
<header><fa icon="cloud"/> {{ $t('drive-config') }}</header>
|
<header><fa icon="cloud"/> {{ $t('drive-config') }}</header>
|
||||||
|
@ -155,6 +156,7 @@ export default Vue.extend({
|
||||||
disableRegistration: false,
|
disableRegistration: false,
|
||||||
disableLocalTimeline: false,
|
disableLocalTimeline: false,
|
||||||
disableGlobalTimeline: false,
|
disableGlobalTimeline: false,
|
||||||
|
enableEmojiReaction: true,
|
||||||
mascotImageUrl: null,
|
mascotImageUrl: null,
|
||||||
bannerUrl: null,
|
bannerUrl: null,
|
||||||
errorImageUrl: null,
|
errorImageUrl: null,
|
||||||
|
@ -206,6 +208,7 @@ export default Vue.extend({
|
||||||
this.disableRegistration = meta.disableRegistration;
|
this.disableRegistration = meta.disableRegistration;
|
||||||
this.disableLocalTimeline = meta.disableLocalTimeline;
|
this.disableLocalTimeline = meta.disableLocalTimeline;
|
||||||
this.disableGlobalTimeline = meta.disableGlobalTimeline;
|
this.disableGlobalTimeline = meta.disableGlobalTimeline;
|
||||||
|
this.enableEmojiReaction = meta.enableEmojiReaction;
|
||||||
this.mascotImageUrl = meta.mascotImageUrl;
|
this.mascotImageUrl = meta.mascotImageUrl;
|
||||||
this.bannerUrl = meta.bannerUrl;
|
this.bannerUrl = meta.bannerUrl;
|
||||||
this.errorImageUrl = meta.errorImageUrl;
|
this.errorImageUrl = meta.errorImageUrl;
|
||||||
|
@ -267,6 +270,7 @@ export default Vue.extend({
|
||||||
disableRegistration: this.disableRegistration,
|
disableRegistration: this.disableRegistration,
|
||||||
disableLocalTimeline: this.disableLocalTimeline,
|
disableLocalTimeline: this.disableLocalTimeline,
|
||||||
disableGlobalTimeline: this.disableGlobalTimeline,
|
disableGlobalTimeline: this.disableGlobalTimeline,
|
||||||
|
enableEmojiReaction: this.enableEmojiReaction,
|
||||||
mascotImageUrl: this.mascotImageUrl,
|
mascotImageUrl: this.mascotImageUrl,
|
||||||
bannerUrl: this.bannerUrl,
|
bannerUrl: this.bannerUrl,
|
||||||
errorImageUrl: this.errorImageUrl,
|
errorImageUrl: this.errorImageUrl,
|
||||||
|
|
|
@ -29,7 +29,11 @@ export default Vue.extend({
|
||||||
customEmojis: {
|
customEmojis: {
|
||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
},
|
||||||
|
isReaction: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -46,7 +50,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
useOsDefaultEmojis(): boolean {
|
useOsDefaultEmojis(): boolean {
|
||||||
return this.$store.state.device.useOsDefaultEmojis;
|
return this.$store.state.device.useOsDefaultEmojis && !this.isReaction;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="mk-reaction-icon">
|
<mk-emoji :emoji="str.startsWith(':') ? null : str" :name="str.startsWith(':') ? str.substr(1, str.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true"/>
|
||||||
<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" :alt="$t('@.reactions.like')">
|
|
||||||
<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" :alt="$t('@.reactions.love')">
|
|
||||||
<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" :alt="$t('@.reactions.laugh')">
|
|
||||||
<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" :alt="$t('@.reactions.hmm')">
|
|
||||||
<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" :alt="$t('@.reactions.surprise')">
|
|
||||||
<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" :alt="$t('@.reactions.congrats')">
|
|
||||||
<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" :alt="$t('@.reactions.angry')">
|
|
||||||
<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" :alt="$t('@.reactions.confused')">
|
|
||||||
<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" :alt="$t('@.reactions.rip')">
|
|
||||||
<template v-if="reaction == 'pudding'">
|
|
||||||
<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" :alt="$t('@.reactions.pudding')">
|
|
||||||
<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" :alt="$t('@.reactions.pudding')">
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -21,7 +7,34 @@ import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n(),
|
i18n: i18n(),
|
||||||
props: ['reaction']
|
props: {
|
||||||
|
reaction: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
customEmojis: (this.$root.getMetaSync() || { emojis: [] }).emojis || []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
str(): any {
|
||||||
|
switch (this.reaction) {
|
||||||
|
case 'like': return '👍';
|
||||||
|
case 'love': return '❤';
|
||||||
|
case 'laugh': return '😆';
|
||||||
|
case 'hmm': return '🤔';
|
||||||
|
case 'surprise': return '😮';
|
||||||
|
case 'congrats': return '🎉';
|
||||||
|
case 'angry': return '💢';
|
||||||
|
case 'confused': return '😥';
|
||||||
|
case 'rip': return '😇';
|
||||||
|
case 'pudding': return (this.$store.getters.isSignedIn && this.$store.state.settings.iLikeSushi) ? '🍣' : '🍮';
|
||||||
|
default: return this.reaction;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||||
<div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover">
|
<div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover">
|
||||||
<p v-if="!$root.isMobile">{{ title }}</p>
|
<p v-if="!$root.isMobile">{{ title }}</p>
|
||||||
<div ref="buttons" :class="{ showFocus }">
|
<div class="buttons" ref="buttons" :class="{ showFocus }">
|
||||||
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" :title="$t('@.reactions.like')" v-particle><mk-reaction-icon reaction="like"/></button>
|
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" :title="$t('@.reactions.like')" v-particle><mk-reaction-icon reaction="like"/></button>
|
||||||
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" :title="$t('@.reactions.love')" v-particle><mk-reaction-icon reaction="love"/></button>
|
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" :title="$t('@.reactions.love')" v-particle><mk-reaction-icon reaction="love"/></button>
|
||||||
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" :title="$t('@.reactions.laugh')" v-particle><mk-reaction-icon reaction="laugh"/></button>
|
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" :title="$t('@.reactions.laugh')" v-particle><mk-reaction-icon reaction="laugh"/></button>
|
||||||
|
@ -15,6 +15,9 @@
|
||||||
<button @click="react('rip')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="9" :title="$t('@.reactions.rip')" v-particle><mk-reaction-icon reaction="rip"/></button>
|
<button @click="react('rip')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="9" :title="$t('@.reactions.rip')" v-particle><mk-reaction-icon reaction="rip"/></button>
|
||||||
<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" :title="$t('@.reactions.pudding')" v-particle><mk-reaction-icon reaction="pudding"/></button>
|
<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" :title="$t('@.reactions.pudding')" v-particle><mk-reaction-icon reaction="pudding"/></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="enableEmojiReaction" class="text">
|
||||||
|
<input v-model="text" placeholder="または絵文字を入力" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -23,6 +26,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import anime from 'animejs';
|
import anime from 'animejs';
|
||||||
|
import { emojiRegex } from '../../../../../misc/emoji-regex';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/components/reaction-picker.vue'),
|
i18n: i18n('common/views/components/reaction-picker.vue'),
|
||||||
|
@ -56,6 +60,8 @@ export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
title: this.$t('choose-reaction'),
|
title: this.$t('choose-reaction'),
|
||||||
|
text: null,
|
||||||
|
enableEmojiReaction: false,
|
||||||
focus: null
|
focus: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -94,6 +100,10 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$root.getMeta().then(meta => {
|
||||||
|
this.enableEmojiReaction = meta.enableEmojiReaction;
|
||||||
|
});
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.focus = 0;
|
this.focus = 0;
|
||||||
|
|
||||||
|
@ -143,6 +153,17 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reactText() {
|
||||||
|
if (!this.text) return;
|
||||||
|
this.react(this.text);
|
||||||
|
},
|
||||||
|
|
||||||
|
tryReactText() {
|
||||||
|
if (!this.text) return;
|
||||||
|
if (!this.text.match(emojiRegex)) return;
|
||||||
|
this.reactText();
|
||||||
|
},
|
||||||
|
|
||||||
onMouseover(e) {
|
onMouseover(e) {
|
||||||
this.title = e.target.title;
|
this.title = e.target.title;
|
||||||
},
|
},
|
||||||
|
@ -256,9 +277,9 @@ export default Vue.extend({
|
||||||
color var(--popupFg)
|
color var(--popupFg)
|
||||||
border-bottom solid var(--lineWidth) var(--faceDivider)
|
border-bottom solid var(--lineWidth) var(--faceDivider)
|
||||||
|
|
||||||
> div
|
> .buttons
|
||||||
padding 4px
|
padding 4px
|
||||||
width 240px
|
width 216px
|
||||||
text-align center
|
text-align center
|
||||||
|
|
||||||
&.showFocus
|
&.showFocus
|
||||||
|
@ -283,6 +304,9 @@ export default Vue.extend({
|
||||||
font-size 24px
|
font-size 24px
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
|
||||||
|
> *
|
||||||
|
height 1em
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background var(--reactionPickerButtonHoverBg)
|
background var(--reactionPickerButtonHoverBg)
|
||||||
|
|
||||||
|
@ -290,4 +314,29 @@ export default Vue.extend({
|
||||||
background var(--primary)
|
background var(--primary)
|
||||||
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
|
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
|
||||||
|
|
||||||
|
> .text
|
||||||
|
width 216px
|
||||||
|
padding 4px 8px 8px 8px
|
||||||
|
|
||||||
|
> input
|
||||||
|
width 100%
|
||||||
|
padding 10px
|
||||||
|
margin 0
|
||||||
|
text-align center
|
||||||
|
font-size 16px
|
||||||
|
color var(--desktopPostFormTextareaFg)
|
||||||
|
background var(--desktopPostFormTextareaBg)
|
||||||
|
outline none
|
||||||
|
border solid 1px var(--primaryAlpha01)
|
||||||
|
border-radius 4px
|
||||||
|
transition border-color .2s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color var(--primaryAlpha02)
|
||||||
|
transition border-color .1s ease
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
border-color var(--primaryAlpha05)
|
||||||
|
transition border-color 0s ease
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -136,12 +136,8 @@ export default Vue.extend({
|
||||||
&:hover
|
&:hover
|
||||||
background var(--reactionViewerButtonHoverBg)
|
background var(--reactionViewerButtonHoverBg)
|
||||||
|
|
||||||
> .mk-reaction-icon
|
|
||||||
font-size 1.4em
|
|
||||||
|
|
||||||
> span
|
> span
|
||||||
font-size 1.1em
|
font-size 1.1em
|
||||||
line-height 32px
|
line-height 32px
|
||||||
vertical-align middle
|
|
||||||
color var(--text)
|
color var(--text)
|
||||||
</style>
|
</style>
|
||||||
|
|
File diff suppressed because one or more lines are too long
1
src/misc/emoji-regex.ts
Normal file
1
src/misc/emoji-regex.ts
Normal file
File diff suppressed because one or more lines are too long
|
@ -13,6 +13,7 @@ const defaultMeta: any = {
|
||||||
originalUsersCount: 0
|
originalUsersCount: 0
|
||||||
},
|
},
|
||||||
maxNoteTextLength: 1000,
|
maxNoteTextLength: 1000,
|
||||||
|
enableEmojiReaction: true,
|
||||||
enableTwitterIntegration: false,
|
enableTwitterIntegration: false,
|
||||||
enableGithubIntegration: false,
|
enableGithubIntegration: false,
|
||||||
enableDiscordIntegration: false,
|
enableDiscordIntegration: false,
|
||||||
|
|
59
src/misc/reaction-lib.ts
Normal file
59
src/misc/reaction-lib.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import Emoji from '../models/emoji';
|
||||||
|
import { emojiRegex } from './emoji-regex';
|
||||||
|
|
||||||
|
const basic10: Record<string, string> = {
|
||||||
|
'👍': 'like',
|
||||||
|
'❤': 'love', // ここに記述する場合は異体字セレクタを入れない
|
||||||
|
'😆': 'laugh',
|
||||||
|
'🤔': 'hmm',
|
||||||
|
'😮': 'surprise',
|
||||||
|
'🎉': 'congrats',
|
||||||
|
'💢': 'angry',
|
||||||
|
'😥': 'confused',
|
||||||
|
'😇': 'rip',
|
||||||
|
'🍮': 'pudding',
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getFallbackReaction(): Promise<string> {
|
||||||
|
return 'like';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toDbReaction(reaction: string, enableEmoji = true): Promise<string> {
|
||||||
|
if (reaction == null) return await getFallbackReaction();
|
||||||
|
|
||||||
|
// 既存の文字列リアクションはそのまま
|
||||||
|
if (Object.values(basic10).includes(reaction)) return reaction;
|
||||||
|
|
||||||
|
if (!enableEmoji) return await getFallbackReaction();
|
||||||
|
|
||||||
|
// Unicode絵文字
|
||||||
|
const match = emojiRegex.exec(reaction);
|
||||||
|
if (match) {
|
||||||
|
// 合字を含む1つの絵文字
|
||||||
|
const unicode = match[0];
|
||||||
|
|
||||||
|
// 異体字セレクタ除去後の絵文字
|
||||||
|
const normalized = unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
||||||
|
|
||||||
|
// Unicodeプリンは寿司化不能とするため文字列化しない
|
||||||
|
if (normalized === '🍮') return normalized;
|
||||||
|
|
||||||
|
// プリン以外の既存のリアクションは文字列化する
|
||||||
|
if (basic10[normalized]) return basic10[normalized];
|
||||||
|
|
||||||
|
// それ以外はUnicodeのまま
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const custom = reaction.match(/:([\w+-]+):/);
|
||||||
|
if (custom) {
|
||||||
|
const emoji = await Emoji.findOne({
|
||||||
|
host: null,
|
||||||
|
name: custom[1],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emoji) return reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getFallbackReaction();
|
||||||
|
}
|
|
@ -194,6 +194,7 @@ export type IMeta = {
|
||||||
disableRegistration?: boolean;
|
disableRegistration?: boolean;
|
||||||
disableLocalTimeline?: boolean;
|
disableLocalTimeline?: boolean;
|
||||||
disableGlobalTimeline?: boolean;
|
disableGlobalTimeline?: boolean;
|
||||||
|
enableEmojiReaction?: boolean;
|
||||||
hidedTags?: string[];
|
hidedTags?: string[];
|
||||||
mascotImageUrl?: string;
|
mascotImageUrl?: string;
|
||||||
bannerUrl?: string;
|
bannerUrl?: string;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { packMany as packFileMany, IDriveFile } from './drive-file';
|
||||||
import Following from './following';
|
import Following from './following';
|
||||||
import Emoji from './emoji';
|
import Emoji from './emoji';
|
||||||
import { dbLogger } from '../db/logger';
|
import { dbLogger } from '../db/logger';
|
||||||
|
import { unique, concat } from '../prelude/array';
|
||||||
|
|
||||||
const Note = db.get<INote>('notes');
|
const Note = db.get<INote>('notes');
|
||||||
Note.createIndex('uri', { sparse: true, unique: true });
|
Note.createIndex('uri', { sparse: true, unique: true });
|
||||||
|
@ -242,6 +243,11 @@ export const pack = async (
|
||||||
|
|
||||||
const id = _note._id;
|
const id = _note._id;
|
||||||
|
|
||||||
|
// Some counts
|
||||||
|
_note.renoteCount = _note.renoteCount || 0;
|
||||||
|
_note.repliesCount = _note.repliesCount || 0;
|
||||||
|
_note.reactionCounts = _note.reactionCounts || {};
|
||||||
|
|
||||||
// _note._userを消す前か、_note.userを解決した後でないとホストがわからない
|
// _note._userを消す前か、_note.userを解決した後でないとホストがわからない
|
||||||
if (_note._user) {
|
if (_note._user) {
|
||||||
const host = _note._user.host;
|
const host = _note._user.host;
|
||||||
|
@ -253,6 +259,8 @@ export const pack = async (
|
||||||
fields: { _id: false }
|
fields: { _id: false }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
_note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts)]));
|
||||||
|
|
||||||
_note.emojis = Emoji.find({
|
_note.emojis = Emoji.find({
|
||||||
name: { $in: _note.emojis },
|
name: { $in: _note.emojis },
|
||||||
host: host
|
host: host
|
||||||
|
@ -290,11 +298,6 @@ export const pack = async (
|
||||||
// Populate files
|
// Populate files
|
||||||
_note.files = packFileMany(_note.fileIds || []);
|
_note.files = packFileMany(_note.fileIds || []);
|
||||||
|
|
||||||
// Some counts
|
|
||||||
_note.renoteCount = _note.renoteCount || 0;
|
|
||||||
_note.repliesCount = _note.repliesCount || 0;
|
|
||||||
_note.reactionCounts = _note.reactionCounts || {};
|
|
||||||
|
|
||||||
// 後方互換性のため
|
// 後方互換性のため
|
||||||
_note.mediaIds = _note.fileIds;
|
_note.mediaIds = _note.fileIds;
|
||||||
_note.media = _note.files;
|
_note.media = _note.files;
|
||||||
|
|
|
@ -3,7 +3,6 @@ import Note from '../../../models/note';
|
||||||
import { IRemoteUser } from '../../../models/user';
|
import { IRemoteUser } from '../../../models/user';
|
||||||
import { ILike } from '../type';
|
import { ILike } from '../type';
|
||||||
import create from '../../../services/note/reaction/create';
|
import create from '../../../services/note/reaction/create';
|
||||||
import { validateReaction } from '../../../models/note-reaction';
|
|
||||||
|
|
||||||
export default async (actor: IRemoteUser, activity: ILike) => {
|
export default async (actor: IRemoteUser, activity: ILike) => {
|
||||||
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
||||||
|
@ -18,12 +17,5 @@ export default async (actor: IRemoteUser, activity: ILike) => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
let reaction = 'like';
|
await create(actor, note, activity._misskey_reaction);
|
||||||
|
|
||||||
// 他のMisskeyインスタンスからのリアクション
|
|
||||||
if (activity._misskey_reaction && validateReaction.ok(activity._misskey_reaction)) {
|
|
||||||
reaction = activity._misskey_reaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
await create(actor, note, reaction);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,6 +41,13 @@ export const meta = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enableEmojiReaction: {
|
||||||
|
validator: $.optional.nullable.bool,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '絵文字リアクションを有効にするか否か'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
hidedTags: {
|
hidedTags: {
|
||||||
validator: $.optional.nullable.arr($.str),
|
validator: $.optional.nullable.arr($.str),
|
||||||
desc: {
|
desc: {
|
||||||
|
@ -351,6 +358,10 @@ export default define(meta, async (ps) => {
|
||||||
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof ps.enableEmojiReaction === 'boolean') {
|
||||||
|
set.enableEmojiReaction = ps.enableEmojiReaction;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(ps.hidedTags)) {
|
if (Array.isArray(ps.hidedTags)) {
|
||||||
set.hidedTags = ps.hidedTags;
|
set.hidedTags = ps.hidedTags;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether disabled GTL.',
|
description: 'Whether disabled GTL.',
|
||||||
},
|
},
|
||||||
|
enableEmojiReaction: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether enabled emoji reaction.',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -107,6 +111,7 @@ export default define(meta, async (ps, me) => {
|
||||||
disableRegistration: instance.disableRegistration,
|
disableRegistration: instance.disableRegistration,
|
||||||
disableLocalTimeline: instance.disableLocalTimeline,
|
disableLocalTimeline: instance.disableLocalTimeline,
|
||||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||||
|
enableEmojiReaction: instance.enableEmojiReaction,
|
||||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import ID, { transform } from '../../../../../misc/cafy-id';
|
import ID, { transform } from '../../../../../misc/cafy-id';
|
||||||
import createReaction from '../../../../../services/note/reaction/create';
|
import createReaction from '../../../../../services/note/reaction/create';
|
||||||
import { validateReaction } from '../../../../../models/note-reaction';
|
|
||||||
import define from '../../../define';
|
import define from '../../../define';
|
||||||
import { getNote } from '../../../common/getters';
|
import { getNote } from '../../../common/getters';
|
||||||
import { ApiError } from '../../../error';
|
import { ApiError } from '../../../error';
|
||||||
|
@ -30,7 +29,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
reaction: {
|
reaction: {
|
||||||
validator: $.str.pipe(validateReaction.ok),
|
validator: $.str,
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'リアクションの種類'
|
'ja-JP': 'リアクションの種類'
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const meta = {
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 5,
|
max: 60,
|
||||||
minInterval: ms('3sec')
|
minInterval: ms('3sec')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { deliver } from '../../../queue';
|
||||||
import { renderActivity } from '../../../remote/activitypub/renderer';
|
import { renderActivity } from '../../../remote/activitypub/renderer';
|
||||||
import perUserReactionsChart from '../../../services/chart/per-user-reactions';
|
import perUserReactionsChart from '../../../services/chart/per-user-reactions';
|
||||||
import { IdentifiableError } from '../../../misc/identifiable-error';
|
import { IdentifiableError } from '../../../misc/identifiable-error';
|
||||||
|
import { toDbReaction } from '../../../misc/reaction-lib';
|
||||||
|
import fetchMeta from '../../../misc/fetch-meta';
|
||||||
|
|
||||||
export default async (user: IUser, note: INote, reaction: string) => {
|
export default async (user: IUser, note: INote, reaction: string) => {
|
||||||
// Myself
|
// Myself
|
||||||
|
@ -17,6 +19,9 @@ export default async (user: IUser, note: INote, reaction: string) => {
|
||||||
throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
|
throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
reaction = await toDbReaction(reaction, meta.enableEmojiReaction);
|
||||||
|
|
||||||
// Create reaction
|
// Create reaction
|
||||||
await NoteReaction.insert({
|
await NoteReaction.insert({
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
|
91
test/reaction-lib.ts
Normal file
91
test/reaction-lib.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Tests of MFM
|
||||||
|
*
|
||||||
|
* How to run the tests:
|
||||||
|
* > mocha test/reaction-lib.ts --require ts-node/register
|
||||||
|
*
|
||||||
|
* To specify test:
|
||||||
|
* > mocha test/reaction-lib.ts --require ts-node/register -g 'test name'
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
import { toDbReaction } from '../src/misc/reaction-lib';
|
||||||
|
|
||||||
|
describe('toDbReaction', async () => {
|
||||||
|
it('既存の文字列リアクションはそのまま', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('like'), 'like');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('🍮'), '🍮');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する like', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('👍'), 'like');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する love', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('❤️'), 'love');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('❤'), 'love');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する laugh', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('😆'), 'laugh');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する hmm', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('🤔'), 'hmm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する surprise', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('😮'), 'surprise');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する congrats', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('🎉'), 'congrats');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する angry', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('💢'), 'angry');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する confused', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('😥'), 'confused');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('プリン以外の既存のリアクションは文字列化する rip', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('😇'), 'rip');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('それ以外はUnicodeのまま', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('🍅'), '🍅');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('異体字セレクタ除去', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('㊗️'), '㊗');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('異体字セレクタ除去 必要なし', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('㊗'), '㊗');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fallback - undefined', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction(undefined), 'like');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fallback - null', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction(null), 'like');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fallback - empty', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction(''), 'like');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fallback - unknown', async () => {
|
||||||
|
assert.strictEqual(await toDbReaction('unknown'), 'like');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue