enhance: クリップのノート数を表示するように (#13686)
* enhance: クリップのノート数を表示できるように * Update Changelog
This commit is contained in:
parent
8c5d9a6295
commit
bba3097765
@ -7,6 +7,7 @@
|
|||||||
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
|
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
|
||||||
- Enhance: アンテナでBotによるノートを除外できるように
|
- Enhance: アンテナでBotによるノートを除外できるように
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
|
||||||
|
- Enhance: クリップのノート数を表示するように
|
||||||
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
|
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@ -4944,6 +4944,10 @@ export interface Locale extends ILocale {
|
|||||||
* この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。
|
* この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。
|
||||||
*/
|
*/
|
||||||
"keepOriginalFilenameDescription": string;
|
"keepOriginalFilenameDescription": string;
|
||||||
|
/**
|
||||||
|
* 説明文はありません
|
||||||
|
*/
|
||||||
|
"noDescription": string;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -1232,6 +1232,7 @@ launchApp: "アプリを起動"
|
|||||||
useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する"
|
useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する"
|
||||||
keepOriginalFilename: "オリジナルのファイル名を保持"
|
keepOriginalFilename: "オリジナルのファイル名を保持"
|
||||||
keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。"
|
keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。"
|
||||||
|
noDescription: "説明文はありません"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
|
import type { ClipNotesRepository, ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/Blocking.js';
|
import type { } from '@/models/Blocking.js';
|
||||||
@ -20,6 +20,9 @@ export class ClipEntityService {
|
|||||||
@Inject(DI.clipsRepository)
|
@Inject(DI.clipsRepository)
|
||||||
private clipsRepository: ClipsRepository,
|
private clipsRepository: ClipsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipNotesRepository)
|
||||||
|
private clipNotesRepository: ClipNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.clipFavoritesRepository)
|
@Inject(DI.clipFavoritesRepository)
|
||||||
private clipFavoritesRepository: ClipFavoritesRepository,
|
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||||
|
|
||||||
@ -47,6 +50,7 @@ export class ClipEntityService {
|
|||||||
isPublic: clip.isPublic,
|
isPublic: clip.isPublic,
|
||||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||||
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||||
|
notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,5 +52,9 @@ export const packedClipSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
notesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -4,37 +4,59 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root" class="_panel">
|
<MkA :to="`/clips/${clip.id}`" :class="$style.link">
|
||||||
|
<div :class="$style.root" class="_panel _gaps_s">
|
||||||
<b>{{ clip.name }}</b>
|
<b>{{ clip.name }}</b>
|
||||||
<div v-if="clip.description" :class="$style.description">{{ clip.description }}</div>
|
<div :class="$style.description">
|
||||||
|
<div v-if="clip.description"><Mfm :text="clip.description" :plain="true" :nowrap="true"/></div>
|
||||||
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
|
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
|
||||||
<div :class="$style.user">
|
<div v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.divider"></div>
|
||||||
|
<div>
|
||||||
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { computed } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
import number from '@/filters/number.js';
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
clip: any;
|
clip: Misskey.entities.Clip;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const remaining = computed(() => {
|
||||||
|
return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.link {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.divider {
|
||||||
padding: 8px 0;
|
height: 1px;
|
||||||
|
background: var(--divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user {
|
.description {
|
||||||
padding-top: 16px;
|
font-size: 90%;
|
||||||
border-top: solid 0.5px var(--divider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.userAvatar {
|
.userAvatar {
|
||||||
|
@ -9,11 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSpacer :contentMax="800">
|
<MkSpacer :contentMax="800">
|
||||||
<div v-if="clip" class="_gaps">
|
<div v-if="clip" class="_gaps">
|
||||||
<div class="_panel">
|
<div class="_panel">
|
||||||
<div v-if="clip.description" :class="$style.description">
|
<div class="_gaps_s" :class="$style.description">
|
||||||
|
<div v-if="clip.description">
|
||||||
<Mfm :text="clip.description" :isNote="false"/>
|
<Mfm :text="clip.description" :isNote="false"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>({{ i18n.ts.noDescription }})</div>
|
||||||
|
<div>
|
||||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||||
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div :class="$style.user">
|
<div :class="$style.user">
|
||||||
<MkAvatar :user="clip.user" :class="$style.avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
<MkAvatar :user="clip.user" :class="$style.avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,15 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
|
|
||||||
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
|
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
|
||||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
|
<MkClipPreview v-for="item in items" :key="item.id" :clip="item"/>
|
||||||
<MkClipPreview :clip="item"/>
|
|
||||||
</MkA>
|
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
|
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
|
||||||
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
|
<MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/>
|
||||||
<MkClipPreview :clip="item"/>
|
|
||||||
</MkA>
|
|
||||||
</div>
|
</div>
|
||||||
</MkHorizontalSwipe>
|
</MkHorizontalSwipe>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -26,9 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div v-if="clips && clips.length > 0" class="_margin">
|
<div v-if="clips && clips.length > 0" class="_margin">
|
||||||
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
|
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`">
|
<MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/>
|
||||||
<MkClipPreview :clip="item"/>
|
|
||||||
</MkA>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
|
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
|
||||||
|
@ -26,6 +26,14 @@ export async function getNoteClipMenu(props: {
|
|||||||
isDeleted: Ref<boolean>;
|
isDeleted: Ref<boolean>;
|
||||||
currentClip?: Misskey.entities.Clip;
|
currentClip?: Misskey.entities.Clip;
|
||||||
}) {
|
}) {
|
||||||
|
function getClipName(clip: Misskey.entities.Clip) {
|
||||||
|
if ($i && clip.userId === $i.id && clip.notesCount != null) {
|
||||||
|
return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`;
|
||||||
|
} else {
|
||||||
|
return clip.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isRenote = (
|
const isRenote = (
|
||||||
props.note.renote != null &&
|
props.note.renote != null &&
|
||||||
props.note.text == null &&
|
props.note.text == null &&
|
||||||
@ -37,7 +45,7 @@ export async function getNoteClipMenu(props: {
|
|||||||
|
|
||||||
const clips = await clipsCache.fetch();
|
const clips = await clipsCache.fetch();
|
||||||
const menu: MenuItem[] = [...clips.map(clip => ({
|
const menu: MenuItem[] = [...clips.map(clip => ({
|
||||||
text: clip.name,
|
text: getClipName(clip),
|
||||||
action: () => {
|
action: () => {
|
||||||
claimAchievement('noteClipped1');
|
claimAchievement('noteClipped1');
|
||||||
os.promiseDialog(
|
os.promiseDialog(
|
||||||
@ -50,7 +58,18 @@ export async function getNoteClipMenu(props: {
|
|||||||
text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
|
text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
|
||||||
});
|
});
|
||||||
if (!confirm.canceled) {
|
if (!confirm.canceled) {
|
||||||
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
|
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => {
|
||||||
|
clipsCache.set(clips.map(c => {
|
||||||
|
if (c.id === clip.id) {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
|
if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -60,7 +79,18 @@ export async function getNoteClipMenu(props: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
).then(() => {
|
||||||
|
clipsCache.set(clips.map(c => {
|
||||||
|
if (c.id === clip.id) {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
notesCount: (c.notesCount ?? 0) + 1,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})), { type: 'divider' }, {
|
})), { type: 'divider' }, {
|
||||||
icon: 'ti ti-plus',
|
icon: 'ti ti-plus',
|
||||||
|
@ -4460,6 +4460,7 @@ export type components = {
|
|||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
favoritedCount: number;
|
favoritedCount: number;
|
||||||
isFavorited?: boolean;
|
isFavorited?: boolean;
|
||||||
|
notesCount?: number;
|
||||||
};
|
};
|
||||||
FederationInstance: {
|
FederationInstance: {
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
|
Loading…
Reference in New Issue
Block a user