feat(client): 投稿フォームのボタンの説明を表示するように (#6408)

* 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>
This commit is contained in:
tamaina 2020-06-03 13:30:17 +09:00 committed by GitHub
parent f2964101d1
commit 111eb43fd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 207 additions and 105 deletions

View file

@ -45,6 +45,7 @@ loadMore: "もっと見る"
youGotNewFollower: "フォローされました" youGotNewFollower: "フォローされました"
receiveFollowRequest: "フォローリクエストされました" receiveFollowRequest: "フォローリクエストされました"
followRequestAccepted: "フォローが承認されました" followRequestAccepted: "フォローが承認されました"
mention: "メンション"
mentions: "あなた宛て" mentions: "あなた宛て"
directNotes: "ダイレクト投稿" directNotes: "ダイレクト投稿"
importAndExport: "インポートとエクスポート" importAndExport: "インポートとエクスポート"
@ -104,6 +105,7 @@ suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?" unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択" selectList: "リストを選択"
customEmojis: "カスタム絵文字" customEmojis: "カスタム絵文字"
emoji: "絵文字"
emojiName: "絵文字名" emojiName: "絵文字名"
emojiUrl: "絵文字画像URL" emojiUrl: "絵文字画像URL"
addEmoji: "絵文字を追加" addEmoji: "絵文字を追加"
@ -510,6 +512,9 @@ serviceworkerInfo: "プッシュ通知を行うには有効する必要があり
deletedNote: "削除された投稿" deletedNote: "削除された投稿"
invisibleNote: "非公開の投稿" invisibleNote: "非公開の投稿"
enableInfiniteScroll: "自動でもっと見る" enableInfiniteScroll: "自動でもっと見る"
visibility: "公開範囲"
poll: "アンケート"
useCw: "内容を隠す"
_theme: _theme:
explore: "テーマを探す" explore: "テーマを探す"
@ -648,7 +653,6 @@ _cw:
show: "もっと見る" show: "もっと見る"
chars: "{count}文字" chars: "{count}文字"
files: "{count}ファイル" files: "{count}ファイル"
poll: "アンケート"
_poll: _poll:
noOnlyOneChoice: "選択肢は最低2つ必要です" noOnlyOneChoice: "選択肢は最低2つ必要です"
@ -1119,3 +1123,4 @@ _notification:
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
youWereInvitedToGroup: "グループに招待されました" youWereInvitedToGroup: "グループに招待されました"

View file

@ -27,7 +27,7 @@ export default Vue.extend({
return concat([ return concat([
this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [], 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.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(' / '); ] as string[][]).join(' / ');
} }
}, },

View file

@ -62,13 +62,13 @@ export default Vue.extend({
} }
}, },
onMouseover() { onMouseover() {
if (isDeviceTouch()) return; if (isDeviceTouch) return;
clearTimeout(this.showTimer); clearTimeout(this.showTimer);
clearTimeout(this.hideTimer); clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500); this.showTimer = setTimeout(this.showPreview, 500);
}, },
onMouseleave() { onMouseleave() {
if (isDeviceTouch()) return; if (isDeviceTouch) return;
clearTimeout(this.showTimer); clearTimeout(this.showTimer);
clearTimeout(this.hideTimer); clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500); this.hideTimer = setTimeout(this.closePreview, 500);

View file

@ -9,13 +9,13 @@
<button v-if="!fixed" class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button> <button v-if="!fixed" class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button>
<div> <div>
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span> <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 === 'public'"><fa :icon="faGlobe"/></span>
<span v-if="visibility === 'home'"><fa :icon="faHome"/></span> <span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span> <span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span>
<span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span> <span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span>
</button> </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> <button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button>
</div> </div>
</header> </header>
@ -26,7 +26,7 @@
<div v-if="visibility === 'specified'" class="to-specified"> <div v-if="visibility === 'specified'" class="to-specified">
<span style="margin-right: 8px;">{{ $t('recipient') }}</span> <span style="margin-right: 8px;">{{ $t('recipient') }}</span>
<div class="visibleUsers"> <div class="visibleUsers">
<span v-for="u in visibleUsers"> <span v-for="u in visibleUsers" :key="u.id">
<mk-acct :user="u"/> <mk-acct :user="u"/>
<button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button> <button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button>
</span> </span>
@ -39,11 +39,11 @@
<x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> <x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
<x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> <x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
<footer> <footer>
<button class="_button" @click="chooseFileFrom"><fa :icon="faPhotoVideo"/></button> <button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><fa :icon="faPhotoVideo"/></button>
<button class="_button" @click="poll = !poll" :class="{ active: poll }"><fa :icon="faPollH"/></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 }"><fa :icon="faEyeSlash"/></button> <button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><fa :icon="faEyeSlash"/></button>
<button class="_button" @click="insertMention"><fa :icon="faAt"/></button> <button class="_button" @click="insertMention" v-tooltip="$t('mention')"><fa :icon="faAt"/></button>
<button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button> <button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><fa :icon="faLaughSquint"/></button>
</footer> </footer>
<input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/> <input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/>
</div> </div>
@ -576,7 +576,7 @@ export default Vue.extend({
insertTextAtCursor(this.$refs.text, emoji); insertTextAtCursor(this.$refs.text, emoji);
vm.close(); vm.close();
}); });
} },
} }
}); });
</script> </script>

