FEAT: Allow users to view pending follow requests they sent
This commit implements the `following/requests/sent` interface firstly implemented on Firefish, and provides a UI interface to view the pending follow requests users sent.
This commit is contained in:
parent
b1073714ba
commit
0959dec291
|
@ -187,6 +187,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
|
|||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
||||
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
|
||||
import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
|
||||
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
|
||||
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
|
||||
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
|
||||
|
@ -574,6 +575,7 @@ const $following_invalidate: Provider = { provide: 'ep:following/invalidate', us
|
|||
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
|
||||
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
|
||||
const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default };
|
||||
const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default };
|
||||
const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default };
|
||||
const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default };
|
||||
const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default };
|
||||
|
@ -965,6 +967,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$following_requests_accept,
|
||||
$following_requests_cancel,
|
||||
$following_requests_list,
|
||||
$following_requests_sent,
|
||||
$following_requests_reject,
|
||||
$gallery_featured,
|
||||
$gallery_popular,
|
||||
|
|
|
@ -193,6 +193,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
|
|||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
||||
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
|
||||
import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
|
||||
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
|
||||
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
|
||||
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
|
||||
|
@ -578,6 +579,7 @@ const eps = [
|
|||
['following/requests/accept', ep___following_requests_accept],
|
||||
['following/requests/cancel', ep___following_requests_cancel],
|
||||
['following/requests/list', ep___following_requests_list],
|
||||
['following/requests/sent', ep___following_requests_sent],
|
||||
['following/requests/reject', ep___following_requests_reject],
|
||||
['gallery/featured', ep___gallery_featured],
|
||||
['gallery/popular', ep___gallery_popular],
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import type { FollowRequestsRepository } from '@/models/_.js';
|
||||
import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['following', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:following',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
follower: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
followee: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
private followRequestEntityService: FollowRequestEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
|
||||
.andWhere('request.followerId = :meId', { meId: me.id });
|
||||
|
||||
const requests = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.followRequestEntityService.packMany(requests, me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -40,7 +40,6 @@ export const navbarItemDef = reactive({
|
|||
followRequests: {
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ti ti-user-plus',
|
||||
show: computed(() => $i != null && $i.isLocked),
|
||||
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
|
||||
to: '/my/follow-requests',
|
||||
},
|
||||
|
|
|
@ -5,39 +5,43 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" indicator link preview/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div class="commands">
|
||||
<MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div :key="tab" class="_gaps">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="displayUser(req)" indicator link preview/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="displayUser(req).id" class="name" :to="userPage(displayUser(req))"><MkUserName :user="displayUser(req)"/></MkA>
|
||||
<p class="acct">@{{ acct(displayUser(req)) }}</p>
|
||||
</div>
|
||||
<div v-if="tab === 'list'" class="commands">
|
||||
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, computed } from 'vue';
|
||||
import { shallowRef, computed, ref } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { userPage, acct } from '@/filters/user.js';
|
||||
|
@ -45,29 +49,52 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
|
||||
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'following/requests/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
const pagination = computed(() => tab.value === 'list'
|
||||
? {
|
||||
endpoint: 'following/requests/list' as const,
|
||||
limit: 10,
|
||||
}
|
||||
: {
|
||||
endpoint: 'following/requests/sent' as const,
|
||||
limit: 10,
|
||||
},
|
||||
);
|
||||
|
||||
function accept(user) {
|
||||
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function reject(user) {
|
||||
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function displayUser(req) {
|
||||
return tab.value === 'list' ? req.follower : req.followee;
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
const headerTabs = computed(() => [
|
||||
{
|
||||
key: 'list',
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ph-envelope ph-bold ph-lg',
|
||||
}, {
|
||||
key: 'sent',
|
||||
title: i18n.ts.followRequestPending,
|
||||
icon: 'ph-paper-plane-tilt ph-bold ph-lg',
|
||||
},
|
||||
]);
|
||||
|
||||
const tab = ref('list');
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
|
|
|
@ -1500,6 +1500,8 @@ declare namespace entities {
|
|||
FollowingRequestsCancelResponse,
|
||||
FollowingRequestsListRequest,
|
||||
FollowingRequestsListResponse,
|
||||
FollowingRequestsSentRequest,
|
||||
FollowingRequestsSentResponse,
|
||||
FollowingRequestsRejectRequest,
|
||||
GalleryFeaturedRequest,
|
||||
GalleryFeaturedResponse,
|
||||
|
@ -2014,6 +2016,12 @@ type FollowingRequestsListResponse = operations['following___requests___list']['
|
|||
// @public (undocumented)
|
||||
type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
|
@ -2008,6 +2008,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:following*
|
||||
*/
|
||||
request<E extends 'following/requests/sent', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -278,6 +278,8 @@ import type {
|
|||
FollowingRequestsCancelResponse,
|
||||
FollowingRequestsListRequest,
|
||||
FollowingRequestsListResponse,
|
||||
FollowingRequestsSentRequest,
|
||||
FollowingRequestsSentResponse,
|
||||
FollowingRequestsRejectRequest,
|
||||
GalleryFeaturedRequest,
|
||||
GalleryFeaturedResponse,
|
||||
|
@ -760,6 +762,7 @@ export type Endpoints = {
|
|||
'following/requests/accept': { req: FollowingRequestsAcceptRequest; res: EmptyResponse };
|
||||
'following/requests/cancel': { req: FollowingRequestsCancelRequest; res: FollowingRequestsCancelResponse };
|
||||
'following/requests/list': { req: FollowingRequestsListRequest; res: FollowingRequestsListResponse };
|
||||
'following/requests/sent': { req: FollowingRequestsSentRequest; res: FollowingRequestsSentResponse };
|
||||
'following/requests/reject': { req: FollowingRequestsRejectRequest; res: EmptyResponse };
|
||||
'gallery/featured': { req: GalleryFeaturedRequest; res: GalleryFeaturedResponse };
|
||||
'gallery/popular': { req: EmptyRequest; res: GalleryPopularResponse };
|
||||
|
|
|
@ -281,6 +281,8 @@ export type FollowingRequestsCancelRequest = operations['following___requests___
|
|||
export type FollowingRequestsCancelResponse = operations['following___requests___cancel']['responses']['200']['content']['application/json'];
|
||||
export type FollowingRequestsListRequest = operations['following___requests___list']['requestBody']['content']['application/json'];
|
||||
export type FollowingRequestsListResponse = operations['following___requests___list']['responses']['200']['content']['application/json'];
|
||||
export type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json'];
|
||||
export type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json'];
|
||||
export type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json'];
|
||||
export type GalleryFeaturedRequest = operations['gallery___featured']['requestBody']['content']['application/json'];
|
||||
export type GalleryFeaturedResponse = operations['gallery___featured']['responses']['200']['content']['application/json'];
|
||||
|
|
|
@ -1753,6 +1753,15 @@ export type paths = {
|
|||
*/
|
||||
post: operations['following___requests___list'];
|
||||
};
|
||||
'/following/requests/sent': {
|
||||
/**
|
||||
* following/requests/sent
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:following*
|
||||
*/
|
||||
post: operations['following___requests___sent'];
|
||||
};
|
||||
'/following/requests/reject': {
|
||||
/**
|
||||
* following/requests/reject
|
||||
|
@ -16027,6 +16036,69 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* following/requests/sent
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:following*
|
||||
*/
|
||||
following___requests___sent: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
follower: components['schemas']['UserLite'];
|
||||
followee: components['schemas']['UserLite'];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* following/requests/reject
|
||||
* @description No description provided.
|
||||
|
|
Loading…
Reference in a new issue