diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad55752b1f834f2bf53130296ba9081a4bd5197..4038755124ba48e11137822142cec1b3d2749f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - Enhance: プラグインã§`Plugin:register_note_view_interruptor`を用ã„ã¦noteã®ä»£ã‚ã‚Šã«nullã‚’è¿”å´ã™ã‚‹ã“ã¨ã§ãƒŽãƒ¼ãƒˆã‚’éžè¡¨ç¤ºã«ã§ãるよã†ã«ãªã‚Šã¾ã—㟠- Enhance: AiScript関数`Mk:nyaize()`ãŒè¿½åŠ ã•ã‚Œã¾ã—㟠- Enhance: æƒ…å ±â†’ãƒ„ãƒ¼ãƒ« ã¯ãƒŠãƒ“ゲーションãƒãƒ¼ã«ãƒ„ールã¨ã—ã¦ç‹¬ç«‹ã—ãŸé …ç›®ã«ãªã‚Šã¾ã—㟠+- Enhance: ノート内ã®ã‚«ã‚¹ã‚¿ãƒ 絵文å—をクリックã™ã‚‹ã“ã¨ã§ã€ã‚³ãƒ”ーãŠã‚ˆã³ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ãŒã§ãるよã†ã« - Enhance: ãã®ä»–ç´°ã‹ãªãƒ–ラッシュアップ - Fix: 投稿フォームã§ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼å¤‰æ›´ãŒãƒ—レビューã«åæ˜ ã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ - Fix: ユーザーページ㮠ノート > ファイル付ã タブã«ãƒªãƒ—ライãŒè¡¨ç¤ºã•ã‚Œã¦ã—ã¾ã† diff --git a/locales/index.d.ts b/locales/index.d.ts index 50b11acc06953766a1199e45ea4f831f02700798..aedaaa9f7c828c623cf1560fba2c159abf9eae1c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1160,6 +1160,7 @@ export interface Locale { "useGroupedNotifications": string; "signupPendingError": string; "cwNotationRequired": string; + "doReaction": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index de4e8ce2b38a1d15bd3826bff3125f6e8a145085..6ecebfc393c7ff9c51a94aefc8efc726637a2b0e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1157,6 +1157,7 @@ disableStreamingTimeline: "タイムラインã®ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ æ›´æ–°ã‚’ç„¡ useGroupedNotifications: "通知をグルーピングã—ã¦è¡¨ç¤ºã™ã‚‹" signupPendingError: "メールアドレスã®ç¢ºèªä¸ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚リンクã®æœ‰åŠ¹æœŸé™ãŒåˆ‡ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚" cwNotationRequired: "ã€Œå†…å®¹ã‚’éš ã™ã€ãŒã‚ªãƒ³ã®å ´åˆã¯æ³¨é‡ˆã®è¨˜è¿°ãŒå¿…è¦ã§ã™ã€‚" +doReaction: "リアクションã™ã‚‹" _announcement: forExistingUsers: "æ—¢å˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿" diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 30a68f38f2bbf2cd70fda6390d0a7e0578393bfa..0ae3423a210692fc13e2ab6ce79daa42af3978fa 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -53,19 +53,28 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div :class="$style.text"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm + v-if="appearNote.text" + :parsedNodes="parsed" + :text="appearNote.text" + :author="appearNote.user" + :nyaize="'account'" + :emojiUrls="appearNote.emojis" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + /> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> </div> </div> </div> @@ -242,6 +251,13 @@ const keymap = { 's': () => showContent.value !== showContent.value, }; +provide('react', (reaction: string) => { + os.api('notes/reactions/create', { + noteId: appearNote.id, + reaction: reaction, + }); +}); + if (props.mock) { watch(() => props.note, (to) => { note = deepClone(to); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 9e9b1035d7e5725fda5792284237285cc43264a6..ff2941344ebc9c4b40525fd99cc1bbbf7474da49 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -67,19 +67,19 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> <MkCwButton v-model="showContent" :note="appearNote"/> </p> <div v-show="appearNote.cw == null || showContent"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> </div> </div> <div v-if="appearNote.files.length > 0"> diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index dc401a7ecbb0037537a339090f4d69dd535a16aa..28b00af246f09ff66c37a7bd71bec8dfbd5dc7a1 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i" :emojiUrls="note.emojis"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 3cc87670074d8674f291b52932eb171f8b544d1b..f61b963d9b4412b5f538f7e3d4766e9e3fc08ae5 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 51dabee1614ea11a17f5c56ff41bea7a04f3ab3b..e9f2b838d2fb92ef5a65a1a3dc1250f5a108e8f2 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/> + <Mfm v-if="note.text" :text="note.text" :author="note.user" :emojiUrls="note.emojis"/> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files.length > 0"> diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index c13ef60f3bd1cfe2f275ea4d42b05465accf5a2e..eaebbf03e74187ab45815f052abf2fcce104f786 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> <div :class="$style.description"> <div v-if="user.description" :class="$style.mfm"> - <Mfm :text="user.description" :author="user" :i="$i"/> + <Mfm :text="user.description" :author="user"/> </div> <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> </div> diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index bcba4196b55f8a340e100ec8b9a459d5ae291c63..20eb9b3e9301632bd11a3eec0a524a052d69c085 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.username"><MkAcct :user="user"/></div> </div> <div :class="$style.description"> - <Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/> + <Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user"/> <div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div> </div> <div :class="$style.status"> diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index 746781d71f280436dd1e0787dd41e6877e4a7fd3..4fbaf7545467d945f5e8145ad0fe86c5fce544cb 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.description"> <div v-if="user.description" :class="$style.mfm"> - <Mfm :text="user.description" :author="user" :i="$i"/> + <Mfm :text="user.description" :author="user"/> </div> <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> </div> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 063b122f8badc35ff29f731ded233f31dc6a7bbf..1e17bab849dde757fc443b09473b01eb29ee2951 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -5,14 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <span v-if="errored">:{{ customEmojiName }}:</span> -<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/> +<img + v-else + :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" + :src="url" + :alt="alt" + :title="alt" + decoding="async" + @error="errored = true" + @load="errored = false" + @click="onClick" +/> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, inject } from 'vue'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; import { customEmojisMap } from '@/custom-emojis.js'; +import * as os from '@/os.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { i18n } from '@/i18n.js'; const props = defineProps<{ name: string; @@ -21,8 +34,12 @@ const props = defineProps<{ host?: string | null; url?: string; useOriginalSize?: boolean; + menu?: boolean; + menuReaction?: boolean; }>(); +const react = inject<((name: string) => void) | null>('react', null); + const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', '')); const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); @@ -55,6 +72,28 @@ const url = computed(() => { const alt = computed(() => `:${customEmojiName.value}:`); let errored = $ref(url.value == null); + +function onClick(ev: MouseEvent) { + if (props.menu) { + os.popupMenu([{ + type: 'label', + text: `:${props.name}:`, + }, { + text: i18n.ts.copy, + icon: 'ti ti-copy', + action: () => { + copyToClipboard(`:${props.name}:`); + os.success(); + }, + }, ...(props.menuReaction && react ? [{ + text: i18n.ts.doReaction, + icon: 'ti ti-plus', + action: () => { + react(`:${props.name}:`); + }, + }] : [])], ev.currentTarget ?? ev.target); + } +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index ab8a342691f9ed75a8ad71e90a9bdb4b1b9b0efe..d7e149050297791cc010093ac14f046daad6e640 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -33,12 +33,13 @@ type MfmProps = { plain?: boolean; nowrap?: boolean; author?: Misskey.entities.UserLite; - i?: Misskey.entities.UserLite | null; isNote?: boolean; emojiUrls?: string[]; rootScale?: number; nyaize: boolean | 'account'; parsedNodes?: mfm.MfmNode[] | null; + enableEmojiMenu?: boolean; + enableEmojiMenuReaction?: boolean; }; // eslint-disable-next-line import/no-default-export @@ -328,6 +329,8 @@ export default function(props: MfmProps) { normal: props.plain, host: null, useOriginalSize: scale >= 2.5, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, })]; } else { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue index 35021be95ef1930e545767f91d0a84cdd51427d6..e0f1a4af90a32c35d3a823861d1ec02af11d6a7a 100644 --- a/packages/frontend/src/components/page/page.text.vue +++ b/packages/frontend/src/components/page/page.text.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> - <Mfm :text="block.text" :isNote="false" :i="$i"/> + <Mfm :text="block.text" :isNote="false"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url"/> </div> </template> diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 911f4e95d2e28d0542af7868484c49fad32e79c7..1d41fe7529b0778228b99386d9882bc2578f7339 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.bannerFade"></div> </div> <div v-if="channel.description" :class="$style.description"> - <Mfm :text="channel.description" :isNote="false" :i="$i"/> + <Mfm :text="channel.description" :isNote="false"/> </div> </div> diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 80b94acb6b230ddd37b0ececeb0aa1b9d93657e7..4573bbb81c95acb2a6dcb6be155e2a1af09a75a2 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="clip" class="_gaps"> <div class="_panel"> <div v-if="clip.description" :class="$style.description"> - <Mfm :text="clip.description" :isNote="false" :i="$i"/> + <Mfm :text="clip.description" :isNote="false"/> </div> <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> <MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 4c425898d5d3f9f5ff69f86af101079a8cb42352..7ff490bf8b6a2a1414adea86fe664ca6ec5ccfa8 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="description"> <MkOmit> - <Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" :i="$i"/> + <Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user"/> <p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p> </MkOmit> </div> @@ -100,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="field.name" :plain="true" :colored="false"/> </dt> <dd class="value"> - <Mfm :text="field.value" :author="user" :i="$i" :colored="false"/> + <Mfm :text="field.value" :author="user" :colored="false"/> <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i> </dd> </dl> diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index f2e151468a7ebff92e333fc916af8b6bdb276a31..8e2192074d1bd8622ab071ee8840ffd617f5b23b 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_panel" :class="$style.content"> <div> <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> + <Mfm v-if="note.text" :text="note.text" :author="note.user"/> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <div v-if="note.files.length > 0" :class="$style.richcontent">