emoji more
This commit is contained in:
parent
bba25036fa
commit
a64c607584
|
@ -162,26 +162,30 @@ export class ReactionService {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create reaction
|
// Create reaction
|
||||||
try {
|
const exists = await this.noteReactionsRepository.findOneBy({
|
||||||
await this.noteReactionsRepository.insert(record);
|
noteId: note.id,
|
||||||
} catch (e) {
|
userId: user.id,
|
||||||
if (isDuplicateKeyValueError(e)) {
|
reaction: record.reaction,
|
||||||
const exists = await this.noteReactionsRepository.findOneByOrFail({
|
});
|
||||||
noteId: note.id,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exists.reaction !== reaction) {
|
const count = await this.noteReactionsRepository.countBy({
|
||||||
// 別のリアクションがすでにされていたら置き換える
|
noteId: note.id,
|
||||||
await this.delete(user, note);
|
userId: user.id,
|
||||||
await this.noteReactionsRepository.insert(record);
|
});
|
||||||
} else {
|
|
||||||
// 同じリアクションがすでにされていたらエラー
|
if (count > 3) {
|
||||||
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exists == null) {
|
||||||
|
if (user.host == null) {
|
||||||
|
await this.noteReactionsRepository.insert(record);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 同じリアクションがすでにされていたらエラー
|
||||||
|
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment reactions count
|
// Increment reactions count
|
||||||
|
@ -275,17 +279,24 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) {
|
public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, reaction?: string) {
|
||||||
// if already unreacted
|
// if already unreacted
|
||||||
const exist = await this.noteReactionsRepository.findOneBy({
|
let exist;
|
||||||
noteId: note.id,
|
if (reaction == null) {
|
||||||
userId: user.id,
|
exist = await this.noteReactionsRepository.findOneBy({
|
||||||
});
|
noteId: note.id,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
exist = await this.noteReactionsRepository.findOneBy({
|
||||||
|
noteId: note.id,
|
||||||
|
userId: user.id,
|
||||||
|
reaction: reaction.replace(/@./, ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
if (exist == null) {
|
if (exist == null) {
|
||||||
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete reaction
|
// Delete reaction
|
||||||
const result = await this.noteReactionsRepository.delete(exist.id);
|
const result = await this.noteReactionsRepository.delete(exist.id);
|
||||||
|
|
||||||
|
|
|
@ -209,6 +209,30 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@bindThis
|
||||||
|
public async populateMyReactions(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
|
||||||
|
myReactions: Map<MiNote['id'], string | null>;
|
||||||
|
}) {
|
||||||
|
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
if (reactionsCount === 0) return undefined;
|
||||||
|
|
||||||
|
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
||||||
|
if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reactions = await this.noteReactionsRepository.findBy({
|
||||||
|
userId: meId,
|
||||||
|
noteId: note.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reactions.length > 0) {
|
||||||
|
return reactions.map(reaction => this.reactionService.convertLegacyReaction(reaction.reaction));
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
|
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
|
||||||
|
@ -382,6 +406,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
...(meId && Object.keys(note.reactions).length > 0 ? {
|
...(meId && Object.keys(note.reactions).length > 0 ? {
|
||||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||||
|
myReactions: this.populateMyReactions(note, meId, options?._hint_),
|
||||||
} : {}),
|
} : {}),
|
||||||
} : {}),
|
} : {}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,8 @@ import { MiUser } from './User.js';
|
||||||
import { MiNote } from './Note.js';
|
import { MiNote } from './Note.js';
|
||||||
|
|
||||||
@Entity('note_reaction')
|
@Entity('note_reaction')
|
||||||
@Index(['userId', 'noteId'], { unique: true })
|
@Index(['userId', 'noteId', 'reaction'], { unique: true })
|
||||||
|
@Index(['userId', 'noteId'])
|
||||||
export class MiNoteReaction {
|
export class MiNoteReaction {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
|
@ -42,6 +42,7 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
noteId: { type: 'string', format: 'misskey:id' },
|
noteId: { type: 'string', format: 'misskey:id' },
|
||||||
|
reaction: { type: 'string' },
|
||||||
},
|
},
|
||||||
required: ['noteId'],
|
required: ['noteId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -57,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
await this.reactionService.delete(me, note).catch(err => {
|
await this.reactionService.delete(me, note, ps.reaction ?? undefined).catch(err => {
|
||||||
if (err.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
|
if (err.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,43 +4,45 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!hardMuted && muted === false"
|
v-if="!hardMuted && muted === false"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="[$style.root,
|
:class="[$style.root,
|
||||||
{ [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover } ,
|
{ [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover } ,
|
||||||
{[$style.home] : defaultStore.state.showVisibilityColor && note.visibility === 'home'
|
{[$style.home] : defaultStore.state.showVisibilityColor && note.visibility === 'home'
|
||||||
,[$style.followers] : defaultStore.state.showVisibilityColor && note.visibility === 'followers'
|
,[$style.followers] : defaultStore.state.showVisibilityColor && note.visibility === 'followers'
|
||||||
,[$style.specified] : defaultStore.state.showVisibilityColor && note.visibility === 'specified'
|
,[$style.specified] : defaultStore.state.showVisibilityColor && note.visibility === 'specified'
|
||||||
},{[$style.localonly] : defaultStore.state.showVisibilityColor && note.localOnly }
|
},{[$style.localonly] : defaultStore.state.showVisibilityColor && note.localOnly }
|
||||||
]"
|
]"
|
||||||
|
|
||||||
:tabindex="!isDeleted ? '-1' : undefined"
|
:tabindex="!isDeleted ? '-1' : undefined"
|
||||||
>
|
>
|
||||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||||
<!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
|
<!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
|
||||||
<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
|
<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
|
||||||
<div v-if="isRenote" :class="$style.renote">
|
<div v-if="isRenote" :class="$style.renote">
|
||||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||||
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
|
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
|
||||||
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
|
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
|
||||||
<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
|
<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
|
||||||
<template #user>
|
<template #user>
|
||||||
<MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)">
|
<MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)">
|
||||||
<MkUserName :user="note.user"/>
|
<MkUserName :user="note.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
<div :class="$style.renoteInfo">
|
<div :class="$style.renoteInfo">
|
||||||
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
|
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
|
||||||
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;"
|
<span
|
||||||
:title="i18n.ts._visibility[note.visibility]">
|
v-if="note.visibility !== 'public'" style="margin-left: 0.5em;"
|
||||||
|
:title="i18n.ts._visibility[note.visibility]"
|
||||||
|
>
|
||||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
@ -127,8 +129,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReactions?.length >= 3 " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
<i v-else-if="appearNote.myReactions?.length >= 3 " class="ti ti-minus" style="color: var(--accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||||
|
@ -168,7 +170,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
|
import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
|
@ -182,33 +184,33 @@ import MkPoll from '@/components/MkPoll.vue';
|
||||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import {pleaseLogin} from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import {focusPrev, focusNext} from '@/scripts/focus.js';
|
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||||
import {checkWordMute} from '@/scripts/check-word-mute.js';
|
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||||
import {userPage} from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import {defaultStore, noteViewInterruptors} from '@/store.js';
|
import { defaultStore, noteViewInterruptors } from '@/store.js';
|
||||||
import {reactionPicker} from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import {extractUrlFromMfm} from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
import {$i} from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import {i18n} from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import {getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu} from '@/scripts/get-note-menu.js';
|
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||||
import {useNoteCapture} from '@/scripts/use-note-capture.js';
|
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||||
import {deepClone} from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
import {useTooltip} from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import {claimAchievement} from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import {getNoteSummary} from '@/scripts/get-note-summary.js';
|
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||||
import {MenuItem} from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import {showMovedDialog} from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
import {shouldCollapsed} from '@/scripts/collapsed.js';
|
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note & {myReactions: string[]};
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
mock?: boolean;
|
mock?: boolean;
|
||||||
withHardMute?: boolean;
|
withHardMute?: boolean;
|
||||||
|
@ -416,7 +418,7 @@ function react(viaKeyboard = false): void {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
const y = rect.top + (el.offsetHeight / 2);
|
||||||
os.popup(MkRippleEffect, {x, y}, {}, 'end');
|
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
|
@ -441,25 +443,9 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(targetNote: Misskey.entities.Note): void {
|
|
||||||
const oldReaction = targetNote.myReaction;
|
|
||||||
if (!oldReaction) return;
|
|
||||||
|
|
||||||
if (props.mock) {
|
|
||||||
emit('removeReaction', oldReaction);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
misskeyApi('notes/reactions/delete', {
|
|
||||||
noteId: targetNote.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleReact() {
|
function toggleReact() {
|
||||||
if (appearNote.value.myReaction == null) {
|
if (appearNote.value.myReactions?.length > 3 || !appearNote.value.myReactions ) {
|
||||||
react();
|
react();
|
||||||
} else {
|
|
||||||
undoReact(appearNote.value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -485,13 +471,13 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
const {menu, cleanup} = getNoteMenu({
|
const { menu, cleanup } = getNoteMenu({
|
||||||
note: note.value,
|
note: note.value,
|
||||||
translating,
|
translating,
|
||||||
translation,
|
translation,
|
||||||
|
|
||||||
isDeleted,
|
isDeleted,
|
||||||
currentClip: currentClip?.value
|
currentClip: currentClip?.value,
|
||||||
});
|
});
|
||||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
@ -502,13 +488,13 @@ function showMenu(viaKeyboard = false): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {menu, cleanup} = getNoteMenu({
|
const { menu, cleanup } = getNoteMenu({
|
||||||
note: note.value,
|
note: note.value,
|
||||||
translating,
|
translating,
|
||||||
translation,
|
translation,
|
||||||
|
|
||||||
isDeleted,
|
isDeleted,
|
||||||
currentClip: currentClip?.value
|
currentClip: currentClip?.value,
|
||||||
});
|
});
|
||||||
os.popupMenu(menu, menuButton.value, {
|
os.popupMenu(menu, menuButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
|
@ -523,7 +509,7 @@ async function clip() {
|
||||||
os.popupMenu(await getNoteClipMenu({
|
os.popupMenu(await getNoteClipMenu({
|
||||||
note: note.value,
|
note: note.value,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
currentClip: currentClip?.value
|
currentClip: currentClip?.value,
|
||||||
}), clipButton.value).then(focus);
|
}), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -583,13 +569,6 @@ function focusAfter() {
|
||||||
focusNext(rootEl.value ?? null);
|
focusNext(rootEl.value ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPromo() {
|
|
||||||
misskeyApi('promo/read', {
|
|
||||||
noteId: appearNote.value.id,
|
|
||||||
});
|
|
||||||
isDeleted.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitUpdReaction(emoji: string, delta: number) {
|
function emitUpdReaction(emoji: string, delta: number) {
|
||||||
if (delta < 0) {
|
if (delta < 0) {
|
||||||
emit('removeReaction', emoji);
|
emit('removeReaction', emoji);
|
||||||
|
|
|
@ -129,8 +129,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReactions?.length >= 3 " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
<i v-else-if="appearNote.myReactions?.length >= 3 " class="ti ti-minus" style="color: var(--accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||||
|
@ -257,13 +257,11 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { infoImageUrl, instance } from '@/instance.js';
|
import { infoImageUrl, instance, isEnabledUrlPreview } from '@/instance.js';
|
||||||
import MkPostForm from '@/components/MkPostFormSimple.vue';
|
import MkPostForm from '@/components/MkPostFormSimple.vue';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
|
|
||||||
const MOBILE_THRESHOLD = 500;
|
const MOBILE_THRESHOLD = 500;
|
||||||
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
|
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -504,10 +502,8 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReact() {
|
function toggleReact() {
|
||||||
if (appearNote.value.myReaction == null) {
|
if (appearNote.value.myReactions?.length > 3 || !appearNote.value.myReactions ) {
|
||||||
react();
|
react();
|
||||||
} else {
|
|
||||||
undoReact(appearNote.value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="buttonEl"
|
ref="buttonEl"
|
||||||
v-ripple="canToggle"
|
v-ripple="canToggle"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="[$style.root, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' ,[$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
:class="[$style.root, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' ,[$style.reacted]: note.myReactions?.includes(reaction) , [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
@contextmenu.prevent.stop="menu"
|
@contextmenu.prevent.stop="menu"
|
||||||
>
|
>
|
||||||
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, inject, onMounted, ref, shallowRef, watch} from 'vue';
|
import { computed, inject, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||||
|
@ -42,7 +42,9 @@ const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
count: number;
|
count: number;
|
||||||
isInitial: boolean;
|
isInitial: boolean;
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note & {
|
||||||
|
myReactions: string[];
|
||||||
|
}
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const mock = inject<boolean>('mock', false);
|
const mock = inject<boolean>('mock', false);
|
||||||
|
@ -64,14 +66,15 @@ const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction
|
||||||
async function toggleReaction() {
|
async function toggleReaction() {
|
||||||
if (!canToggle.value) return;
|
if (!canToggle.value) return;
|
||||||
|
|
||||||
const oldReaction = props.note.myReaction;
|
const oldReaction = props.note.myReactions.includes(props.reaction) ? props.reaction : null;
|
||||||
|
console.log(oldReaction);
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm,
|
text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm,
|
||||||
});
|
});
|
||||||
if (confirm.canceled) return;
|
if (confirm.canceled) return;
|
||||||
|
props.note.myReactions.splice(props.note.myReactions.indexOf(oldReaction), 1);
|
||||||
if (oldReaction !== props.reaction) {
|
if (oldReaction !== props.reaction) {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
}
|
}
|
||||||
|
@ -83,8 +86,9 @@ async function toggleReaction() {
|
||||||
|
|
||||||
misskeyApi('notes/reactions/delete', {
|
misskeyApi('notes/reactions/delete', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
|
reaction: oldReaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (oldReaction !== props.reaction) {
|
if (oldReaction !== props.reaction ) {
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
|
|
|
@ -24,7 +24,9 @@ import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note & {
|
||||||
|
myReactions: string[];
|
||||||
|
}
|
||||||
maxNumber?: number;
|
maxNumber?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
maxNumber: Infinity,
|
maxNumber: Infinity,
|
||||||
|
@ -57,7 +59,6 @@ function onMockToggleReaction(emoji: string, count: number) {
|
||||||
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
||||||
let newReactions: [string, number][] = [];
|
let newReactions: [string, number][] = [];
|
||||||
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
||||||
|
|
||||||
for (let i = 0; i < reactions.value.length; i++) {
|
for (let i = 0; i < reactions.value.length; i++) {
|
||||||
const reaction = reactions.value[i][0];
|
const reaction = reactions.value[i][0];
|
||||||
if (reaction in newSource && newSource[reaction] !== 0) {
|
if (reaction in newSource && newSource[reaction] !== 0) {
|
||||||
|
@ -76,7 +77,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
||||||
|
|
||||||
newReactions = newReactions.slice(0, props.maxNumber);
|
newReactions = newReactions.slice(0, props.maxNumber);
|
||||||
|
|
||||||
if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
|
if (props.note. myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
|
||||||
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
|
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,9 @@ const props = withDefaults(defineProps<{
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
.text{
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
.spinner {
|
.spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -39,6 +39,13 @@ export function useNoteCapture(props: {
|
||||||
|
|
||||||
if ($i && (body.userId === $i.id)) {
|
if ($i && (body.userId === $i.id)) {
|
||||||
note.value.myReaction = reaction;
|
note.value.myReaction = reaction;
|
||||||
|
console.log(note.value.myReactions);
|
||||||
|
if (!note.value.myReactions) {
|
||||||
|
note.value.myReactions = [];
|
||||||
|
note.value.myReactions.push(reaction);
|
||||||
|
} else if (!note.value.myReactions.includes(reaction)) {
|
||||||
|
note.value.myReactions.push(reaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -55,6 +62,7 @@ export function useNoteCapture(props: {
|
||||||
|
|
||||||
if ($i && (body.userId === $i.id)) {
|
if ($i && (body.userId === $i.id)) {
|
||||||
note.value.myReaction = null;
|
note.value.myReaction = null;
|
||||||
|
note.value.myReactions = note.value.myReactions.filter(r => r !== reaction);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue