Channel (#6621)
* wip * wip * wip * wip * wip * wip * wip * wip * wop * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * add notes * wip * wip * wip * wip * sound * wip * add kick_gaba2 * wip
This commit is contained in:
parent
122076e8ea
commit
9855405b89
70 changed files with 2191 additions and 184 deletions
43
src/models/entities/channel-following.ts
Normal file
43
src/models/entities/channel-following.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
import { Channel } from './channel';
|
||||
|
||||
@Entity()
|
||||
@Index(['followerId', 'followeeId'], { unique: true })
|
||||
export class ChannelFollowing {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the ChannelFollowing.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The followee channel ID.'
|
||||
})
|
||||
public followeeId: Channel['id'];
|
||||
|
||||
@ManyToOne(type => Channel, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public followee: Channel | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The follower user ID.'
|
||||
})
|
||||
public followerId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public follower: User | null;
|
||||
}
|
35
src/models/entities/channel-note-pining.ts
Normal file
35
src/models/entities/channel-note-pining.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { Note } from './note';
|
||||
import { Channel } from './channel';
|
||||
import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
@Index(['channelId', 'noteId'], { unique: true })
|
||||
export class ChannelNotePining {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the ChannelNotePining.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public channelId: Channel['id'];
|
||||
|
||||
@ManyToOne(type => Channel, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public channel: Channel | null;
|
||||
|
||||
@Column(id())
|
||||
public noteId: Note['id'];
|
||||
|
||||
@ManyToOne(type => Note, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public note: Note | null;
|
||||
}
|
74
src/models/entities/channel.ts
Normal file
74
src/models/entities/channel.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
import { DriveFile } from './drive-file';
|
||||
|
||||
@Entity()
|
||||
export class Channel {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Channel.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true
|
||||
})
|
||||
public lastNotedAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The owner ID.'
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'SET NULL'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
comment: 'The name of the Channel.'
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 2048, nullable: true,
|
||||
comment: 'The description of the Channel.'
|
||||
})
|
||||
public description: string | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
comment: 'The ID of banner Channel.'
|
||||
})
|
||||
public bannerId: DriveFile['id'] | null;
|
||||
|
||||
@ManyToOne(type => DriveFile, {
|
||||
onDelete: 'SET NULL'
|
||||
})
|
||||
@JoinColumn()
|
||||
public banner: DriveFile | null;
|
||||
|
||||
@Index()
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
comment: 'The count of notes.'
|
||||
})
|
||||
public notesCount: number;
|
||||
|
||||
@Index()
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
comment: 'The count of users.'
|
||||
})
|
||||
public usersCount: number;
|
||||
}
|
|
@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
|
|||
import { User } from './user';
|
||||
import { Note } from './note';
|
||||
import { id } from '../id';
|
||||
import { Channel } from './channel';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'noteId'], { unique: true })
|
||||
|
@ -29,15 +30,34 @@ export class NoteUnread {
|
|||
@JoinColumn()
|
||||
public note: Note | null;
|
||||
|
||||
/**
|
||||
* メンションか否か
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean')
|
||||
public isMentioned: boolean;
|
||||
|
||||
/**
|
||||
* ダイレクト投稿か否か
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean')
|
||||
public isSpecified: boolean;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: '[Denormalized]'
|
||||
})
|
||||
public noteUserId: User['id'];
|
||||
|
||||
/**
|
||||
* ダイレクト投稿か
|
||||
*/
|
||||
@Column('boolean')
|
||||
public isSpecified: boolean;
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
comment: '[Denormalized]'
|
||||
})
|
||||
public noteChannelId: Channel['id'] | null;
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { User } from './user';
|
|||
import { DriveFile } from './drive-file';
|
||||
import { id } from '../id';
|
||||
import { noteVisibilities } from '../../types';
|
||||
|
||||
import { Channel } from './channel';
|
||||
|
||||
@Entity()
|
||||
@Index('IDX_NOTE_TAGS', { synchronize: false })
|
||||
|
@ -173,6 +173,20 @@ export class Note {
|
|||
})
|
||||
public hasPoll: boolean;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true, default: null,
|
||||
comment: 'The ID of source channel.'
|
||||
})
|
||||
public channelId: Channel['id'] | null;
|
||||
|
||||
@ManyToOne(type => Channel, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public channel: Channel | null;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { DriveFileRepository } from './repositories/drive-file';
|
|||
import { DriveFolderRepository } from './repositories/drive-folder';
|
||||
import { Log } from './entities/log';
|
||||
import { AccessToken } from './entities/access-token';
|
||||
import { UserNotePining } from './entities/user-note-pinings';
|
||||
import { UserNotePining } from './entities/user-note-pining';
|
||||
import { SigninRepository } from './repositories/signin';
|
||||
import { MessagingMessageRepository } from './repositories/messaging-message';
|
||||
import { ReversiGameRepository } from './repositories/games/reversi/game';
|
||||
|
@ -53,7 +53,10 @@ import { PromoNote } from './entities/promo-note';
|
|||
import { PromoRead } from './entities/promo-read';
|
||||
import { EmojiRepository } from './repositories/emoji';
|
||||
import { RelayRepository } from './repositories/relay';
|
||||
import { ChannelRepository } from './repositories/channel';
|
||||
import { MutedNote } from './entities/muted-note';
|
||||
import { ChannelFollowing } from './entities/channel-following';
|
||||
import { ChannelNotePining } from './entities/channel-note-pining';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
|
@ -110,3 +113,6 @@ export const PromoNotes = getRepository(PromoNote);
|
|||
export const PromoReads = getRepository(PromoRead);
|
||||
export const Relays = getCustomRepository(RelayRepository);
|
||||
export const MutedNotes = getRepository(MutedNote);
|
||||
export const Channels = getCustomRepository(ChannelRepository);
|
||||
export const ChannelFollowings = getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
||||
|
|
101
src/models/repositories/channel.ts
Normal file
101
src/models/repositories/channel.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Channel } from '../entities/channel';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { DriveFiles, ChannelFollowings, NoteUnreads } from '..';
|
||||
import { User } from '../entities/user';
|
||||
|
||||
export type PackedChannel = SchemaType<typeof packedChannelSchema>;
|
||||
|
||||
@EntityRepository(Channel)
|
||||
export class ChannelRepository extends Repository<Channel> {
|
||||
public async pack(
|
||||
src: Channel['id'] | Channel,
|
||||
me?: User['id'] | User | null | undefined,
|
||||
): Promise<PackedChannel> {
|
||||
const channel = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
const meId = me ? typeof me === 'string' ? me : me.id : null;
|
||||
|
||||
const banner = channel.bannerId ? await DriveFiles.findOne(channel.bannerId) : null;
|
||||
|
||||
const hasUnreadNote = me ? (await NoteUnreads.findOne({ noteChannelId: channel.id, userId: meId })) != null : undefined;
|
||||
|
||||
const following = await ChannelFollowings.findOne({
|
||||
followerId: meId,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
createdAt: channel.createdAt.toISOString(),
|
||||
lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null,
|
||||
name: channel.name,
|
||||
description: channel.description,
|
||||
userId: channel.userId,
|
||||
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
|
||||
usersCount: channel.usersCount,
|
||||
notesCount: channel.notesCount,
|
||||
|
||||
...(me ? {
|
||||
isFollowing: following != null,
|
||||
hasUnreadNote,
|
||||
} : {})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const packedChannelSchema = {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Channel.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the Channel was created.'
|
||||
},
|
||||
lastNotedAt: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The name of the Channel.'
|
||||
},
|
||||
description: {
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
bannerUrl: {
|
||||
type: 'string' as const,
|
||||
format: 'url',
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
notesCount: {
|
||||
type: 'number' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
},
|
||||
usersCount: {
|
||||
type: 'number' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
},
|
||||
isFollowing: {
|
||||
type: 'boolean' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
},
|
||||
userId: {
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import { EntityRepository, Repository, In } from 'typeorm';
|
||||
import { Note } from '../entities/note';
|
||||
import { User } from '../entities/user';
|
||||
import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
|
||||
import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
|
@ -207,6 +207,12 @@ export class NoteRepository extends Repository<Note> {
|
|||
text = `【${note.name}】\n${(note.text || '').trim()}\n\n${note.url || note.uri}`;
|
||||
}
|
||||
|
||||
const channel = note.channelId
|
||||
? note.channel
|
||||
? note.channel
|
||||
: await Channels.findOne(note.channelId)
|
||||
: null;
|
||||
|
||||
const packed = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
|
@ -227,6 +233,11 @@ export class NoteRepository extends Repository<Note> {
|
|||
files: DriveFiles.packMany(note.fileIds),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
channelId: note.channelId || undefined,
|
||||
channel: channel ? {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
} : undefined,
|
||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri || undefined,
|
||||
url: note.url || undefined,
|
||||
|
@ -391,6 +402,16 @@ export const packedNoteSchema = {
|
|||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
},
|
||||
|
||||
channelId: {
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
channel: {
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'Channel'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import $ from 'cafy';
|
||||
import { EntityRepository, Repository, In, Not } from 'typeorm';
|
||||
import { User, ILocalUser, IRemoteUser } from '../entities/user';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes } from '..';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import config from '../../config';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
@ -107,6 +107,17 @@ export class UserRepository extends Repository<User> {
|
|||
return unread != null;
|
||||
}
|
||||
|
||||
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
||||
const channels = await ChannelFollowings.find({ followerId: userId });
|
||||
|
||||
const unread = channels.length > 0 ? await NoteUnreads.findOne({
|
||||
userId: userId,
|
||||
noteChannelId: In(channels.map(x => x.id)),
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
}
|
||||
|
||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.find({
|
||||
muterId: userId
|
||||
|
@ -139,7 +150,6 @@ export class UserRepository extends Repository<User> {
|
|||
options?: {
|
||||
detail?: boolean,
|
||||
includeSecrets?: boolean,
|
||||
includeHasUnreadNotes?: boolean
|
||||
}
|
||||
): Promise<PackedUser> {
|
||||
const opts = Object.assign({
|
||||
|
@ -181,17 +191,6 @@ export class UserRepository extends Repository<User> {
|
|||
select: ['name', 'host', 'url', 'aliases']
|
||||
}) : [],
|
||||
|
||||
...(opts.includeHasUnreadNotes ? {
|
||||
hasUnreadSpecifiedNotes: NoteUnreads.count({
|
||||
where: { userId: user.id, isSpecified: true },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
hasUnreadMentions: NoteUnreads.count({
|
||||
where: { userId: user.id },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
} : {}),
|
||||
|
||||
...(opts.detail ? {
|
||||
url: profile!.url,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
|
@ -233,8 +232,17 @@ export class UserRepository extends Repository<User> {
|
|||
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
|
||||
carefulBot: profile!.carefulBot,
|
||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
||||
hasUnreadSpecifiedNotes: NoteUnreads.count({
|
||||
where: { userId: user.id, isSpecified: true },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
hasUnreadMentions: NoteUnreads.count({
|
||||
where: { userId: user.id, isMentioned: true },
|
||||
take: 1
|
||||
}).then(count => count > 0),
|
||||
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||
hasUnreadChannel: this.getHasUnreadChannel(user.id),
|
||||
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
|
||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
|
@ -276,7 +284,6 @@ export class UserRepository extends Repository<User> {
|
|||
options?: {
|
||||
detail?: boolean,
|
||||
includeSecrets?: boolean,
|
||||
includeHasUnreadNotes?: boolean
|
||||
}
|
||||
) {
|
||||
return Promise.all(users.map(u => this.pack(u, me, options)));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue