wip
This commit is contained in:
		
							parent
							
								
									a3a3404398
								
							
						
					
					
						commit
						7355ae4f8a
					
				|  | @ -0,0 +1,23 @@ | |||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and misskey-project | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
| 
 | ||||
| <template> | ||||
| <EmCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl" :fallbackToImage="true"/> | ||||
| <EmEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import EmCustomEmoji from './EmCustomEmoji.vue'; | ||||
| import EmEmoji from './EmEmoji.vue'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	reaction: string; | ||||
| 	noStyle?: boolean; | ||||
| 	emojiUrl?: string; | ||||
| 	withTooltip?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| </script> | ||||
|  | @ -5,36 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 
 | ||||
| <template> | ||||
| <button | ||||
| 	ref="buttonEl" | ||||
| 	v-ripple="canToggle" | ||||
| 	class="_button" | ||||
| 	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" | ||||
| 	@click="toggleReaction()" | ||||
| 	@contextmenu.prevent.stop="menu" | ||||
| 	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction }]" | ||||
| > | ||||
| 	<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> | ||||
| 	<MkReactionIcon :class="$style.limitWidth" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> | ||||
| 	<span :class="$style.count">{{ count }}</span> | ||||
| </button> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, inject, onMounted, shallowRef, watch } from 'vue'; | ||||
| import { } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; | ||||
| import XDetails from '@/components/MkReactionsViewer.details.vue'; | ||||
| import MkReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import * as os from '@/os.js'; | ||||
| import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; | ||||
| import { useTooltip } from '@/scripts/use-tooltip.js'; | ||||
| import { $i } from '@/account.js'; | ||||
| import MkReactionEffect from '@/components/MkReactionEffect.vue'; | ||||
| import { claimAchievement } from '@/scripts/achievements.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import * as sound from '@/scripts/sound.js'; | ||||
| import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; | ||||
| import { customEmojisMap } from '@/custom-emojis.js'; | ||||
| import { getUnicodeEmoji } from '@/scripts/emojilist.js'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	reaction: string; | ||||
|  | @ -42,130 +24,6 @@ const props = defineProps<{ | |||
| 	isInitial: boolean; | ||||
| 	note: Misskey.entities.Note; | ||||
| }>(); | ||||
| 
 | ||||
| const mock = inject<boolean>('mock', false); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'reactionToggled', emoji: string, newCount: number): void; | ||||
| }>(); | ||||
| 
 | ||||
| const buttonEl = shallowRef<HTMLElement>(); | ||||
| 
 | ||||
| const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, '')); | ||||
| const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction)); | ||||
| 
 | ||||
| const canToggle = computed(() => { | ||||
| 	return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); | ||||
| }); | ||||
| const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); | ||||
| 
 | ||||
