From 111eb43fd93d0a496da054c36ec84c6066c1c434 Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Wed, 3 Jun 2020 13:30:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E6=8A=95=E7=A8=BF=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=BC=E3=83=A0=E3=81=AE=E3=83=9C=E3=82=BF=E3=83=B3?= =?UTF-8?q?=E3=81=AE=E8=AA=AC=E6=98=8E=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#6408)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add title attr with buttons on the post form * fix * tooltip * missing ; * remove title attr * fix bug * Update reactions-viewer.details.vue * help wip * ok! * i18n Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- locales/ja-JP.yml | 7 +- src/client/components/cw-button.vue | 2 +- src/client/components/link.vue | 4 +- src/client/components/post-form.vue | 18 +-- .../components/reactions-viewer.details.vue | 104 ++++-------------- .../components/reactions-viewer.reaction.vue | 5 +- src/client/components/ui/tooltip.vue | 96 ++++++++++++++++ src/client/components/url.vue | 4 +- src/client/directives/index.ts | 2 + src/client/directives/tooltip.ts | 62 +++++++++++ src/client/scripts/compose-notification.ts | 2 +- src/client/scripts/is-device-touch.ts | 4 +- src/misc/get-note-summary.ts | 2 +- 13 files changed, 207 insertions(+), 105 deletions(-) create mode 100644 src/client/components/ui/tooltip.vue create mode 100644 src/client/directives/tooltip.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 38827ea35a..c97aad482e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -45,6 +45,7 @@ loadMore: "もっと見る" youGotNewFollower: "フォローされました" receiveFollowRequest: "フォローリクエストされました" followRequestAccepted: "フォローが承認されました" +mention: "メンション" mentions: "あなた宛て" directNotes: "ダイレクト投稿" importAndExport: "インポートとエクスポート" @@ -104,6 +105,7 @@ suspendConfirm: "凍結しますか?" unsuspendConfirm: "解凍しますか?" selectList: "リストを選択" customEmojis: "カスタム絵文字" +emoji: "絵文字" emojiName: "絵文字名" emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" @@ -510,6 +512,9 @@ serviceworkerInfo: "プッシュ通知を行うには有効する必要があり deletedNote: "削除された投稿" invisibleNote: "非公開の投稿" enableInfiniteScroll: "自動でもっと見る" +visibility: "公開範囲" +poll: "アンケート" +useCw: "内容を隠す" _theme: explore: "テーマを探す" @@ -648,7 +653,6 @@ _cw: show: "もっと見る" chars: "{count}文字" files: "{count}ファイル" - poll: "アンケート" _poll: noOnlyOneChoice: "選択肢は最低2つ必要です" @@ -1119,3 +1123,4 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" youWereInvitedToGroup: "グループに招待されました" + diff --git a/src/client/components/cw-button.vue b/src/client/components/cw-button.vue index 07a44d970f..16a9b84f62 100644 --- a/src/client/components/cw-button.vue +++ b/src/client/components/cw-button.vue @@ -27,7 +27,7 @@ export default Vue.extend({ return concat([ this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [], this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [], - this.note.poll != null ? [this.$t('_cw.poll')] : [] + this.note.poll != null ? [this.$t('poll')] : [] ] as string[][]).join(' / '); } }, diff --git a/src/client/components/link.vue b/src/client/components/link.vue index 4c709375d3..7a364d0986 100644 --- a/src/client/components/link.vue +++ b/src/client/components/link.vue @@ -62,13 +62,13 @@ export default Vue.extend({ } }, onMouseover() { - if (isDeviceTouch()) return; + if (isDeviceTouch) return; clearTimeout(this.showTimer); clearTimeout(this.hideTimer); this.showTimer = setTimeout(this.showPreview, 500); }, onMouseleave() { - if (isDeviceTouch()) return; + if (isDeviceTouch) return; clearTimeout(this.showTimer); clearTimeout(this.hideTimer); this.hideTimer = setTimeout(this.closePreview, 500); diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index cdb61f51d5..ee6148a355 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -9,13 +9,13 @@ <button v-if="!fixed" class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button> <div> <span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span> - <button class="_button visibility" @click="setVisibility" ref="visibilityButton"> + <button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')"> <span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span> <span v-if="visibility === 'home'"><fa :icon="faHome"/></span> <span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span> <span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span> </button> - <button class="_button localOnly" v-if="visibility !== 'specified'" @click="localOnly = !localOnly" :class="{ active: localOnly }"><fa :icon="faBiohazard"/></button> + <button class="_button localOnly" v-if="visibility !== 'specified'" @click="localOnly = !localOnly" :class="{ active: localOnly }" v-tooltip="$t('_visibility.localOnly')"><fa :icon="faBiohazard"/></button> <button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button> </div> </header> @@ -26,7 +26,7 @@ <div v-if="visibility === 'specified'" class="to-specified"> <span style="margin-right: 8px;">{{ $t('recipient') }}</span> <div class="visibleUsers"> - <span v-for="u in visibleUsers"> + <span v-for="u in visibleUsers" :key="u.id"> <mk-acct :user="u"/> <button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button> </span> @@ -39,11 +39,11 @@ <x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> <x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> <footer> - <button class="_button" @click="chooseFileFrom"><fa :icon="faPhotoVideo"/></button> - <button class="_button" @click="poll = !poll" :class="{ active: poll }"><fa :icon="faPollH"/></button> - <button class="_button" @click="useCw = !useCw" :class="{ active: useCw }"><fa :icon="faEyeSlash"/></button> - <button class="_button" @click="insertMention"><fa :icon="faAt"/></button> - <button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button> + <button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><fa :icon="faPhotoVideo"/></button> + <button class="_button" @click="poll = !poll" :class="{ active: poll }" v-tooltip="$t('poll')"><fa :icon="faPollH"/></button> + <button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><fa :icon="faEyeSlash"/></button> + <button class="_button" @click="insertMention" v-tooltip="$t('mention')"><fa :icon="faAt"/></button> + <button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><fa :icon="faLaughSquint"/></button> </footer> <input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/> </div> @@ -576,7 +576,7 @@ export default Vue.extend({ insertTextAtCursor(this.$refs.text, emoji); vm.close(); }); - } + }, } }); </script> diff --git a/src/client/components/reactions-viewer.details.vue b/src/client/components/reactions-viewer.details.vue index 67c8b261be..96d1408fc1 100644 --- a/src/client/components/reactions-viewer.details.vue +++ b/src/client/components/reactions-viewer.details.vue @@ -1,27 +1,29 @@ <template> -<transition name="zoom-in-top"> - <div class="buebdbiu" ref="popover" v-if="show"> - <template v-if="users.length <= 10"> - <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> - <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> - <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> - </b> - </template> - <template v-if="10 < users.length"> - <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> - <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> - <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> - </b> - <span slot="omitted">+{{ count - 10 }}</span> - </template> - </div> -</transition> +<mk-tooltip :source="source" ref="tooltip"> + <template v-if="users.length <= 10"> + <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> + <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> + <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> + </b> + </template> + <template v-if="10 < users.length"> + <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> + <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> + <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> + </b> + <span slot="omitted">+{{ count - 10 }}</span> + </template> +</mk-tooltip> </template> <script lang="ts"> import Vue from 'vue'; +import MkTooltip from './ui/tooltip.vue'; export default Vue.extend({ + components: { + MkTooltip + }, props: { reaction: { type: String, @@ -39,77 +41,11 @@ export default Vue.extend({ required: true, } }, - data() { - return { - show: false - }; - }, - mounted() { - this.show = true; - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - if (this.source == null) { - this.destroyDom(); - return; - } - const rect = this.source.getBoundingClientRect(); - - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - popover.style.left = (x - 28) + 'px'; - popover.style.top = (y + 16) + 'px'; - }); - } methods: { close() { - this.show = false; - setTimeout(this.destroyDom, 300); + this.$refs.tooltip.close(); } } }) </script> - -<style lang="scss" scoped> -.buebdbiu { - z-index: 10000; - display: block; - position: absolute; - max-width: 240px; - font-size: 0.8em; - padding: 6px 8px; - background: var(--panel); - text-align: center; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0,0,0,0.25); - pointer-events: none; - transform-origin: center -16px; - - &:before { - content: ""; - pointer-events: none; - display: block; - position: absolute; - top: -28px; - left: 12px; - border-top: solid 14px transparent; - border-right: solid 14px transparent; - border-bottom: solid 14px rgba(0,0,0,0.1); - border-left: solid 14px transparent; - } - - &:after { - content: ""; - pointer-events: none; - display: block; - position: absolute; - top: -27px; - left: 12px; - border-top: solid 14px transparent; - border-right: solid 14px transparent; - border-bottom: solid 14px var(--panel); - border-left: solid 14px transparent; - } -} -</style> diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue index 33911dedb8..6b72f2e105 100644 --- a/src/client/components/reactions-viewer.reaction.vue +++ b/src/client/components/reactions-viewer.reaction.vue @@ -4,8 +4,10 @@ :class="{ reacted: note.myReaction == reaction, canToggle }" @click="toggleReaction(reaction)" v-if="count > 0" + @touchstart="onMouseover" @mouseover="onMouseover" @mouseleave="onMouseleave" + @touchend="onMouseleave" ref="reaction" v-particle > @@ -90,16 +92,17 @@ export default Vue.extend({ } }, onMouseover() { + if (this.isHovering) return; this.isHovering = true; this.detailsTimeoutId = setTimeout(this.openDetails, 300); }, onMouseleave() { + if (!this.isHovering) return; this.isHovering = false; clearTimeout(this.detailsTimeoutId); this.closeDetails(); }, openDetails() { - if (this.$root.isMobile) return; this.$root.api('notes/reactions', { noteId: this.note.id, type: this.reaction, diff --git a/src/client/components/ui/tooltip.vue b/src/client/components/ui/tooltip.vue new file mode 100644 index 0000000000..b7a56708b7 --- /dev/null +++ b/src/client/components/ui/tooltip.vue @@ -0,0 +1,96 @@ +<template> +<transition name="zoom-in-top" appear> + <div class="buebdbiu" v-if="show"> + <slot>{{ text }}</slot> + </div> +</transition> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + source: { + required: true, + }, + text: { + type: String, + required: false + } + }, + + data() { + return { + show: false + }; + }, + + mounted() { + this.show = true; + + this.$nextTick(() => { + if (this.source == null) { + this.destroyDom(); + return; + } + const rect = this.source.getBoundingClientRect(); + + const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); + const y = rect.top + window.pageYOffset + this.source.offsetHeight; + this.$el.style.left = (x - 28) + 'px'; + this.$el.style.top = (y + 16) + 'px'; + }); + }, + + methods: { + close() { + this.show = false; + setTimeout(this.destroyDom, 300); + } + } +}) +</script> + +<style lang="scss" scoped> +.buebdbiu { + z-index: 11000; + display: block; + position: absolute; + max-width: 240px; + font-size: 0.8em; + padding: 6px 8px; + background: var(--panel); + text-align: center; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + pointer-events: none; + transform-origin: center -16px; + + &:before { + content: ""; + pointer-events: none; + display: block; + position: absolute; + top: -28px; + left: 12px; + border-top: solid 14px transparent; + border-right: solid 14px transparent; + border-bottom: solid 14px rgba(0,0,0,0.1); + border-left: solid 14px transparent; + } + + &:after { + content: ""; + pointer-events: none; + display: block; + position: absolute; + top: -27px; + left: 12px; + border-top: solid 14px transparent; + border-right: solid 14px transparent; + border-bottom: solid 14px var(--panel); + border-left: solid 14px transparent; + } +} +</style> diff --git a/src/client/components/url.vue b/src/client/components/url.vue index 4dd23a50ed..0a5a5bc508 100644 --- a/src/client/components/url.vue +++ b/src/client/components/url.vue @@ -93,13 +93,13 @@ export default Vue.extend({ } }, onMouseover() { - if (isDeviceTouch()) return; + if (isDeviceTouch) return; clearTimeout(this.showTimer); clearTimeout(this.hideTimer); this.showTimer = setTimeout(this.showPreview, 500); }, onMouseleave() { - if (isDeviceTouch()) return; + if (isDeviceTouch) return; clearTimeout(this.showTimer); clearTimeout(this.hideTimer); this.hideTimer = setTimeout(this.closePreview, 500); diff --git a/src/client/directives/index.ts b/src/client/directives/index.ts index 64d33c0ff3..8cd5ed464d 100644 --- a/src/client/directives/index.ts +++ b/src/client/directives/index.ts @@ -4,9 +4,11 @@ import userPreview from './user-preview'; import autocomplete from './autocomplete'; import size from './size'; import particle from './particle'; +import tooltip from './tooltip'; Vue.directive('autocomplete', autocomplete); Vue.directive('userPreview', userPreview); Vue.directive('user-preview', userPreview); Vue.directive('size', size); Vue.directive('particle', particle); +Vue.directive('tooltip', tooltip); diff --git a/src/client/directives/tooltip.ts b/src/client/directives/tooltip.ts new file mode 100644 index 0000000000..28d22fc024 --- /dev/null +++ b/src/client/directives/tooltip.ts @@ -0,0 +1,62 @@ +import MkTooltip from '../components/ui/tooltip.vue'; +import { isDeviceTouch } from '../scripts/is-device-touch'; + +const start = isDeviceTouch ? 'touchstart' : 'mouseover'; +const end = isDeviceTouch ? 'touchend' : 'mouseleave'; + +export default { + bind(el: HTMLElement, binding, vn) { + const self = (el as any)._tooltipDirective_ = {} as any; + + self.text = binding.value as string; + self.tag = null; + self.showTimer = null; + self.hideTimer = null; + self.checkTimer = null; + + self.close = () => { + if (self.tag) { + clearInterval(self.checkTimer); + self.tag.close(); + self.tag = null; + } + }; + + const show = e => { + if (!document.body.contains(el)) return; + if (self.tag) return; + + self.tag = new MkTooltip({ + parent: vn.context, + propsData: { + text: self.text, + source: el + } + }).$mount(); + + document.body.appendChild(self.tag.$el); + }; + + el.addEventListener(start, () => { + clearTimeout(self.showTimer); + clearTimeout(self.hideTimer); + self.showTimer = setTimeout(show, 300); + }); + + el.addEventListener(end, () => { + clearTimeout(self.showTimer); + clearTimeout(self.hideTimer); + self.hideTimer = setTimeout(self.close, 300); + }); + + el.addEventListener('click', () => { + clearTimeout(self.showTimer); + self.close(); + }); + }, + + unbind(el, binding, vn) { + const self = el._tooltipDirective_; + clearInterval(self.checkTimer); + }, +}; diff --git a/src/client/scripts/compose-notification.ts b/src/client/scripts/compose-notification.ts index c3281955e4..1552d45e4e 100644 --- a/src/client/scripts/compose-notification.ts +++ b/src/client/scripts/compose-notification.ts @@ -5,7 +5,7 @@ import { clientDb, get, bulkGet } from '../db'; const getTranslation = (text: string): Promise<string> => get(text, clientDb.i18n); export default async function(type, data): Promise<[string, NotificationOptions]> { - const contexts = ['deletedNote', 'invisibleNote', 'withNFiles', '_cw.poll']; + const contexts = ['deletedNote', 'invisibleNote', 'withNFiles', 'poll']; const locale = Object.fromEntries(await bulkGet(contexts, clientDb.i18n) as [string, string][]); switch (type) { diff --git a/src/client/scripts/is-device-touch.ts b/src/client/scripts/is-device-touch.ts index 9f439ae4fd..3f0bfefed2 100644 --- a/src/client/scripts/is-device-touch.ts +++ b/src/client/scripts/is-device-touch.ts @@ -1,3 +1 @@ -export function isDeviceTouch(): boolean { - return 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; -} +export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; diff --git a/src/misc/get-note-summary.ts b/src/misc/get-note-summary.ts index c23306ab11..7db8bca3ec 100644 --- a/src/misc/get-note-summary.ts +++ b/src/misc/get-note-summary.ts @@ -27,7 +27,7 @@ const summarize = (note: any, locale: any): string => { // 投票が添付されているとき if (note.poll) { - summary += ` (${locale._cw?.poll || locale['_cw.poll']})`; + summary += ` (${locale['poll']})`; } // 返信のとき