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:
		
							parent
							
								
									9b092e918a
								
							
						
					
					
						commit
						0e3213ff6d
					
				
					 4 changed files with 227 additions and 94 deletions
				
			
		| 
						 | 
				
			
			@ -94,12 +94,7 @@
 | 
			
		|||
					<template v-else><i class="fas fa-reply"></i></template>
 | 
			
		||||
					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button v-if="canRenote" @click="renote()" class="button _button" 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>
 | 
			
		||||
				<XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
 | 
			
		||||
				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
 | 
			
		||||
					<i class="fas fa-plus"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -136,16 +131,17 @@ import XReactionsViewer from './reactions-viewer.vue';
 | 
			
		|||
import XMediaList from './media-list.vue';
 | 
			
		||||
import XCwButton from './cw-button.vue';
 | 
			
		||||
import XPoll from './poll.vue';
 | 
			
		||||
import { pleaseLogin } from '@/scripts/please-login';
 | 
			
		||||
import { focusPrev, focusNext } from '@/scripts/focus';
 | 
			
		||||
import { url } from '@/config';
 | 
			
		||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
			
		||||
import { checkWordMute } from '@/scripts/check-word-mute';
 | 
			
		||||
import { userPage } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { noteActions, noteViewInterruptors } from '@/store';
 | 
			
		||||
import { reactionPicker } from '@/scripts/reaction-picker';
 | 
			
		||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
			
		||||
import XRenoteButton from './renote-button.vue';
 | 
			
		||||
import { pleaseLogin } from '@client/scripts/please-login';
 | 
			
		||||
import { focusPrev, focusNext } from '@client/scripts/focus';
 | 
			
		||||
import { url } from '@client/config';
 | 
			
		||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
 | 
			
		||||
import { checkWordMute } from '@client/scripts/check-word-mute';
 | 
			
		||||
import { userPage } from '@client/filters/user';
 | 
			
		||||
import * as os from '@client/os';
 | 
			
		||||
import { noteActions, noteViewInterruptors } from '@client/store';
 | 
			
		||||
import { reactionPicker } from '@client/scripts/reaction-picker';
 | 
			
		||||
import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
 | 
			
		||||
 | 
			
		||||
// TODO: note.vueとほぼ同じなので共通化したい
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
| 
						 | 
				
			
			@ -157,8 +153,9 @@ export default defineComponent({
 | 
			
		|||
		XMediaList,
 | 
			
		||||
		XCwButton,
 | 
			
		||||
		XPoll,
 | 
			
		||||
		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
 | 
			
		||||
		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
 | 
			
		||||
		XRenoteButton,
 | 
			
		||||
		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
 | 
			
		||||
		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	inject: {
 | 
			
		||||
| 
						 | 
				
			
			@ -197,7 +194,7 @@ export default defineComponent({
 | 
			
		|||
			return {
 | 
			
		||||
				'r': () => this.reply(true),
 | 
			
		||||
				'e|a|plus': () => this.react(true),
 | 
			
		||||
				'q': () => this.renote(true),
 | 
			
		||||
				'q': () => this.$refs.renoteButton.renote(true),
 | 
			
		||||
				'f|b': this.favorite,
 | 
			
		||||
				'delete|ctrl+d': this.del,
 | 
			
		||||
				'ctrl+q': this.renoteDirectly,
 | 
			
		||||
| 
						 | 
				
			
			@ -238,10 +235,6 @@ export default defineComponent({
 | 
			
		|||
			return this.$i && (this.$i.id === this.note.userId);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		canRenote(): boolean {
 | 
			
		||||
			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reactionsCount(): number {
 | 
			
		||||
			return 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() {
 | 
			
		||||
			os.apiWithDialog('notes/create', {
 | 
			
		||||
				renoteId: this.appearNote.id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,12 +78,7 @@
 | 
			
		|||
					<template v-else><i class="fas fa-reply"></i></template>
 | 
			
		||||
					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button v-if="canRenote" @click="renote()" class="button _button" 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>
 | 
			
		||||
				<XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
 | 
			
		||||
				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
 | 
			
		||||
					<i class="fas fa-plus"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -119,16 +114,17 @@ import XReactionsViewer from './reactions-viewer.vue';
 | 
			
		|||
import XMediaList from './media-list.vue';
 | 
			
		||||
import XCwButton from './cw-button.vue';
 | 
			
		||||
import XPoll from './poll.vue';
 | 
			
		||||
import { pleaseLogin } from '@/scripts/please-login';
 | 
			
		||||
import { focusPrev, focusNext } from '@/scripts/focus';
 | 
			
		||||
import { url } from '@/config';
 | 
			
		||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
			
		||||
import { checkWordMute } from '@/scripts/check-word-mute';
 | 
			
		||||
import { userPage } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { noteActions, noteViewInterruptors } from '@/store';
 | 
			
		||||
import { reactionPicker } from '@/scripts/reaction-picker';
 | 
			
		||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
			
		||||
import XRenoteButton from './renote-button.vue';
 | 
			
		||||
import { pleaseLogin } from '@client/scripts/please-login';
 | 
			
		||||
import { focusPrev, focusNext } from '@client/scripts/focus';
 | 
			
		||||
import { url } from '@client/config';
 | 
			
		||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
 | 
			
		||||
import { checkWordMute } from '@client/scripts/check-word-mute';
 | 
			
		||||
import { userPage } from '@client/filters/user';
 | 
			
		||||
import * as os from '@client/os';
 | 
			
		||||
import { noteActions, noteViewInterruptors } from '@client/store';
 | 
			
		||||
import { reactionPicker } from '@client/scripts/reaction-picker';
 | 
			
		||||
import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
| 
						 | 
				
			
			@ -139,8 +135,9 @@ export default defineComponent({
 | 
			
		|||
		XMediaList,
 | 
			
		||||
		XCwButton,
 | 
			
		||||
		XPoll,
 | 
			
		||||
		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
 | 
			
		||||
		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
 | 
			
		||||
		XRenoteButton,
 | 
			
		||||
		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
 | 
			
		||||
		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	inject: {
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +181,7 @@ export default defineComponent({
 | 
			
		|||
			return {
 | 
			
		||||
				'r': () => this.reply(true),
 | 
			
		||||
				'e|a|plus': () => this.react(true),
 | 
			
		||||
				'q': () => this.renote(true),
 | 
			
		||||
				'q': () => this.$refs.renoteButton.renote(true),
 | 
			
		||||
				'f|b': this.favorite,
 | 
			
		||||
				'delete|ctrl+d': this.del,
 | 
			
		||||
				'ctrl+q': this.renoteDirectly,
 | 
			
		||||
| 
						 | 
				
			
			@ -225,10 +222,6 @@ export default defineComponent({
 | 
			
		|||
			return this.$i && (this.$i.id === this.note.userId);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		canRenote(): boolean {
 | 
			
		||||
			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reactionsCount(): number {
 | 
			
		||||
			return 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() {
 | 
			
		||||
			os.apiWithDialog('notes/create', {
 | 
			
		||||
				renoteId: this.appearNote.id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										149
									
								
								packages/client/src/components/renote-button.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								packages/client/src/components/renote-button.vue
									
										
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										46
									
								
								packages/client/src/components/renote.details.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								packages/client/src/components/renote.details.vue
									
										
									
									
									
										Normal 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>
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue