mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-11-25 15:35:12 +00:00
parent
5d94062581
commit
3cb0cc7989
17 changed files with 327 additions and 10 deletions
|
@ -15,7 +15,7 @@
|
||||||
## 13.x.x (unreleased)
|
## 13.x.x (unreleased)
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- チャンネルをお気に入りに登録できるように
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
|
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
|
||||||
|
|
21
packages/backend/migration/1680228513388-channelFavorite.js
Normal file
21
packages/backend/migration/1680228513388-channelFavorite.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export class channelFavorite1680228513388 {
|
||||||
|
name = 'channelFavorite1680228513388'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "channel_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_59bddfd54d48689a298d41af00c" PRIMARY KEY ("id")); COMMENT ON COLUMN "channel_favorite"."createdAt" IS 'The created date of the ChannelFavorite.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d3ca0db011b75ac2a940a2337d" ON "channel_favorite" ("channelId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_8302bd27226605ece14842fb25" ON "channel_favorite" ("userId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_8302bd27226605ece14842fb25a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_8302bd27226605ece14842fb25a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_8302bd27226605ece14842fb25"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d3ca0db011b75ac2a940a2337d"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "channel_favorite"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
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 { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js';
|
import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/entities/Blocking.js';
|
import type { } from '@/models/entities/Blocking.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
|
@ -18,6 +18,9 @@ export class ChannelEntityService {
|
||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFavoritesRepository)
|
||||||
|
private channelFavoritesRepository: ChannelFavoritesRepository,
|
||||||
|
|
||||||
@Inject(DI.noteUnreadsRepository)
|
@Inject(DI.noteUnreadsRepository)
|
||||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||||
|
|
||||||
|
@ -46,6 +49,11 @@ export class ChannelEntityService {
|
||||||
followeeId: channel.id,
|
followeeId: channel.id,
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
|
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
|
||||||
|
userId: meId,
|
||||||
|
channelId: channel.id,
|
||||||
|
}) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
createdAt: channel.createdAt.toISOString(),
|
createdAt: channel.createdAt.toISOString(),
|
||||||
|
@ -59,6 +67,7 @@ export class ChannelEntityService {
|
||||||
|
|
||||||
...(me ? {
|
...(me ? {
|
||||||
isFollowing: following != null,
|
isFollowing: following != null,
|
||||||
|
isFavorited: favorite != null,
|
||||||
hasUnreadNote,
|
hasUnreadNote,
|
||||||
} : {}),
|
} : {}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -61,6 +61,7 @@ export const DI = {
|
||||||
mutedNotesRepository: Symbol('mutedNotesRepository'),
|
mutedNotesRepository: Symbol('mutedNotesRepository'),
|
||||||
channelsRepository: Symbol('channelsRepository'),
|
channelsRepository: Symbol('channelsRepository'),
|
||||||
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
||||||
|
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
||||||
channelNotePiningsRepository: Symbol('channelNotePiningsRepository'),
|
channelNotePiningsRepository: Symbol('channelNotePiningsRepository'),
|
||||||
registryItemsRepository: Symbol('registryItemsRepository'),
|
registryItemsRepository: Symbol('registryItemsRepository'),
|
||||||
webhooksRepository: Symbol('webhooksRepository'),
|
webhooksRepository: Symbol('webhooksRepository'),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
|
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
|
||||||
import type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -340,6 +340,12 @@ const $channelFollowingsRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $channelFavoritesRepository: Provider = {
|
||||||
|
provide: DI.channelFavoritesRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(ChannelFavorite),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $channelNotePiningsRepository: Provider = {
|
const $channelNotePiningsRepository: Provider = {
|
||||||
provide: DI.channelNotePiningsRepository,
|
provide: DI.channelNotePiningsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(ChannelNotePining),
|
useFactory: (db: DataSource) => db.getRepository(ChannelNotePining),
|
||||||
|
@ -460,6 +466,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$mutedNotesRepository,
|
$mutedNotesRepository,
|
||||||
$channelsRepository,
|
$channelsRepository,
|
||||||
$channelFollowingsRepository,
|
$channelFollowingsRepository,
|
||||||
|
$channelFavoritesRepository,
|
||||||
$channelNotePiningsRepository,
|
$channelNotePiningsRepository,
|
||||||
$registryItemsRepository,
|
$registryItemsRepository,
|
||||||
$webhooksRepository,
|
$webhooksRepository,
|
||||||
|
@ -528,6 +535,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$mutedNotesRepository,
|
$mutedNotesRepository,
|
||||||
$channelsRepository,
|
$channelsRepository,
|
||||||
$channelFollowingsRepository,
|
$channelFollowingsRepository,
|
||||||
|
$channelFavoritesRepository,
|
||||||
$channelNotePiningsRepository,
|
$channelNotePiningsRepository,
|
||||||
$registryItemsRepository,
|
$registryItemsRepository,
|
||||||
$webhooksRepository,
|
$webhooksRepository,
|
||||||
|
|
41
packages/backend/src/models/entities/ChannelFavorite.ts
Normal file
41
packages/backend/src/models/entities/ChannelFavorite.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { id } from '../id.js';
|
||||||
|
import { User } from './User.js';
|
||||||
|
import { Channel } from './Channel.js';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['userId', 'channelId'], { unique: true })
|
||||||
|
export class ChannelFavorite {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The created date of the ChannelFavorite.',
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
})
|
||||||
|
public channelId: Channel['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => Channel, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public channel: Channel | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
})
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User | null;
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'
|
||||||
import { AuthSession } from '@/models/entities/AuthSession.js';
|
import { AuthSession } from '@/models/entities/AuthSession.js';
|
||||||
import { Blocking } from '@/models/entities/Blocking.js';
|
import { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
||||||
|
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
|
||||||
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
||||||
import { Clip } from '@/models/entities/Clip.js';
|
import { Clip } from '@/models/entities/Clip.js';
|
||||||
import { ClipNote } from '@/models/entities/ClipNote.js';
|
import { ClipNote } from '@/models/entities/ClipNote.js';
|
||||||
|
@ -79,6 +80,7 @@ export {
|
||||||
AuthSession,
|
AuthSession,
|
||||||
Blocking,
|
Blocking,
|
||||||
ChannelFollowing,
|
ChannelFollowing,
|
||||||
|
ChannelFavorite,
|
||||||
ChannelNotePining,
|
ChannelNotePining,
|
||||||
Clip,
|
Clip,
|
||||||
ClipNote,
|
ClipNote,
|
||||||
|
@ -147,6 +149,7 @@ export type AttestationChallengesRepository = Repository<AttestationChallenge>;
|
||||||
export type AuthSessionsRepository = Repository<AuthSession>;
|
export type AuthSessionsRepository = Repository<AuthSession>;
|
||||||
export type BlockingsRepository = Repository<Blocking>;
|
export type BlockingsRepository = Repository<Blocking>;
|
||||||
export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
|
export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
|
||||||
|
export type ChannelFavoritesRepository = Repository<ChannelFavorite>;
|
||||||
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
|
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
|
||||||
export type ClipsRepository = Repository<Clip>;
|
export type ClipsRepository = Repository<Clip>;
|
||||||
export type ClipNotesRepository = Repository<ClipNote>;
|
export type ClipNotesRepository = Repository<ClipNote>;
|
||||||
|
|
|
@ -42,6 +42,10 @@ export const packedChannelSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
isFavorited: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'
|
||||||
import { AuthSession } from '@/models/entities/AuthSession.js';
|
import { AuthSession } from '@/models/entities/AuthSession.js';
|
||||||
import { Blocking } from '@/models/entities/Blocking.js';
|
import { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
||||||
|
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
|
||||||
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
||||||
import { Clip } from '@/models/entities/Clip.js';
|
import { Clip } from '@/models/entities/Clip.js';
|
||||||
import { ClipNote } from '@/models/entities/ClipNote.js';
|
import { ClipNote } from '@/models/entities/ClipNote.js';
|
||||||
|
@ -175,6 +176,7 @@ export const entities = [
|
||||||
MutedNote,
|
MutedNote,
|
||||||
Channel,
|
Channel,
|
||||||
ChannelFollowing,
|
ChannelFollowing,
|
||||||
|
ChannelFavorite,
|
||||||
ChannelNotePining,
|
ChannelNotePining,
|
||||||
RegistryItem,
|
RegistryItem,
|
||||||
Ad,
|
Ad,
|
||||||
|
|
|
@ -95,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
|
||||||
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
|
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
|
||||||
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
|
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
|
||||||
import * as ep___channels_update from './endpoints/channels/update.js';
|
import * as ep___channels_update from './endpoints/channels/update.js';
|
||||||
|
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
|
||||||
|
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
|
||||||
|
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
|
||||||
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||||
|
@ -424,6 +427,9 @@ const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___c
|
||||||
const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default };
|
const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default };
|
||||||
const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default };
|
const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default };
|
||||||
const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default };
|
const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default };
|
||||||
|
const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default };
|
||||||
|
const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
|
||||||
|
const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
|
||||||
const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
|
const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
|
||||||
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
|
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
|
||||||
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
|
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
|
||||||
|
@ -757,6 +763,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$channels_timeline,
|
$channels_timeline,
|
||||||
$channels_unfollow,
|
$channels_unfollow,
|
||||||
$channels_update,
|
$channels_update,
|
||||||
|
$channels_favorite,
|
||||||
|
$channels_unfavorite,
|
||||||
|
$channels_myFavorites,
|
||||||
$charts_activeUsers,
|
$charts_activeUsers,
|
||||||
$charts_apRequest,
|
$charts_apRequest,
|
||||||
$charts_drive,
|
$charts_drive,
|
||||||
|
@ -1084,6 +1093,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$channels_timeline,
|
$channels_timeline,
|
||||||
$channels_unfollow,
|
$channels_unfollow,
|
||||||
$channels_update,
|
$channels_update,
|
||||||
|
$channels_favorite,
|
||||||
|
$channels_unfavorite,
|
||||||
|
$channels_myFavorites,
|
||||||
$charts_activeUsers,
|
$charts_activeUsers,
|
||||||
$charts_apRequest,
|
$charts_apRequest,
|
||||||
$charts_drive,
|
$charts_drive,
|
||||||
|
|
|
@ -95,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
|
||||||
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
|
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
|
||||||
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
|
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
|
||||||
import * as ep___channels_update from './endpoints/channels/update.js';
|
import * as ep___channels_update from './endpoints/channels/update.js';
|
||||||
|
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
|
||||||
|
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
|
||||||
|
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
|
||||||
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||||
|
@ -422,6 +425,9 @@ const eps = [
|
||||||
['channels/timeline', ep___channels_timeline],
|
['channels/timeline', ep___channels_timeline],
|
||||||
['channels/unfollow', ep___channels_unfollow],
|
['channels/unfollow', ep___channels_unfollow],
|
||||||
['channels/update', ep___channels_update],
|
['channels/update', ep___channels_update],
|
||||||
|
['channels/favorite', ep___channels_favorite],
|
||||||
|
['channels/unfavorite', ep___channels_unfavorite],
|
||||||
|
['channels/my-favorites', ep___channels_myFavorites],
|
||||||
['charts/active-users', ep___charts_activeUsers],
|
['charts/active-users', ep___charts_activeUsers],
|
||||||
['charts/ap-request', ep___charts_apRequest],
|
['charts/ap-request', ep___charts_apRequest],
|
||||||
['charts/drive', ep___charts_drive],
|
['charts/drive', ep___charts_drive],
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['channels'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:channels',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchChannel: {
|
||||||
|
message: 'No such channel.',
|
||||||
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
id: '4938f5f3-6167-4c04-9149-6607b7542861',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
channelId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['channelId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.channelsRepository)
|
||||||
|
private channelsRepository: ChannelsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFavoritesRepository)
|
||||||
|
private channelFavoritesRepository: ChannelFavoritesRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
|
id: ps.channelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channel == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.channelFavoritesRepository.insert({
|
||||||
|
id: this.idService.genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
userId: me.id,
|
||||||
|
channelId: channel.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { ChannelFavoritesRepository } from '@/models/index.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['channels', 'account'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'read:channels',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'Channel',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.channelFavoritesRepository)
|
||||||
|
private channelFavoritesRepository: ChannelFavoritesRepository,
|
||||||
|
|
||||||
|
private channelEntityService: ChannelEntityService,
|
||||||
|
private queryService: QueryService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const query = this.channelFavoritesRepository.createQueryBuilder('favorite')
|
||||||
|
.andWhere('favorite.userId = :meId', { meId: me.id })
|
||||||
|
.leftJoinAndSelect('favorite.channel', 'channel');
|
||||||
|
|
||||||
|
const favorites = await query
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return await Promise.all(favorites.map(x => this.channelEntityService.pack(x.channel!, me)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['channels'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:channels',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchChannel: {
|
||||||
|
message: 'No such channel.',
|
||||||
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
id: '353c68dd-131a-476c-aa99-88a345e83668',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
channelId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['channelId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.channelsRepository)
|
||||||
|
private channelsRepository: ChannelsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFavoritesRepository)
|
||||||
|
private channelFavoritesRepository: ChannelFavoritesRepository,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
|
id: ps.channelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channel == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.channelFavoritesRepository.delete({
|
||||||
|
userId: me.id,
|
||||||
|
channelId: channel.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,9 @@
|
||||||
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
|
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
|
||||||
|
<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-star"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="channel && tab === 'timeline'" class="_gaps">
|
<div v-if="channel && tab === 'timeline'" class="_gaps">
|
||||||
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
||||||
|
@ -63,6 +66,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
let tab = $ref('timeline');
|
let tab = $ref('timeline');
|
||||||
let channel = $ref(null);
|
let channel = $ref(null);
|
||||||
|
let favorited = $ref(false);
|
||||||
const featuredPagination = $computed(() => ({
|
const featuredPagination = $computed(() => ({
|
||||||
endpoint: 'notes/featured' as const,
|
endpoint: 'notes/featured' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
@ -76,6 +80,7 @@ watch(() => props.channelId, async () => {
|
||||||
channel = await os.api('channels/show', {
|
channel = await os.api('channels/show', {
|
||||||
channelId: props.channelId,
|
channelId: props.channelId,
|
||||||
});
|
});
|
||||||
|
favorited = channel.isFavorited;
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
|
@ -90,6 +95,27 @@ function openPostForm() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function favorite() {
|
||||||
|
os.apiWithDialog('channels/favorite', {
|
||||||
|
channelId: channel.id,
|
||||||
|
}).then(() => {
|
||||||
|
favorited = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unfavorite() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.unfavoriteConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
os.apiWithDialog('channels/unfavorite', {
|
||||||
|
channelId: channel.id,
|
||||||
|
}).then(() => {
|
||||||
|
favorited = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => {
|
const headerActions = $computed(() => {
|
||||||
if (channel && channel.userId) {
|
if (channel && channel.userId) {
|
||||||
const share = {
|
const share = {
|
||||||
|
|
|
@ -2,17 +2,22 @@
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :content-max="700">
|
<MkSpacer :content-max="700">
|
||||||
<div v-if="tab === 'featured'" class="grwlizim featured">
|
<div v-if="tab === 'featured'">
|
||||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'following'" class="grwlizim following">
|
<div v-else-if="tab === 'favorites'">
|
||||||
|
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
|
||||||
|
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="tab === 'following'">
|
||||||
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
||||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'owned'" class="grwlizim owned">
|
<div v-else-if="tab === 'owned'">
|
||||||
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
|
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
|
||||||
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
||||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||||
|
@ -39,13 +44,17 @@ const featuredPagination = {
|
||||||
endpoint: 'channels/featured' as const,
|
endpoint: 'channels/featured' as const,
|
||||||
noPaging: true,
|
noPaging: true,
|
||||||
};
|
};
|
||||||
|
const favoritesPagination = {
|
||||||
|
endpoint: 'channels/my-favorites' as const,
|
||||||
|
limit: 100,
|
||||||
|
};
|
||||||
const followingPagination = {
|
const followingPagination = {
|
||||||
endpoint: 'channels/followed' as const,
|
endpoint: 'channels/followed' as const,
|
||||||
limit: 5,
|
limit: 10,
|
||||||
};
|
};
|
||||||
const ownedPagination = {
|
const ownedPagination = {
|
||||||
endpoint: 'channels/owned' as const,
|
endpoint: 'channels/owned' as const,
|
||||||
limit: 5,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
function create() {
|
function create() {
|
||||||
|
@ -62,10 +71,14 @@ const headerTabs = $computed(() => [{
|
||||||
key: 'featured',
|
key: 'featured',
|
||||||
title: i18n.ts._channel.featured,
|
title: i18n.ts._channel.featured,
|
||||||
icon: 'ti ti-comet',
|
icon: 'ti ti-comet',
|
||||||
|
}, {
|
||||||
|
key: 'favorites',
|
||||||
|
title: i18n.ts.favorites,
|
||||||
|
icon: 'ti ti-star',
|
||||||
}, {
|
}, {
|
||||||
key: 'following',
|
key: 'following',
|
||||||
title: i18n.ts._channel.following,
|
title: i18n.ts._channel.following,
|
||||||
icon: 'ti ti-heart',
|
icon: 'ti ti-eye',
|
||||||
}, {
|
}, {
|
||||||
key: 'owned',
|
key: 'owned',
|
||||||
title: i18n.ts._channel.owned,
|
title: i18n.ts._channel.owned,
|
||||||
|
|
|
@ -83,7 +83,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseChannel(ev: MouseEvent): Promise<void> {
|
async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||||
const channels = await os.api('channels/followed', {
|
const channels = await os.api('channels/my-favorites', {
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
const items = channels.map(channel => ({
|
const items = channels.map(channel => ({
|
||||||
|
|
Loading…
Reference in a new issue