Skip to content

Commit bba3097

Browse files
enhance: クリップのノート数を表示するように (misskey-dev#13686)
* enhance: クリップのノート数を表示できるように * Update Changelog
1 parent 8c5d9a6 commit bba3097

File tree

11 files changed

+99
-33
lines changed

11 files changed

+99
-33
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
88
- Enhance: アンテナでBotによるノートを除外できるように
99
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
10+
- Enhance: クリップのノート数を表示するように
1011
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
1112

1213
### Client

locales/index.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -4944,6 +4944,10 @@ export interface Locale extends ILocale {
49444944
* この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。
49454945
*/
49464946
"keepOriginalFilenameDescription": string;
4947+
/**
4948+
* 説明文はありません
4949+
*/
4950+
"noDescription": string;
49474951
"_bubbleGame": {
49484952
/**
49494953
* 遊び方

locales/ja-JP.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,7 @@ launchApp: "アプリを起動"
12321232
useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する"
12331233
keepOriginalFilename: "オリジナルのファイル名を保持"
12341234
keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。"
1235+
noDescription: "説明文はありません"
12351236

12361237
_bubbleGame:
12371238
howToPlay: "遊び方"

packages/backend/src/core/entities/ClipEntityService.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { Inject, Injectable } from '@nestjs/common';
77
import { DI } from '@/di-symbols.js';
8-
import type { ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
8+
import type { ClipNotesRepository, ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
99
import { awaitAll } from '@/misc/prelude/await-all.js';
1010
import type { Packed } from '@/misc/json-schema.js';
1111
import type { } from '@/models/Blocking.js';
@@ -20,6 +20,9 @@ export class ClipEntityService {
2020
@Inject(DI.clipsRepository)
2121
private clipsRepository: ClipsRepository,
2222

23+
@Inject(DI.clipNotesRepository)
24+
private clipNotesRepository: ClipNotesRepository,
25+
2326
@Inject(DI.clipFavoritesRepository)
2427
private clipFavoritesRepository: ClipFavoritesRepository,
2528

@@ -47,6 +50,7 @@ export class ClipEntityService {
4750
isPublic: clip.isPublic,
4851
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
4952
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
53+
notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
5054
});
5155
}
5256

packages/backend/src/models/json-schema/clip.ts

+4
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,9 @@ export const packedClipSchema = {
5252
type: 'boolean',
5353
optional: true, nullable: false,
5454
},
55+
notesCount: {
56+
type: 'integer',
57+
optional: true, nullable: false,
58+
},
5559
},
5660
} as const;

packages/frontend/src/components/MkClipPreview.vue

+37-15
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,59 @@ SPDX-License-Identifier: AGPL-3.0-only
44
-->
55

66
<template>
7-
<div :class="$style.root" class="_panel">
8-
<b>{{ clip.name }}</b>
9-
<div v-if="clip.description" :class="$style.description">{{ clip.description }}</div>
10-
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
11-
<div :class="$style.user">
12-
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
7+
<MkA :to="`/clips/${clip.id}`" :class="$style.link">
8+
<div :class="$style.root" class="_panel _gaps_s">
9+
<b>{{ clip.name }}</b>
10+
<div :class="$style.description">
11+
<div v-if="clip.description"><Mfm :text="clip.description" :plain="true" :nowrap="true"/></div>
12+
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
13+
<div v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div>
14+
</div>
15+
<div :class="$style.divider"></div>
16+
<div>
17+
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
18+
</div>
1319
</div>
14-
</div>
20+
</MkA>
1521
</template>
1622

1723
<script lang="ts" setup>
24+
import * as Misskey from 'misskey-js';
25+
import { computed } from 'vue';
1826
import { i18n } from '@/i18n.js';
27+
import { $i } from '@/account.js';
28+
import number from '@/filters/number.js';
1929

20-
defineProps<{
21-
clip: any;
30+
const props = defineProps<{
31+
clip: Misskey.entities.Clip;
2232
}>();
33+
34+
const remaining = computed(() => {
35+
return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown;
36+
});
2337
</script>
2438

2539
<style lang="scss" module>
26-
.root {
40+
.link {
2741
display: block;
42+
43+
&:hover {
44+
text-decoration: none;
45+
color: var(--accent);
46+
}
47+
}
48+
49+
.root {
2850
padding: 16px;
2951
}
3052

31-
.description {
32-
padding: 8px 0;
53+
.divider {
54+
height: 1px;
55+
background: var(--divider);
3356
}
3457

35-
.user {
36-
padding-top: 16px;
37-
border-top: solid 0.5px var(--divider);
58+
.description {
59+
font-size: 90%;
3860
}
3961

4062
.userAvatar {

packages/frontend/src/pages/clip.vue

+9-4
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
99
<MkSpacer :contentMax="800">
1010
<div v-if="clip" class="_gaps">
1111
<div class="_panel">
12-
<div v-if="clip.description" :class="$style.description">
13-
<Mfm :text="clip.description" :isNote="false"/>
12+
<div class="_gaps_s" :class="$style.description">
13+
<div v-if="clip.description">
14+
<Mfm :text="clip.description" :isNote="false"/>
15+
</div>
16+
<div v-else>({{ i18n.ts.noDescription }})</div>
17+
<div>
18+
<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>
19+
<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>
20+
</div>
1421
</div>
15-
<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>
16-
<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>
1722
<div :class="$style.user">
1823
<MkAvatar :user="clip.user" :class="$style.avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
1924
</div>

packages/frontend/src/pages/my-clips/index.vue

+3-7
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
1111
<div v-if="tab === 'my'" key="my" class="_gaps">
1212
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
1313

14-
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
15-
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
16-
<MkClipPreview :clip="item"/>
17-
</MkA>
14+
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
15+
<MkClipPreview v-for="item in items" :key="item.id" :clip="item"/>
1816
</MkPagination>
1917
</div>
2018
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
21-
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
22-
<MkClipPreview :clip="item"/>
23-
</MkA>
19+
<MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/>
2420
</div>
2521
</MkHorizontalSwipe>
2622
</MkSpacer>

packages/frontend/src/pages/note.vue

+1-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
2626
<div v-if="clips && clips.length > 0" class="_margin">
2727
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
2828
<div class="_gaps">
29-
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`">
30-
<MkClipPreview :clip="item"/>
31-
</MkA>
29+
<MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/>
3230
</div>
3331
</div>
3432
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">

packages/frontend/src/scripts/get-note-menu.ts

+33-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export async function getNoteClipMenu(props: {
2626
isDeleted: Ref<boolean>;
2727
currentClip?: Misskey.entities.Clip;
2828
}) {
29+
function getClipName(clip: Misskey.entities.Clip) {
30+
if ($i && clip.userId === $i.id && clip.notesCount != null) {
31+
return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`;
32+
} else {
33+
return clip.name;
34+
}
35+
}
36+
2937
const isRenote = (
3038
props.note.renote != null &&
3139
props.note.text == null &&
@@ -37,7 +45,7 @@ export async function getNoteClipMenu(props: {
3745

3846
const clips = await clipsCache.fetch();
3947
const menu: MenuItem[] = [...clips.map(clip => ({
40-
text: clip.name,
48+
text: getClipName(clip),
4149
action: () => {
4250
claimAchievement('noteClipped1');
4351
os.promiseDialog(
@@ -50,7 +58,18 @@ export async function getNoteClipMenu(props: {
5058
text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
5159
});
5260
if (!confirm.canceled) {
53-
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
61+
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => {
62+
clipsCache.set(clips.map(c => {
63+
if (c.id === clip.id) {
64+
return {
65+
...c,
66+
notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)),
67+
};
68+
} else {
69+
return c;
70+
}
71+
}));
72+
});
5473
if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
5574
}
5675
} else {
@@ -60,7 +79,18 @@ export async function getNoteClipMenu(props: {
6079
});
6180
}
6281
},
63-
);
82+
).then(() => {
83+
clipsCache.set(clips.map(c => {
84+
if (c.id === clip.id) {
85+
return {
86+
...c,
87+
notesCount: (c.notesCount ?? 0) + 1,
88+
};
89+
} else {
90+
return c;
91+
}
92+
}));
93+
});
6494
},
6595
})), { type: 'divider' }, {
6696
icon: 'ti ti-plus',

packages/misskey-js/src/autogen/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4460,6 +4460,7 @@ export type components = {
44604460
isPublic: boolean;
44614461
favoritedCount: number;
44624462
isFavorited?: boolean;
4463+
notesCount?: number;
44634464
};
44644465
FederationInstance: {
44654466
/** Format: id */

0 commit comments

Comments
 (0)