View file

@ -1,27 +1,29 @@
<template> <template>
<transition name="zoom-in-top"> <mk-tooltip :source="source" ref="tooltip">
<div class="buebdbiu" ref="popover" v-if="show"> <template v-if="users.length <= 10">
<template v-if="users.length <= 10"> <b v-for="u in users" :key="u.id" style="margin-right: 12px;">
<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-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> </b>
</b> </template>
</template> <template v-if="10 < users.length">
<template v-if="10 < users.length"> <b v-for="u in users" :key="u.id" style="margin-right: 12px;">
<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-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> </b>
</b> <span slot="omitted">+{{ count - 10 }}</span>
<span slot="omitted">+{{ count - 10 }}</span> </template>
</template> </mk-tooltip>
</div>
</transition>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import MkTooltip from './ui/tooltip.vue';
export default Vue.extend({ export default Vue.extend({
components: {
MkTooltip
},
props: { props: {
reaction: { reaction: {
type: String, type: String,
@ -39,77 +41,11 @@ export default Vue.extend({
required: true, 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: { methods: {
close() { close() {
this.show = false; this.$refs.tooltip.close();
setTimeout(this.destroyDom, 300);
} }
} }
}) })
</script> </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>

View file

@ -4,8 +4,10 @@
:class="{ reacted: note.myReaction == reaction, canToggle }" :class="{ reacted: note.myReaction == reaction, canToggle }"
@click="toggleReaction(reaction)" @click="toggleReaction(reaction)"
v-if="count > 0" v-if="count > 0"
@touchstart="onMouseover"
@mouseover="onMouseover" @mouseover="onMouseover"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
@touchend="onMouseleave"
ref="reaction" ref="reaction"
v-particle v-particle
> >
@ -90,16 +92,17 @@ export default Vue.extend({
} }
}, },
onMouseover() { onMouseover() {
if (this.isHovering) return;
this.isHovering = true; this.isHovering = true;
this.detailsTimeoutId = setTimeout(this.openDetails, 300); this.detailsTimeoutId = setTimeout(this.openDetails, 300);
}, },
onMouseleave() { onMouseleave() {
if (!this.isHovering) return;
this.isHovering = false; this.isHovering = false;
clearTimeout(this.detailsTimeoutId); clearTimeout(this.detailsTimeoutId);
this.closeDetails(); this.closeDetails();
}, },
openDetails() { openDetails() {
if (this.$root.isMobile) return;
this.$root.api('notes/reactions', { this.$root.api('notes/reactions', {
noteId: this.note.id, noteId: this.note.id,
type: this.reaction, type: this.reaction,

View file

@ -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>

View file

@ -93,13 +93,13 @@ export default Vue.extend({
} }
}, },
onMouseover() { onMouseover() {
if (isDeviceTouch()) return; if (isDeviceTouch) return;
clearTimeout(this.showTimer); clearTimeout(this.showTimer);
clearTimeout(this.hideTimer); clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500); this.showTimer = setTimeout(this.showPreview, 500);
}, },
onMouseleave() { onMouseleave() {
if (isDeviceTouch()) return; if (isDeviceTouch) return;
clearTimeout(this.showTimer); clearTimeout(this.showTimer);
clearTimeout(this.hideTimer); clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500); this.hideTimer = setTimeout(this.closePreview, 500);

View file

@ -4,9 +4,11 @@ import userPreview from './user-preview';
import autocomplete from './autocomplete'; import autocomplete from './autocomplete';
import size from './size'; import size from './size';
import particle from './particle'; import particle from './particle';
import tooltip from './tooltip';
Vue.directive('autocomplete', autocomplete); Vue.directive('autocomplete', autocomplete);
Vue.directive('userPreview', userPreview); Vue.directive('userPreview', userPreview);
Vue.directive('user-preview', userPreview); Vue.directive('user-preview', userPreview);
Vue.directive('size', size); Vue.directive('size', size);
Vue.directive('particle', particle); Vue.directive('particle', particle);
Vue.directive('tooltip', tooltip);

View file

@ -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);
},
};

View file

@ -5,7 +5,7 @@ import { clientDb, get, bulkGet } from '../db';
const getTranslation = (text: string): Promise<string> => get(text, clientDb.i18n); const getTranslation = (text: string): Promise<string> => get(text, clientDb.i18n);
export default async function(type, data): Promise<[string, NotificationOptions]> { 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][]); const locale = Object.fromEntries(await bulkGet(contexts, clientDb.i18n) as [string, string][]);
switch (type) { switch (type) {

View file

@ -1,3 +1 @@
export function isDeviceTouch(): boolean { export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
return 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
}

View file

@ -27,7 +27,7 @@ const summarize = (note: any, locale: any): string => {
// 投票が添付されているとき // 投票が添付されているとき
if (note.poll) { if (note.poll) {
summary += ` (${locale._cw?.poll || locale['_cw.poll']})`; summary += ` (${locale['poll']})`;
} }
// 返信のとき // 返信のとき