mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-12-28 06:54:10 +00:00
Merge 16c46f5aaf into a33b003282
This commit is contained in:
commit
c68ff2f8d2
@ -4,7 +4,7 @@
|
||||
-
|
||||
|
||||
### Client
|
||||
-
|
||||
- Enhance: ノート詳細ページでリプライ一覧と引用一覧を別々に表示するように
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
@ -1209,7 +1209,9 @@ youHaveUnreadAnnouncements: "未読のお知らせがあります。"
|
||||
useSecurityKey: "ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。"
|
||||
replies: "返信"
|
||||
renotes: "リノート"
|
||||
quotes: "引用"
|
||||
loadReplies: "返信を見る"
|
||||
loadQuotes: "引用を見る"
|
||||
loadConversation: "会話を見る"
|
||||
pinnedList: "ピン留めされたリスト"
|
||||
keepScreenOn: "デバイスの画面を常にオンにする"
|
||||
|
||||
@ -327,6 +327,7 @@ export * as 'notes/local-timeline' from './endpoints/notes/local-timeline.js';
|
||||
export * as 'notes/mentions' from './endpoints/notes/mentions.js';
|
||||
export * as 'notes/polls/recommendation' from './endpoints/notes/polls/recommendation.js';
|
||||
export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js';
|
||||
export * as 'notes/quotes' from './endpoints/notes/quotes.js';
|
||||
export * as 'notes/reactions' from './endpoints/notes/reactions.js';
|
||||
export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js';
|
||||
export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js';
|
||||
|
||||
77
packages/backend/src/server/api/endpoints/notes/quotes.ts
Normal file
77
packages/backend/src/server/api/endpoints/notes/quotes.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Note',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
required: ['noteId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.renoteId = :noteId', { noteId: ps.noteId })
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.text IS NOT NULL')
|
||||
.orWhere('note.fileIds != \'{}\'')
|
||||
.orWhere('note.hasPoll = TRUE');
|
||||
}))
|
||||
.andWhere('note.replyId IS NULL');
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||
|
||||
const notes = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -181,14 +181,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.tabs">
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'quotes' }]" @click="tab = 'quotes'"><i class="ti ti-quote"></i> {{ i18n.ts.quotes }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="tab === 'replies'">
|
||||
<div v-if="!repliesLoaded" style="padding: 16px">
|
||||
<MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
|
||||
<div v-if="!showReplies" style="padding: 16px">
|
||||
<MkButton style="margin: 0 auto;" primary rounded @click="showReplies = true">{{ i18n.ts.loadReplies }}</MkButton>
|
||||
</div>
|
||||
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
|
||||
<MkPagination v-else :paginator="repliesPaginator" :forceDisableInfiniteScroll="true">
|
||||
<template #default="{ items }">
|
||||
<MkNoteSub v-for="item, index in items" :key="item.id" :note="item" :class="{ [$style.replyBorder]: (index > 0) }" :detail="true"/>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
|
||||
<MkPagination :paginator="renotesPaginator" :forceDisableInfiniteScroll="true">
|
||||
@ -201,6 +206,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'quotes'" >
|
||||
<div v-if="!showQuotes" style="padding: 16px">
|
||||
<MkButton style="margin: 0 auto;" primary rounded @click="showQuotes = true">{{ i18n.ts.loadQuotes }}</MkButton>
|
||||
</div>
|
||||
<MkPagination v-else :paginator="quotesPaginator" :forceDisableInfiniteScroll="true">
|
||||
<template #default="{ items }">
|
||||
<MkNoteSub v-for="note in items" :key="note.id" :note="note" :class="$style.quote" :detail="true"/>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
|
||||
<div :class="$style.reactionTabs">
|
||||
<button v-for="reaction in Object.keys($appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
|
||||
@ -335,8 +350,9 @@ const parsed = appearNote.text ? mfm.parse(appearNote.text) : null;
|
||||
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.renote?.url !== url && appearNote.renote?.uri !== url) : null;
|
||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i?.id);
|
||||
const showReplies = ref(false);
|
||||
const showQuotes = ref(false);
|
||||
|
||||
useGlobalEvent('noteDeleted', (noteId) => {
|
||||
if (noteId === note.id || noteId === appearNote.id) {
|
||||
@ -386,6 +402,13 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||
const tab = ref(props.initialTab);
|
||||
const reactionTabType = ref<string | null>(null);
|
||||
|
||||
const repliesPaginator = markRaw(new Paginator('notes/replies', {
|
||||
limit: 10,
|
||||
params: {
|
||||
noteId: appearNote.id,
|
||||
},
|
||||
}));
|
||||
|
||||
const renotesPaginator = markRaw(new Paginator('notes/renotes', {
|
||||
limit: 10,
|
||||
params: {
|
||||
@ -393,6 +416,13 @@ const renotesPaginator = markRaw(new Paginator('notes/renotes', {
|
||||
},
|
||||
}));
|
||||
|
||||
const quotesPaginator = markRaw(new Paginator('notes/quotes', {
|
||||
limit: 10,
|
||||
params: {
|
||||
noteId: appearNote.id,
|
||||
},
|
||||
}));
|
||||
|
||||
const reactionsPaginator = markRaw(new Paginator('notes/reactions', {
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
@ -594,18 +624,6 @@ function blur() {
|
||||
rootEl.value?.blur();
|
||||
}
|
||||
|
||||
const repliesLoaded = ref(false);
|
||||
|
||||
function loadReplies() {
|
||||
repliesLoaded.value = true;
|
||||
misskeyApi('notes/children', {
|
||||
noteId: appearNote.id,
|
||||
limit: 30,
|
||||
}).then(res => {
|
||||
replies.value = res;
|
||||
});
|
||||
}
|
||||
|
||||
const conversationLoaded = ref(false);
|
||||
|
||||
function loadConversation() {
|
||||
@ -857,21 +875,34 @@ function loadConversation() {
|
||||
}
|
||||
}
|
||||
|
||||
.reply:not(:first-child) {
|
||||
.replyBorder {
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.tabsWrapper {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
display: flex;
|
||||
min-width: max-content;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
display: block;
|
||||
padding: 12px 8px;
|
||||
border-top: solid 2px transparent;
|
||||
border-bottom: solid 2px transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabActive {
|
||||
|
||||
@ -4848,10 +4848,18 @@ export interface Locale extends ILocale {
|
||||
* リノート
|
||||
*/
|
||||
"renotes": string;
|
||||
/**
|
||||
* 引用
|
||||
*/
|
||||
"quotes": string;
|
||||
/**
|
||||
* 返信を見る
|
||||
*/
|
||||
"loadReplies": string;
|
||||
/**
|
||||
* 引用を見る
|
||||
*/
|
||||
"loadQuotes": string;
|
||||
/**
|
||||
* 会話を見る
|
||||
*/
|
||||
|
||||
@ -2026,6 +2026,8 @@ declare namespace entities {
|
||||
NotesPollsRecommendationRequest,
|
||||
NotesPollsRecommendationResponse,
|
||||
NotesPollsVoteRequest,
|
||||
NotesQuotesRequest,
|
||||
NotesQuotesResponse,
|
||||
NotesReactionsRequest,
|
||||
NotesReactionsResponse,
|
||||
NotesReactionsCreateRequest,
|
||||
@ -3141,6 +3143,12 @@ type NotesPollsRecommendationResponse = operations['notes___polls___recommendati
|
||||
// @public (undocumented)
|
||||
type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesQuotesRequest = operations['notes___quotes']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesQuotesResponse = operations['notes___quotes']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesReactionsCreateRequest = operations['notes___reactions___create']['requestBody']['content']['application/json'];
|
||||
|
||||
|
||||
@ -3813,6 +3813,17 @@ declare module '../api.js' {
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
request<E extends 'notes/quotes', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
||||
@ -521,6 +521,8 @@ import type {
|
||||
NotesPollsRecommendationRequest,
|
||||
NotesPollsRecommendationResponse,
|
||||
NotesPollsVoteRequest,
|
||||
NotesQuotesRequest,
|
||||
NotesQuotesResponse,
|
||||
NotesReactionsRequest,
|
||||
NotesReactionsResponse,
|
||||
NotesReactionsCreateRequest,
|
||||
@ -1002,6 +1004,7 @@ export type Endpoints = {
|
||||
'notes/mentions': { req: NotesMentionsRequest; res: NotesMentionsResponse };
|
||||
'notes/polls/recommendation': { req: NotesPollsRecommendationRequest; res: NotesPollsRecommendationResponse };
|
||||
'notes/polls/vote': { req: NotesPollsVoteRequest; res: EmptyResponse };
|
||||
'notes/quotes': { req: NotesQuotesRequest; res: NotesQuotesResponse };
|
||||
'notes/reactions': { req: NotesReactionsRequest; res: NotesReactionsResponse };
|
||||
'notes/reactions/create': { req: NotesReactionsCreateRequest; res: EmptyResponse };
|
||||
'notes/reactions/delete': { req: NotesReactionsDeleteRequest; res: EmptyResponse };
|
||||
|
||||
@ -524,6 +524,8 @@ export type NotesMentionsResponse = operations['notes___mentions']['responses'][
|
||||
export type NotesPollsRecommendationRequest = operations['notes___polls___recommendation']['requestBody']['content']['application/json'];
|
||||
export type NotesPollsRecommendationResponse = operations['notes___polls___recommendation']['responses']['200']['content']['application/json'];
|
||||
export type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json'];
|
||||
export type NotesQuotesRequest = operations['notes___quotes']['requestBody']['content']['application/json'];
|
||||
export type NotesQuotesResponse = operations['notes___quotes']['responses']['200']['content']['application/json'];
|
||||
export type NotesReactionsRequest = operations['notes___reactions']['requestBody']['content']['application/json'];
|
||||
export type NotesReactionsResponse = operations['notes___reactions']['responses']['200']['content']['application/json'];
|
||||
export type NotesReactionsCreateRequest = operations['notes___reactions___create']['requestBody']['content']['application/json'];
|
||||
|
||||
@ -3128,6 +3128,15 @@ export type paths = {
|
||||
*/
|
||||
post: operations['notes___polls___vote'];
|
||||
};
|
||||
'/notes/quotes': {
|
||||
/**
|
||||
* notes/quotes
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
post: operations['notes___quotes'];
|
||||
};
|
||||
'/notes/reactions': {
|
||||
/**
|
||||
* notes/reactions
|
||||
@ -30416,6 +30425,80 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
notes___quotes: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
noteId: string;
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string;
|
||||
sinceDate?: number;
|
||||
untilDate?: number;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Note'][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
notes___reactions: {
|
||||
requestBody: {
|
||||
content: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user