enhance: show renoters (#7954)

* refactor: deduplicate renote button into component

For now the renoters tooltip just uses the reaction viewer component
with a fixed emoji symbol instead.

* chore: remove unnecessary CSS

* fix: forgot to rename variable

* enhance: use own tooltip instead of reaction viewer

* clean up style

* fix additional renoters number

* rename file to better represent content
This commit is contained in:
Johann150 2021-11-12 15:15:14 +01:00 committed by GitHub
parent 9b092e918a
commit 0e3213ff6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 227 additions and 94 deletions

View File

@ -94,12 +94,7 @@
<template v-else><i class="fas fa-reply"></i></template> <template v-else><i class="fas fa-reply"></i></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button> </button>
<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton"> <XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button">
<i class="fas fa-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> <button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
@ -136,16 +131,17 @@ import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue'; import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue'; import XCwButton from './cw-button.vue';
import XPoll from './poll.vue'; import XPoll from './poll.vue';
import { pleaseLogin } from '@/scripts/please-login'; import XRenoteButton from './renote-button.vue';
import { focusPrev, focusNext } from '@/scripts/focus'; import { pleaseLogin } from '@client/scripts/please-login';
import { url } from '@/config'; import { focusPrev, focusNext } from '@client/scripts/focus';
import copyToClipboard from '@/scripts/copy-to-clipboard'; import { url } from '@client/config';
import { checkWordMute } from '@/scripts/check-word-mute'; import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { userPage } from '@/filters/user'; import { checkWordMute } from '@client/scripts/check-word-mute';
import * as os from '@/os'; import { userPage } from '@client/filters/user';
import { noteActions, noteViewInterruptors } from '@/store'; import * as os from '@client/os';
import { reactionPicker } from '@/scripts/reaction-picker'; import { noteActions, noteViewInterruptors } from '@client/store';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { reactionPicker } from '@client/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
// TODO: note.vue // TODO: note.vue
export default defineComponent({ export default defineComponent({
@ -157,8 +153,9 @@ export default defineComponent({
XMediaList, XMediaList,
XCwButton, XCwButton,
XPoll, XPoll,
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), XRenoteButton,
MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
}, },
inject: { inject: {
@ -197,7 +194,7 @@ export default defineComponent({
return { return {
'r': () => this.reply(true), 'r': () => this.reply(true),
'e|a|plus': () => this.react(true), 'e|a|plus': () => this.react(true),
'q': () => this.renote(true), 'q': () => this.$refs.renoteButton.renote(true),
'f|b': this.favorite, 'f|b': this.favorite,
'delete|ctrl+d': this.del, 'delete|ctrl+d': this.del,
'ctrl+q': this.renoteDirectly, 'ctrl+q': this.renoteDirectly,
@ -238,10 +235,6 @@ export default defineComponent({
return this.$i && (this.$i.id === this.note.userId); return this.$i && (this.$i.id === this.note.userId);
}, },
canRenote(): boolean {
return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
},
reactionsCount(): number { reactionsCount(): number {
return this.appearNote.reactions return this.appearNote.reactions
? sum(Object.values(this.appearNote.reactions)) ? sum(Object.values(this.appearNote.reactions))
@ -459,30 +452,6 @@ export default defineComponent({
}); });
}, },
renote(viaKeyboard = false) {
pleaseLogin();
this.blur();
os.popupMenu([{
text: this.$ts.renote,
icon: 'fas fa-retweet',
action: () => {
os.api('notes/create', {
renoteId: this.appearNote.id
});
}
}, {
text: this.$ts.quote,
icon: 'fas fa-quote-right',
action: () => {
os.post({
renote: this.appearNote,
});
}
}], this.$refs.renoteButton, {
viaKeyboard
});
},
renoteDirectly() { renoteDirectly() {
os.apiWithDialog('notes/create', { os.apiWithDialog('notes/create', {
renoteId: this.appearNote.id renoteId: this.appearNote.id

View File

@ -78,12 +78,7 @@
<template v-else><i class="fas fa-reply"></i></template> <template v-else><i class="fas fa-reply"></i></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button> </button>
<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton"> <XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button">
<i class="fas fa-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> <button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
@ -119,16 +114,17 @@ import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue'; import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue'; import XCwButton from './cw-button.vue';
import XPoll from './poll.vue'; import XPoll from './poll.vue';
import { pleaseLogin } from '@/scripts/please-login'; import XRenoteButton from './renote-button.vue';
import { focusPrev, focusNext } from '@/scripts/focus'; import { pleaseLogin } from '@client/scripts/please-login';
import { url } from '@/config'; import { focusPrev, focusNext } from '@client/scripts/focus';
import copyToClipboard from '@/scripts/copy-to-clipboard'; import { url } from '@client/config';
import { checkWordMute } from '@/scripts/check-word-mute'; import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { userPage } from '@/filters/user'; import { checkWordMute } from '@client/scripts/check-word-mute';
import * as os from '@/os'; import { userPage } from '@client/filters/user';
import { noteActions, noteViewInterruptors } from '@/store'; import * as os from '@client/os';
import { reactionPicker } from '@/scripts/reaction-picker'; import { noteActions, noteViewInterruptors } from '@client/store';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { reactionPicker } from '@client/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -139,8 +135,9 @@ export default defineComponent({
XMediaList, XMediaList,
XCwButton, XCwButton,
XPoll, XPoll,
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), XRenoteButton,
MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
}, },
inject: { inject: {
@ -184,7 +181,7 @@ export default defineComponent({
return { return {
'r': () => this.reply(true), 'r': () => this.reply(true),
'e|a|plus': () => this.react(true), 'e|a|plus': () => this.react(true),
'q': () => this.renote(true), 'q': () => this.$refs.renoteButton.renote(true),
'f|b': this.favorite, 'f|b': this.favorite,
'delete|ctrl+d': this.del, 'delete|ctrl+d': this.del,
'ctrl+q': this.renoteDirectly, 'ctrl+q': this.renoteDirectly,
@ -225,10 +222,6 @@ export default defineComponent({
return this.$i && (this.$i.id === this.note.userId); return this.$i && (this.$i.id === this.note.userId);
}, },
canRenote(): boolean {
return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
},
reactionsCount(): number { reactionsCount(): number {
return this.appearNote.reactions return this.appearNote.reactions
? sum(Object.values(this.appearNote.reactions)) ? sum(Object.values(this.appearNote.reactions))
@ -435,30 +428,6 @@ export default defineComponent({
}); });
}, },
renote(viaKeyboard = false) {
pleaseLogin();
this.blur();
os.popupMenu([{
text: this.$ts.renote,
icon: 'fas fa-retweet',
action: () => {
os.api('notes/create', {
renoteId: this.appearNote.id
});
}
}, {
text: this.$ts.quote,
icon: 'fas fa-quote-right',
action: () => {
os.post({
renote: this.appearNote,
});
}
}], this.$refs.renoteButton, {
viaKeyboard
});
},
renoteDirectly() { renoteDirectly() {
os.apiWithDialog('notes/create', { os.apiWithDialog('notes/create', {
renoteId: this.appearNote.id renoteId: this.appearNote.id

View File

@ -0,0 +1,149 @@
<template>
<button
class="button _button canRenote"
@click="renote()"
v-if="canRenote"
@touchstart.passive="onMouseover"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
@touchend="onMouseleave"
ref="renoteButton"
>
<i class="fas fa-retweet"></i>
<p class="count" v-if="count > 0">{{ count }}</p>
</button>
<button
v-else
class="button _button"
>
<i class="fas fa-ban"></i>
</button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import XDetails from '@client/components/renote.details.vue';
import { pleaseLogin } from '@client/scripts/please-login';
import * as os from '@client/os';
export default defineComponent({
props: {
count: {
type: Number,
required: true,
},
note: {
type: Object,
required: true,
},
},
data() {
return {
close: null,
detailsTimeoutId: null,
isHovering: false
};
},
computed: {
canRenote(): boolean {
return ['public', 'home'].includes(this.note.visibility) || this.note.userId === this.$i.id;
},
},
watch: {
count(newCount, oldCount) {
if (oldCount < newCount) this.anime();
if (this.close != null) this.openDetails();
},
},
methods: {
renote(viaKeyboard = false) {
pleaseLogin();
os.popupMenu([{
text: this.$ts.renote,
icon: 'fas fa-retweet',
action: () => {
os.api('notes/create', {
renoteId: this.note.id
});
}
}, {
text: this.$ts.quote,
icon: 'fas fa-quote-right',
action: () => {
os.post({
renote: this.note,
});
}
}], this.$refs.renoteButton, {
viaKeyboard
});
},
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() {
os.api('notes/renotes', {
noteId: this.note.id,
limit: 11
}).then((renotes: any[]) => {
const users = renotes
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.map(x => x.user);
this.closeDetails();
if (!this.isHovering || users.length < 1) return;
const showing = ref(true);
os.popup(XDetails, {
showing,
users,
count: this.count,
source: this.$refs.renoteButton
}, {}, 'closed');
this.close = () => {
showing.value = false;
};
});
},
closeDetails() {
if (this.close != null) {
this.close();
this.close = null;
}
},
}
});
</script>
<style lang="scss" scoped>
.button {
display: inline-block;
height: 32px;
margin: 2px;
padding: 0 6px;
border-radius: 4px;
&:not(.canRenote) {
cursor: default;
}
&.renoted {
background: var(--accent);
}
> .count {
display: inline;
margin-left: 8px;
opacity: 0.7;
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')" :max-width="340">
<div class="renoteTooltip">
<b v-for="u in users" :key="u.id">
<MkAvatar :user="u" style="width: 24px; height: 24px;"/><br/>
<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/>
</b>
<span v-if="users.length < count" slot="omitted">+{{ count - users.length }}</span>
</div>
</MkTooltip>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkTooltip from './ui/tooltip.vue';
export default defineComponent({
components: {
MkTooltip,
},
props: {
users: {
type: Array,
required: true,
},
count: {
type: Number,
required: true,
},
source: {
required: true,
}
},
emits: ['closed'],
})
</script>
<style lang="scss" scoped>
.renoteTooltip {
display: flex;
flex: 1;
min-width: 0;
font-size: 0.9em;
gap: 12px;
}
</style>