| async function toggleReaction() { | ||||
| 	if (!canToggle.value) return; | ||||
| 
 | ||||
| 	const oldReaction = props.note.myReaction; | ||||
| 	if (oldReaction) { | ||||
| 		const confirm = await os.confirm({ | ||||
| 			type: 'warning', | ||||
| 			text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm, | ||||
| 		}); | ||||
| 		if (confirm.canceled) return; | ||||
| 
 | ||||
| 		if (oldReaction !== props.reaction) { | ||||
| 			sound.playMisskeySfx('reaction'); | ||||
| 		} | ||||
| 
 | ||||
| 		if (mock) { | ||||
| 			emit('reactionToggled', props.reaction, (props.count - 1)); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		misskeyApi('notes/reactions/delete', { | ||||
| 			noteId: props.note.id, | ||||
| 		}).then(() => { | ||||
| 			if (oldReaction !== props.reaction) { | ||||
| 				misskeyApi('notes/reactions/create', { | ||||
| 					noteId: props.note.id, | ||||
| 					reaction: props.reaction, | ||||
| 				}); | ||||
| 			} | ||||
| 		}); | ||||
| 	} else { | ||||
| 		sound.playMisskeySfx('reaction'); | ||||
| 
 | ||||
| 		if (mock) { | ||||
| 			emit('reactionToggled', props.reaction, (props.count + 1)); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		misskeyApi('notes/reactions/create', { | ||||
| 			noteId: props.note.id, | ||||
| 			reaction: props.reaction, | ||||
| 		}); | ||||
| 		if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { | ||||
| 			claimAchievement('reactWithoutRead'); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| async function menu(ev) { | ||||
| 	if (!canGetInfo.value) return; | ||||
| 
 | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.ts.info, | ||||
| 		icon: 'ti ti-info-circle', | ||||
| 		action: async () => { | ||||
| 			const { dispose } = os.popup(MkCustomEmojiDetailedDialog, { | ||||
| 				emoji: await misskeyApiGet('emoji', { | ||||
| 					name: props.reaction.replace(/:/g, '').replace(/@\./, ''), | ||||
| 				}), | ||||
| 			}, { | ||||
| 				closed: () => dispose(), | ||||
| 			}); | ||||
| 		}, | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| function anime() { | ||||
| 	if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return; | ||||
| 
 | ||||
| 	const rect = buttonEl.value.getBoundingClientRect(); | ||||
| 	const x = rect.left + 16; | ||||
| 	const y = rect.top + (buttonEl.value.offsetHeight / 2); | ||||
| 	const { dispose } = os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, { | ||||
| 		end: () => dispose(), | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| watch(() => props.count, (newCount, oldCount) => { | ||||
| 	if (oldCount < newCount) anime(); | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	if (!props.isInitial) anime(); | ||||
| }); | ||||
| 
 | ||||
| if (!mock) { | ||||
| 	useTooltip(buttonEl, async (showing) => { | ||||
| 		const reactions = await misskeyApiGet('notes/reactions', { | ||||
| 			noteId: props.note.id, | ||||
| 			type: props.reaction, | ||||
| 			limit: 10, | ||||
| 			_cacheKey_: props.count, | ||||
| 		}); | ||||
| 
 | ||||
| 		const users = reactions.map(x => x.user); | ||||
| 
 | ||||
| 		const { dispose } = os.popup(XDetails, { | ||||
| 			showing, | ||||
| 			reaction: props.reaction, | ||||
| 			users, | ||||
| 			count: props.count, | ||||
| 			targetElement: buttonEl.value, | ||||
| 		}, { | ||||
| 			closed: () => dispose(), | ||||
| 		}); | ||||
| 	}, 100); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
|  |  | |||
|  | @ -1,112 +0,0 @@ | |||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and misskey-project | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
| 
 | ||||
| <template> | ||||
| <TransitionGroup | ||||
| 	:enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''" | ||||
| 	:leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''" | ||||
| 	:enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''" | ||||
| 	:leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''" | ||||
| 	:moveClass="defaultStore.state.animation ? $style.transition_x_move : ''" | ||||
| 	tag="div" :class="$style.root" | ||||
| > | ||||
| 	<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/> | ||||
| 	<slot v-if="hasMoreReactions" name="more"/> | ||||
| </TransitionGroup> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { inject, watch, ref } from 'vue'; | ||||
| import XReaction from '@/components/EmReactionsViewer.reaction.vue'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	note: Misskey.entities.Note; | ||||
| 	maxNumber?: number; | ||||
| }>(), { | ||||
| 	maxNumber: Infinity, | ||||
| }); | ||||
| 
 | ||||
| const mock = inject<boolean>('mock', false); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; | ||||
| }>(); | ||||
| 
 | ||||
| const initialReactions = new Set(Object.keys(props.note.reactions)); | ||||
| 
 | ||||
| const reactions = ref<[string, number][]>([]); | ||||
| const hasMoreReactions = ref(false); | ||||
| 
 | ||||
| if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) { | ||||
| 	reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction]; | ||||
| } | ||||
| 
 | ||||
| function onMockToggleReaction(emoji: string, count: number) { | ||||
| 	if (!mock) return; | ||||
| 
 | ||||
| 	const i = reactions.value.findIndex((item) => item[0] === emoji); | ||||
| 	if (i < 0) return; | ||||
| 
 | ||||
| 	emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1])); | ||||
| } | ||||
| 
 | ||||
| watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { | ||||
| 	let newReactions: [string, number][] = []; | ||||
| 	hasMoreReactions.value = Object.keys(newSource).length > maxNumber; | ||||
| 
 | ||||
| 	for (let i = 0; i < reactions.value.length; i++) { | ||||
| 		const reaction = reactions.value[i][0]; | ||||
| 		if (reaction in newSource && newSource[reaction] !== 0) { | ||||
| 			reactions.value[i][1] = newSource[reaction]; | ||||
| 			newReactions.push(reactions.value[i]); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	const newReactionsNames = newReactions.map(([x]) => x); | ||||
| 	newReactions = [ | ||||
| 		...newReactions, | ||||
| 		...Object.entries(newSource) | ||||
| 			.sort(([, a], [, b]) => b - a) | ||||
| 			.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)), | ||||
| 	]; | ||||
| 
 | ||||
| 	newReactions = newReactions.slice(0, props.maxNumber); | ||||
| 
 | ||||
| 	if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) { | ||||
| 		newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); | ||||
| 	} | ||||
| 
 | ||||
| 	reactions.value = newReactions; | ||||
| }, { immediate: true, deep: true }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .transition_x_move, | ||||
| .transition_x_enterActive, | ||||
| .transition_x_leaveActive { | ||||
| 	transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important; | ||||
| } | ||||
| .transition_x_enterFrom, | ||||
| .transition_x_leaveTo { | ||||
| 	opacity: 0; | ||||
| 	transform: scale(0.7); | ||||
| } | ||||
| .transition_x_leaveActive { | ||||
| 	position: absolute; | ||||
| } | ||||
| 
 | ||||
| .root { | ||||
| 	display: flex; | ||||
| 	flex-wrap: wrap; | ||||
| 	align-items: center; | ||||
| 	margin: 4px -2px 0 -2px; | ||||
| 
 | ||||
| 	&:empty { | ||||
| 		display: none; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
		Loading…
	
		Reference in New Issue