parent
81d2c5a4a7
commit
5d56799070
21 changed files with 348 additions and 8 deletions
|
@ -15,7 +15,9 @@
|
||||||
## 13.x.x (unreleased)
|
## 13.x.x (unreleased)
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- カスタム絵文字関連の変更
|
- 指定したロールを持つユーザーのノートのみが流れるロールタイムラインを追加
|
||||||
|
- Deckのカラムとしても追加可能
|
||||||
|
- カスタム絵文字関連の改善
|
||||||
* ノートなどに含まれるemojis(populateEmojiの結果)は(プロキシされたURLではなく)オリジナルのURLを指すように
|
* ノートなどに含まれるemojis(populateEmojiの結果)は(プロキシされたURLではなく)オリジナルのURLを指すように
|
||||||
* MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用するように
|
* MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用するように
|
||||||
|
|
||||||
|
|
|
@ -1943,6 +1943,7 @@ _deck:
|
||||||
channel: "チャンネル"
|
channel: "チャンネル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
direct: "ダイレクト"
|
direct: "ダイレクト"
|
||||||
|
roleTimeline: "ロールタイムライン"
|
||||||
|
|
||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||||
|
|
|
@ -14,11 +14,13 @@ import type {
|
||||||
MainStreamTypes,
|
MainStreamTypes,
|
||||||
NoteStreamTypes,
|
NoteStreamTypes,
|
||||||
UserListStreamTypes,
|
UserListStreamTypes,
|
||||||
|
RoleTimelineStreamTypes,
|
||||||
} from '@/server/api/stream/types.js';
|
} from '@/server/api/stream/types.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { Role } from '@/models';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalEventService {
|
export class GlobalEventService {
|
||||||
|
@ -81,6 +83,11 @@ export class GlobalEventService {
|
||||||
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: Role['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
|
||||||
|
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishNotesStream(note: Packed<'Note'>): void {
|
public publishNotesStream(note: Packed<'Note'>): void {
|
||||||
this.publish('notesStream', null, note);
|
this.publish('notesStream', null, note);
|
||||||
|
|
|
@ -547,6 +547,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
this.globalEventService.publishNotesStream(noteObj);
|
this.globalEventService.publishNotesStream(noteObj);
|
||||||
|
|
||||||
|
this.roleService.addNoteToRoleTimeline(noteObj);
|
||||||
|
|
||||||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
|
@ -64,6 +65,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
public static NotAssignedError = class extends Error {};
|
public static NotAssignedError = class extends Error {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.redisForSub)
|
@Inject(DI.redisForSub)
|
||||||
private redisForSub: Redis.Redis,
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
|
@ -398,6 +402,25 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
|
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
|
||||||
|
const roles = await this.getUserRoles(note.userId);
|
||||||
|
|
||||||
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`roleTimeline:${role.id}`,
|
||||||
|
'MAXLEN', '~', '1000',
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPipeline.exec();
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public onApplicationShutdown(signal?: string | undefined) {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
||||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
|
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -67,6 +68,7 @@ import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
DriveChannelService,
|
DriveChannelService,
|
||||||
GlobalTimelineChannelService,
|
GlobalTimelineChannelService,
|
||||||
HashtagChannelService,
|
HashtagChannelService,
|
||||||
|
RoleTimelineChannelService,
|
||||||
HomeTimelineChannelService,
|
HomeTimelineChannelService,
|
||||||
HybridTimelineChannelService,
|
HybridTimelineChannelService,
|
||||||
LocalTimelineChannelService,
|
LocalTimelineChannelService,
|
||||||
|
|
|
@ -294,6 +294,7 @@ import * as ep___promo_read from './endpoints/promo/read.js';
|
||||||
import * as ep___roles_list from './endpoints/roles/list.js';
|
import * as ep___roles_list from './endpoints/roles/list.js';
|
||||||
import * as ep___roles_show from './endpoints/roles/show.js';
|
import * as ep___roles_show from './endpoints/roles/show.js';
|
||||||
import * as ep___roles_users from './endpoints/roles/users.js';
|
import * as ep___roles_users from './endpoints/roles/users.js';
|
||||||
|
import * as ep___roles_notes from './endpoints/roles/notes.js';
|
||||||
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
|
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
|
||||||
import * as ep___resetDb from './endpoints/reset-db.js';
|
import * as ep___resetDb from './endpoints/reset-db.js';
|
||||||
import * as ep___resetPassword from './endpoints/reset-password.js';
|
import * as ep___resetPassword from './endpoints/reset-password.js';
|
||||||
|
@ -628,6 +629,7 @@ const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_r
|
||||||
const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default };
|
const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default };
|
||||||
const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default };
|
const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default };
|
||||||
const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default };
|
const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default };
|
||||||
|
const $roles_notes: Provider = { provide: 'ep:roles/notes', useClass: ep___roles_notes.default };
|
||||||
const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default };
|
const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default };
|
||||||
const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
|
const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
|
||||||
const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
|
const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
|
||||||
|
@ -966,6 +968,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$roles_list,
|
$roles_list,
|
||||||
$roles_show,
|
$roles_show,
|
||||||
$roles_users,
|
$roles_users,
|
||||||
|
$roles_notes,
|
||||||
$requestResetPassword,
|
$requestResetPassword,
|
||||||
$resetDb,
|
$resetDb,
|
||||||
$resetPassword,
|
$resetPassword,
|
||||||
|
@ -1298,6 +1301,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$roles_list,
|
$roles_list,
|
||||||
$roles_show,
|
$roles_show,
|
||||||
$roles_users,
|
$roles_users,
|
||||||
|
$roles_notes,
|
||||||
$requestResetPassword,
|
$requestResetPassword,
|
||||||
$resetDb,
|
$resetDb,
|
||||||
$resetPassword,
|
$resetPassword,
|
||||||
|
|
|
@ -294,6 +294,7 @@ import * as ep___promo_read from './endpoints/promo/read.js';
|
||||||
import * as ep___roles_list from './endpoints/roles/list.js';
|
import * as ep___roles_list from './endpoints/roles/list.js';
|
||||||
import * as ep___roles_show from './endpoints/roles/show.js';
|
import * as ep___roles_show from './endpoints/roles/show.js';
|
||||||
import * as ep___roles_users from './endpoints/roles/users.js';
|
import * as ep___roles_users from './endpoints/roles/users.js';
|
||||||
|
import * as ep___roles_notes from './endpoints/roles/notes.js';
|
||||||
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
|
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
|
||||||
import * as ep___resetDb from './endpoints/reset-db.js';
|
import * as ep___resetDb from './endpoints/reset-db.js';
|
||||||
import * as ep___resetPassword from './endpoints/reset-password.js';
|
import * as ep___resetPassword from './endpoints/reset-password.js';
|
||||||
|
@ -626,6 +627,7 @@ const eps = [
|
||||||
['roles/list', ep___roles_list],
|
['roles/list', ep___roles_list],
|
||||||
['roles/show', ep___roles_show],
|
['roles/show', ep___roles_show],
|
||||||
['roles/users', ep___roles_users],
|
['roles/users', ep___roles_users],
|
||||||
|
['roles/notes', ep___roles_notes],
|
||||||
['request-reset-password', ep___requestResetPassword],
|
['request-reset-password', ep___requestResetPassword],
|
||||||
['reset-db', ep___resetDb],
|
['reset-db', ep___resetDb],
|
||||||
['reset-password', ep___resetPassword],
|
['reset-password', ep___resetPassword],
|
||||||
|
|
|
@ -76,11 +76,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchAntenna);
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
const noteIdsRes = await this.redisClient.xrevrange(
|
const noteIdsRes = await this.redisClient.xrevrange(
|
||||||
`antennaTimeline:${antenna.id}`,
|
`antennaTimeline:${antenna.id}`,
|
||||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||||
'-',
|
'-',
|
||||||
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
'COUNT', limit);
|
||||||
|
|
||||||
if (noteIdsRes.length === 0) {
|
if (noteIdsRes.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -91,11 +91,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||||
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||||
|
|
||||||
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
const notificationsRes = await this.redisClient.xrevrange(
|
const notificationsRes = await this.redisClient.xrevrange(
|
||||||
`notificationTimeline:${me.id}`,
|
`notificationTimeline:${me.id}`,
|
||||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||||
'-',
|
'-',
|
||||||
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
'COUNT', limit);
|
||||||
|
|
||||||
if (notificationsRes.length === 0) {
|
if (notificationsRes.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
109
packages/backend/src/server/api/endpoints/roles/notes.ts
Normal file
109
packages/backend/src/server/api/endpoints/roles/notes.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { NotesRepository, RolesRepository } from '@/models/index.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['role', 'notes'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchRole: {
|
||||||
|
message: 'No such role.',
|
||||||
|
code: 'NO_SUCH_ROLE',
|
||||||
|
id: 'eb70323a-df61-4dd4-ad90-89c83c7cf26e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'Note',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
roleId: { type: 'string', format: 'misskey:id' },
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
sinceDate: { type: 'integer' },
|
||||||
|
untilDate: { type: 'integer' },
|
||||||
|
},
|
||||||
|
required: ['roleId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.rolesRepository)
|
||||||
|
private rolesRepository: RolesRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
|
private noteEntityService: NoteEntityService,
|
||||||
|
private queryService: QueryService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const role = await this.rolesRepository.findOneBy({
|
||||||
|
id: ps.roleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (role == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
|
const noteIdsRes = await this.redisClient.xrevrange(
|
||||||
|
`roleTimeline:${role.id}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit);
|
||||||
|
|
||||||
|
if (noteIdsRes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||||
|
|
||||||
|
if (noteIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
|
||||||
|
const notes = await query.getMany();
|
||||||
|
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(notes, me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { UserListChannelService } from './channels/user-list.js';
|
||||||
import { AntennaChannelService } from './channels/antenna.js';
|
import { AntennaChannelService } from './channels/antenna.js';
|
||||||
import { DriveChannelService } from './channels/drive.js';
|
import { DriveChannelService } from './channels/drive.js';
|
||||||
import { HashtagChannelService } from './channels/hashtag.js';
|
import { HashtagChannelService } from './channels/hashtag.js';
|
||||||
|
import { RoleTimelineChannelService } from './channels/role-timeline.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChannelsService {
|
export class ChannelsService {
|
||||||
|
@ -24,6 +25,7 @@ export class ChannelsService {
|
||||||
private globalTimelineChannelService: GlobalTimelineChannelService,
|
private globalTimelineChannelService: GlobalTimelineChannelService,
|
||||||
private userListChannelService: UserListChannelService,
|
private userListChannelService: UserListChannelService,
|
||||||
private hashtagChannelService: HashtagChannelService,
|
private hashtagChannelService: HashtagChannelService,
|
||||||
|
private roleTimelineChannelService: RoleTimelineChannelService,
|
||||||
private antennaChannelService: AntennaChannelService,
|
private antennaChannelService: AntennaChannelService,
|
||||||
private channelChannelService: ChannelChannelService,
|
private channelChannelService: ChannelChannelService,
|
||||||
private driveChannelService: DriveChannelService,
|
private driveChannelService: DriveChannelService,
|
||||||
|
@ -43,6 +45,7 @@ export class ChannelsService {
|
||||||
case 'globalTimeline': return this.globalTimelineChannelService;
|
case 'globalTimeline': return this.globalTimelineChannelService;
|
||||||
case 'userList': return this.userListChannelService;
|
case 'userList': return this.userListChannelService;
|
||||||
case 'hashtag': return this.hashtagChannelService;
|
case 'hashtag': return this.hashtagChannelService;
|
||||||
|
case 'roleTimeline': return this.roleTimelineChannelService;
|
||||||
case 'antenna': return this.antennaChannelService;
|
case 'antenna': return this.antennaChannelService;
|
||||||
case 'channel': return this.channelChannelService;
|
case 'channel': return this.channelChannelService;
|
||||||
case 'drive': return this.driveChannelService;
|
case 'drive': return this.driveChannelService;
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import Channel from '../channel.js';
|
||||||
|
import { StreamMessages } from '../types.js';
|
||||||
|
|
||||||
|
class RoleTimelineChannel extends Channel {
|
||||||
|
public readonly chName = 'roleTimeline';
|
||||||
|
public static shouldShare = false;
|
||||||
|
public static requireCredential = false;
|
||||||
|
private roleId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private noteEntityService: NoteEntityService,
|
||||||
|
|
||||||
|
id: string,
|
||||||
|
connection: Channel['connection'],
|
||||||
|
) {
|
||||||
|
super(id, connection);
|
||||||
|
//this.onNote = this.onNote.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async init(params: any) {
|
||||||
|
this.roleId = params.roleId as string;
|
||||||
|
|
||||||
|
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onEvent(data: StreamMessages['roleTimeline']['payload']) {
|
||||||
|
if (data.type === 'note') {
|
||||||
|
const note = data.body;
|
||||||
|
|
||||||
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
|
this.send('note', note);
|
||||||
|
} else {
|
||||||
|
this.send(data.type, data.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose() {
|
||||||
|
// Unsubscribe events
|
||||||
|
this.subscriber.off(`roleTimelineStream:${this.roleId}`, this.onEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleTimelineChannelService {
|
||||||
|
public readonly shouldShare = RoleTimelineChannel.shouldShare;
|
||||||
|
public readonly requireCredential = RoleTimelineChannel.requireCredential;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private noteEntityService: NoteEntityService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
|
||||||
|
return new RoleTimelineChannel(
|
||||||
|
this.noteEntityService,
|
||||||
|
id,
|
||||||
|
connection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,6 +148,10 @@ export interface AntennaStreamTypes {
|
||||||
note: Note;
|
note: Note;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoleTimelineStreamTypes {
|
||||||
|
note: Packed<'Note'>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminStreamTypes {
|
export interface AdminStreamTypes {
|
||||||
newAbuseUserReport: {
|
newAbuseUserReport: {
|
||||||
id: AbuseUserReport['id'];
|
id: AbuseUserReport['id'];
|
||||||
|
@ -209,6 +213,10 @@ export type StreamMessages = {
|
||||||
name: `userListStream:${UserList['id']}`;
|
name: `userListStream:${UserList['id']}`;
|
||||||
payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
|
payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
|
||||||
};
|
};
|
||||||
|
roleTimeline: {
|
||||||
|
name: `roleTimelineStream:${Role['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineStreamTypes>>;
|
||||||
|
};
|
||||||
antenna: {
|
antenna: {
|
||||||
name: `antennaStream:${Antenna['id']}`;
|
name: `antennaStream:${Antenna['id']}`;
|
||||||
payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
|
payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
|
||||||
|
|
|
@ -15,6 +15,7 @@ const props = defineProps<{
|
||||||
list?: string;
|
list?: string;
|
||||||
antenna?: string;
|
antenna?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
role?: string;
|
||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -121,6 +122,15 @@ if (props.src === 'antenna') {
|
||||||
channelId: props.channel,
|
channelId: props.channel,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
|
} else if (props.src === 'role') {
|
||||||
|
endpoint = 'roles/notes';
|
||||||
|
query = {
|
||||||
|
roleId: props.role,
|
||||||
|
};
|
||||||
|
connection = stream.useChannel('roleTimeline', {
|
||||||
|
roleId: props.role,
|
||||||
|
});
|
||||||
|
connection.on('note', prepend);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<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>
|
||||||
<div class="lznhrdub">
|
<div>
|
||||||
<div v-if="tab === 'featured'">
|
<div v-if="tab === 'featured'">
|
||||||
<XFeatured/>
|
<XFeatured/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
|
||||||
|
|
||||||
<MkSpacer :content-max="1200">
|
<MkSpacer v-if="tab === 'users'" :content-max="1200">
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<div v-if="role">{{ role.description }}</div>
|
<div v-if="role">{{ role.description }}</div>
|
||||||
<MkUserList :pagination="users" :extractor="(item) => item.user"/>
|
<MkUserList :pagination="users" :extractor="(item) => item.user"/>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
<MkSpacer v-else-if="tab === 'timeline'" :content-max="700">
|
||||||
|
<MkTimeline ref="timeline" src="role" :role="props.role"/>
|
||||||
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,11 +19,17 @@ import { computed, watch } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import MkUserList from '@/components/MkUserList.vue';
|
import MkUserList from '@/components/MkUserList.vue';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
role: string;
|
role: string;
|
||||||
}>();
|
initialTab?: string;
|
||||||
|
}>(), {
|
||||||
|
initialTab: 'users',
|
||||||
|
});
|
||||||
|
|
||||||
|
let tab = $ref(props.initialTab);
|
||||||
let role = $ref();
|
let role = $ref();
|
||||||
|
|
||||||
watch(() => props.role, () => {
|
watch(() => props.role, () => {
|
||||||
|
@ -39,6 +48,16 @@ const users = $computed(() => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const headerTabs = $computed(() => [{
|
||||||
|
key: 'users',
|
||||||
|
icon: 'ti ti-users',
|
||||||
|
title: i18n.ts.users,
|
||||||
|
}, {
|
||||||
|
key: 'timeline',
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
title: i18n.ts.timeline,
|
||||||
|
}]);
|
||||||
|
|
||||||
definePageMetadata(computed(() => ({
|
definePageMetadata(computed(() => ({
|
||||||
title: role?.name,
|
title: role?.name,
|
||||||
icon: 'ti ti-badge',
|
icon: 'ti ti-badge',
|
||||||
|
|
|
@ -152,6 +152,7 @@ const addColumn = async (ev) => {
|
||||||
'channel',
|
'channel',
|
||||||
'mentions',
|
'mentions',
|
||||||
'direct',
|
'direct',
|
||||||
|
'roleTimeline',
|
||||||
];
|
];
|
||||||
|
|
||||||
const { canceled, result: column } = await os.select({
|
const { canceled, result: column } = await os.select({
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XRoleTimelineColumn v-else-if="column.type === 'roleTimeline'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -23,6 +24,7 @@ import XNotificationsColumn from './notifications-column.vue';
|
||||||
import XWidgetsColumn from './widgets-column.vue';
|
import XWidgetsColumn from './widgets-column.vue';
|
||||||
import XMentionsColumn from './mentions-column.vue';
|
import XMentionsColumn from './mentions-column.vue';
|
||||||
import XDirectColumn from './direct-column.vue';
|
import XDirectColumn from './direct-column.vue';
|
||||||
|
import XRoleTimelineColumn from './role-timeline-column.vue';
|
||||||
import { Column } from './deck-store';
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|
|
@ -22,6 +22,7 @@ export type Column = {
|
||||||
antennaId?: string;
|
antennaId?: string;
|
||||||
listId?: string;
|
listId?: string;
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
|
roleId?: string;
|
||||||
includingTypes?: typeof notificationTypes[number][];
|
includingTypes?: typeof notificationTypes[number][];
|
||||||
tl?: 'home' | 'local' | 'social' | 'global';
|
tl?: 'home' | 'local' | 'social' | 'global';
|
||||||
};
|
};
|
||||||
|
|
67
packages/frontend/src/ui/deck/role-timeline-column.vue
Normal file
67
packages/frontend/src/ui/deck/role-timeline-column.vue
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header>
|
||||||
|
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @after="() => emit('loaded')"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
|
import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'loaded'): void;
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.column.roleId == null) {
|
||||||
|
setRole();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setRole() {
|
||||||
|
const roles = await os.api('roles/list');
|
||||||
|
const { canceled, result: role } = await os.select({
|
||||||
|
title: i18n.ts.role,
|
||||||
|
items: roles.map(x => ({
|
||||||
|
value: x, text: x.name,
|
||||||
|
})),
|
||||||
|
default: props.column.roleId,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = [{
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
text: i18n.ts.role,
|
||||||
|
action: setRole,
|
||||||
|
}];
|
||||||
|
|
||||||
|
/*
|
||||||
|
function focus() {
|
||||||
|
timeline.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
Loading…
Reference in a new issue