メディア、猫のタイムラインの改善

This commit is contained in:
NoriDev 2023-09-29 22:24:06 +09:00
parent 7d5b3c1bdb
commit f1807d29d5
29 changed files with 89 additions and 415 deletions

View file

@ -22,6 +22,17 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
# 릴리즈 노트
이 문서는 CherryPick의 변경 사항만 포함합니다.
## 4.x.x
출시일: unreleased<br>
기반 Misskey 버전: 2023.x.x<br>
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGELOG.md#2023xx) 문서를 참고하십시오.
### General
- 미디어, 고양이 타임라인 개선
- [misskey-dev/misskey@eb740e2](https://github.com/misskey-dev/misskey/commit/eb740e2c72ae6854b244ad099c927c069008720e) 이 추가됨에 따라, 해당 기능에 병합하고 기존 미디어 및 고양이 타임라인을 제거함
---
## 4.3.0
출시일: 2023/09/29<br>
기반 Misskey 버전: 2023.9.1<br>

View file

@ -1200,6 +1200,7 @@ authenticationRequiredToContinue: "Please authenticate to continue"
dateAndTime: "Timestamp"
showRenotes: "Show renotes"
edited: "Edited"
showCatOnly: "Show only cats"
additionalPermissionsForFlash: "Allow to add permission to Play"
thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions"
doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?"
@ -1210,8 +1211,7 @@ _tlTutorial:
step1_1: 'The {icon} Home timeline is where you can see posts from the accounts you follow.'
step1_2: 'The {icon} Local timeline is where you can see posts from everyone else on this server.'
step1_3: 'The {icon} Social timeline is a combination of the Home and Local timelines.'
step1_4: 'The {icon} Cat timeline shows posts from users who have enabled cat on their social timeline.'
step1_5: 'The {icon} Global timeline is where you can see posts from every other connected server.'
step1_4: 'The {icon} Global timeline is where you can see posts from every other connected server.'
_announcement:
forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1612,7 +1612,6 @@ _role:
_options:
gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline"
ctlAvailable: "Can view the cat timeline"
canPublicNote: "Can send public notes"
canEditNote: "Note editing"
canInvite: "Can create instance invite codes"
@ -2198,9 +2197,7 @@ _instanceCharts:
_timelines:
home: "Home"
local: "Local"
media: "Media"
social: "Social"
cat: "Cat"
global: "Global"
_play:
new: "Create Play"

5
locales/index.d.ts vendored
View file

@ -1206,6 +1206,7 @@ export interface Locale {
"notificationRecieveConfig": string;
"mutualFollow": string;
"fileAttachedOnly": string;
"showCatOnly": string;
"additionalPermissionsForFlash": string;
"thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string;
@ -1218,7 +1219,6 @@ export interface Locale {
"step1_2": string;
"step1_3": string;
"step1_4": string;
"step1_5": string;
};
"_announcement": {
"forExistingUsers": string;
@ -1709,7 +1709,6 @@ export interface Locale {
"_options": {
"gtlAvailable": string;
"ltlAvailable": string;
"ctlAvailable": string;
"canPublicNote": string;
"canEditNote": string;
"canInvite": string;
@ -2346,9 +2345,7 @@ export interface Locale {
"_timelines": {
"home": string;
"local": string;
"media": string;
"social": string;
"cat": string;
"global": string;
};
"_play": {

View file

@ -1203,6 +1203,7 @@ edited: "編集済み"
notificationRecieveConfig: "通知の受信設定"
mutualFollow: "相互フォロー"
fileAttachedOnly: "ファイル付きのみ"
showCatOnly: "キャット付きのみ"
additionalPermissionsForFlash: "Playへの追加許可"
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか"
@ -1215,8 +1216,7 @@ _tlTutorial:
step1_1: '{icon} ホームタイムラインは、あなたがフォローしているアカウントの投稿を見られます。'
step1_2: '{icon} ローカルタイムラインでは、このサーバーにいるみんなの投稿を見られます。'
step1_3: '{icon} ソーシャルタイムラインでは、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。'
step1_4: '{icon} キャットタイムラインでは、ソーシャルタイムラインでキャットを有効にしたユーザーの投稿を見ることができます。'
step1_5: '{icon} グローバルタイムラインでは、このサーバーに接続されているすべてのサーバーからの投稿を見られます。'
step1_4: '{icon} グローバルタイムラインでは、このサーバーに接続されているすべてのサーバーからの投稿を見られます。'
_announcement:
forExistingUsers: "既存ユーザーのみ"
@ -1629,7 +1629,6 @@ _role:
_options:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
ctlAvailable: "キャットタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canEditNote: "ノートの編集"
canInvite: "サーバー招待コードの発行"
@ -2258,9 +2257,7 @@ _instanceCharts:
_timelines:
home: "ホーム"
local: "ローカル"
media: "メディア"
social: "ソーシャル"
cat: "キャット"
global: "グローバル"
_play:

View file

@ -1186,6 +1186,7 @@ youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있어요."
useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주세요."
replies: "답글"
renotes: "리노트"
showCatOnly: "고양이만 보기"
additionalPermissionsForFlash: "Play에 대한 추가 권한"
thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요"
doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?"
@ -1196,8 +1197,7 @@ _tlTutorial:
step1_1: '{icon} 홈 타임라인은 내가 팔로우하고 있는 계정의 게시물을 볼 수 있어요.'
step1_2: '{icon} 로컬 타임라인은 이 서버의 모든 유저가 올린 게시물을 볼 수 있어요.'
step1_3: '{icon} 소셜 타임라인은 홈 타임라인과 로컬 타임라인을 합친 것과 같아요.'
step1_4: '{icon} 고양이 타임라인에서는 소셜 타임라인에서 고양이를 활성화한 유저의 게시물을 볼 수 있어요.'
step1_5: '{icon} 글로벌 타임라인에서는 이 서버와 연결된 모든 서버의 게시물을 볼 수 있어요.'
step1_4: '{icon} 글로벌 타임라인에서는 이 서버와 연결된 모든 서버의 게시물을 볼 수 있어요.'
_announcement:
forExistingUsers: "기존 유저에게만 알리기"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시해요. 비활성화하면 게시 후에 가입한 유저에게도 표시해요."
@ -1588,7 +1588,6 @@ _role:
_options:
gtlAvailable: "글로벌 타임라인 보이기"
ltlAvailable: "로컬 타임라인 보이기"
ctlAvailable: "고양이 타임라인 보이기"
canPublicNote: "공개 노트 허용"
canInvite: "서버 초대 코드 발행"
inviteLimit: "초대 한도"
@ -2171,9 +2170,7 @@ _instanceCharts:
_timelines:
home: "홈"
local: "로컬"
media: "미디어"
social: "소셜"
cat: "고양이"
global: "글로벌"
_play:
new: "Play 만들기"

View file

@ -111,7 +111,6 @@ export class NodeinfoServerService {
feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration,
disableLocalTimeline: !basePolicies.ltlAvailable,
disableCatTimeline: !basePolicies.ctlAvailable,
disableGlobalTimeline: !basePolicies.gtlAvailable,
emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha,

View file

@ -35,7 +35,6 @@ import { HashtagChannelService } from './api/stream/channels/hashtag.js';
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
import { CatTimelineChannelService } from './api/stream/channels/cat-timeline.js';
import { MessagingIndexChannelService } from './api/stream/channels/messaging-index.js';
import { MessagingChannelService } from './api/stream/channels/messaging.js';
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
@ -83,7 +82,6 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
HomeTimelineChannelService,
HybridTimelineChannelService,
LocalTimelineChannelService,
CatTimelineChannelService,
MessagingIndexChannelService,
MessagingChannelService,
QueueStatsChannelService,

View file

@ -279,7 +279,6 @@ import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_catTimeline from './endpoints/notes/cat-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
@ -657,7 +656,6 @@ const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep__
const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default };
const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default };
const $notes_catTimeline: Provider = { provide: 'ep:notes/cat-timeline', useClass: ep___notes_catTimeline.default };
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default };
const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default };
@ -1040,7 +1038,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_catTimeline,
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,
@ -1415,7 +1412,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_catTimeline,
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,

View file

@ -278,7 +278,6 @@ import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_catTimeline from './endpoints/notes/cat-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
@ -654,7 +653,6 @@ const eps = [
['notes/global-timeline', ep___notes_globalTimeline],
['notes/hybrid-timeline', ep___notes_hybridTimeline],
['notes/local-timeline', ep___notes_localTimeline],
['notes/cat-timeline', ep___notes_catTimeline],
['notes/mentions', ep___notes_mentions],
['notes/polls/recommendation', ep___notes_polls_recommendation],
['notes/polls/vote', ep___notes_polls_vote],

View file

@ -1,163 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
errors: {
ctlDisabled: {
message: 'Cat timeline has been disabled.',
code: 'CTL_DISABLED',
id: '620763f4-f621-4533-ab33-0577a1a3c342',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
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' },
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me.id);
if (!policies.ctlAvailable) {
throw new ApiError(meta.errors.ctlDisabled);
}
//#region Construct query
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.andWhere(new Brackets(qb => {
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id })
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}))
.andWhere('(select "isCat" from "user" where id = note."userId")')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters(followingQuery.getParameters());
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
});
}
}

View file

@ -42,6 +42,7 @@ export const paramDef = {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@ -100,6 +101,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View file

@ -53,6 +53,7 @@ export const paramDef = {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
},
required: [],
} as const;
@ -148,6 +149,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View file

@ -43,6 +43,7 @@ export const paramDef = {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
fileType: { type: 'array', items: {
type: 'string',
} },
@ -121,6 +122,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View file

@ -43,6 +43,7 @@ export const paramDef = {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
},
required: [],
} as const;
@ -137,6 +138,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View file

@ -56,6 +56,7 @@ export const paramDef = {
default: false,
description: 'Only show notes that have attached files.',
},
withCats: { type: 'boolean', default: false },
},
required: ['listId'],
} as const;
@ -149,6 +150,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View file

@ -43,6 +43,7 @@ export const paramDef = {
userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@ -139,6 +140,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View file

@ -7,7 +7,6 @@ import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
import { LocalTimelineChannelService } from './channels/local-timeline.js';
import { CatTimelineChannelService } from './channels/cat-timeline.js';
import { HomeTimelineChannelService } from './channels/home-timeline.js';
import { GlobalTimelineChannelService } from './channels/global-timeline.js';
import { MainChannelService } from './channels/main.js';
@ -29,7 +28,6 @@ export class ChannelsService {
private mainChannelService: MainChannelService,
private homeTimelineChannelService: HomeTimelineChannelService,
private localTimelineChannelService: LocalTimelineChannelService,
private catTimelineChannelService: CatTimelineChannelService,
private hybridTimelineChannelService: HybridTimelineChannelService,
private globalTimelineChannelService: GlobalTimelineChannelService,
private userListChannelService: UserListChannelService,
@ -53,7 +51,6 @@ export class ChannelsService {
case 'homeTimeline': return this.homeTimelineChannelService;
case 'localTimeline': return this.localTimelineChannelService;
case 'hybridTimeline': return this.hybridTimelineChannelService;
case 'catTimeline': return this.catTimelineChannelService;
case 'globalTimeline': return this.globalTimelineChannelService;
case 'userList': return this.userListChannelService;
case 'hashtag': return this.hashtagChannelService;

View file

@ -1,144 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js';
class CatTimelineChannel extends Channel {
public readonly chName = 'catTimeline';
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
private withRenotes: boolean;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
super(id, connection);
//this.onNote = this.onNote.bind(this);
}
@bindThis
public async init(params: any): Promise<void> {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ctlAvailable) return;
this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@bindThis
private async onNote(note: Packed<'Note'>) {
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
// フォローしているチャンネルの投稿 の場合だけ
if (!(
(note.channelId == null && this.user!.id === note.userId) ||
(note.channelId == null && this.following.has(note.userId)) ||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await this.noteEntityService.pack(note.id, this.user!, {
detail: true,
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await this.noteEntityService.pack(note.replyId, this.user!, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await this.noteEntityService.pack(note.renoteId, this.user!, {
detail: true,
});
}
}
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
// 関係ない返信は除外
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
this.connection.cacheNote(note);
this.send('note', note);
}
@bindThis
public dispose(): void {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
}
}
@Injectable()
export class CatTimelineChannelService {
public readonly shouldShare = CatTimelineChannel.shouldShare;
public readonly requireCredential = CatTimelineChannel.requireCredential;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@bindThis
public create(id: string, connection: Channel['connection']): CatTimelineChannel {
return new CatTimelineChannel(
this.metaService,
this.roleService,
this.noteEntityService,
id,
connection,
);
}
}

View file

@ -222,13 +222,6 @@ export type Channels = {
};
receives: null;
};
catTimeline: {
params: null;
events: {
note: (payload: Note) => void;
};
receives: null;
};
globalTimeline: {
params: null;
events: {
@ -2478,7 +2471,6 @@ type LiteInstanceMetadata = {
feedbackUrl: string;
disableRegistration: boolean;
disableLocalTimeline: boolean;
disableCatTimeline: boolean;
disableGlobalTimeline: boolean;
driveCapacityPerLocalUserMb: number;
driveCapacityPerRemoteUserMb: number;

View file

@ -332,7 +332,6 @@ export type LiteInstanceMetadata = {
feedbackUrl: string;
disableRegistration: boolean;
disableLocalTimeline: boolean;
disableCatTimeline: boolean;
disableGlobalTimeline: boolean;
driveCapacityPerLocalUserMb: number;
driveCapacityPerRemoteUserMb: number;

View file

@ -63,13 +63,6 @@ export type Channels = {
};
receives: null;
};
catTimeline: {
params: null;
events: {
note: (payload: Note) => void;
};
receives: null;
};
globalTimeline: {
params: null;
events: {

View file

@ -30,10 +30,12 @@ const props = withDefaults(defineProps<{
withRenotes?: boolean;
withReplies?: boolean;
onlyFiles?: boolean;
onlyCats?: boolean;
}>(), {
withRenotes: true,
withReplies: false,
onlyFiles: false,
onlyCats: false,
});
const emit = defineEmits<{
@ -93,11 +95,13 @@ if (props.src === 'antenna') {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
@ -112,11 +116,13 @@ if (props.src === 'antenna') {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('localTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
@ -129,46 +135,37 @@ if (props.src === 'antenna') {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
tlIcon = 'ti ti-universe';
tlHint = i18n.ts._tlTutorial.step1_3;
tlHintClosed = defaultStore.state.tlSocialHintClosed;
} else if (props.src === 'cat') {
endpoint = 'notes/cat-timeline';
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel('catTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
tlIcon = 'ti ti-cat';
tlHint = i18n.ts._tlTutorial.step1_4;
tlHintClosed = defaultStore.state.tlCatHintClosed;
} else if (props.src === 'global') {
endpoint = 'notes/global-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
tlIcon = 'ti ti-world';
tlHint = i18n.ts._tlTutorial.step1_5;
tlHint = i18n.ts._tlTutorial.step1_4;
tlHintClosed = defaultStore.state.tlGlobalHintClosed;
} else if (props.src === 'mentions') {
endpoint = 'notes/mentions';
@ -192,12 +189,14 @@ if (props.src === 'antenna') {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
listId: props.list,
};
connection = stream.useChannel('userList', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
listId: props.list,
});
connection.on('note', prepend);
@ -229,15 +228,9 @@ function closeHint() {
case 'local':
defaultStore.set('tlLocalHintClosed', true);
break;
case 'media':
defaultStore.set('tlMediaHintClosed', true);
break;
case 'social':
defaultStore.set('tlSocialHintClosed', true);
break;
case 'cat':
defaultStore.set('tlCatHintClosed', true);
break;
case 'global':
defaultStore.set('tlGlobalHintClosed', true);
break;

View file

@ -105,7 +105,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'enableHomeTimeline',
'enableLocalTimeline',
'enableSocialTimeline',
'enableCatTimeline',
'enableGlobalTimeline',
'enableListTimeline',
'enableAntennaTimeline',

View file

@ -11,7 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="enableHomeTimeline"><i class="ti ti-home"></i> {{ i18n.ts._timelines.home }}</MkSwitch>
<MkSwitch v-model="enableLocalTimeline"><i class="ti ti-planet"></i> {{ i18n.ts._timelines.local }}</MkSwitch>
<MkSwitch v-model="enableSocialTimeline"><i class="ti ti-universe"></i> {{ i18n.ts._timelines.social }}</MkSwitch>
<MkSwitch v-model="enableCatTimeline"><i class="ti ti-cat"></i> {{ i18n.ts._timelines.cat }}</MkSwitch>
<MkSwitch v-model="enableGlobalTimeline"><i class="ti ti-world"></i> {{ i18n.ts._timelines.global }}</MkSwitch>
</div>
</FormSection>
@ -56,7 +55,6 @@ function save() {
const enableHomeTimeline = computed(defaultStore.makeGetterSetter('enableHomeTimeline'));
const enableLocalTimeline = computed(defaultStore.makeGetterSetter('enableLocalTimeline'));
const enableSocialTimeline = computed(defaultStore.makeGetterSetter('enableSocialTimeline'));
const enableCatTimeline = computed(defaultStore.makeGetterSetter('enableCatTimeline'));
const enableGlobalTimeline = computed(defaultStore.makeGetterSetter('enableGlobalTimeline'));
const enableListTimeline = computed(defaultStore.makeGetterSetter('enableListTimeline'));
const enableAntennaTimeline = computed(defaultStore.makeGetterSetter('enableAntennaTimeline'));

View file

@ -33,12 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl">
<MkTimeline
ref="tlComponent"
:key="src + withRenotes + withReplies + onlyFiles"
:key="src + withRenotes + withReplies + onlyFiles + onlyCats"
:src="src.split(':')[0]"
:list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"
:onlyFiles="onlyFiles"
:onlyCats="onlyCats"
:sound="true"
@queue="queueUpdated"
/>
@ -82,7 +83,6 @@ const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
const isCatTimelineAvailable = ($i == null && instance.policies.ctlAvailable) || ($i != null && $i.policies.ctlAvailable);
const keymap = {
't': focus,
};
@ -96,6 +96,7 @@ const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src
const withRenotes = $ref(true);
const withReplies = $ref(false);
const onlyFiles = $ref(false);
const onlyCats = $ref(false);
const friendlyEnableNotifications = computed(defaultStore.makeGetterSetter('friendlyEnableNotifications'));
const friendlyEnableWidgets = computed(defaultStore.makeGetterSetter('friendlyEnableWidgets'));
@ -160,7 +161,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'cat' | 'global' | `list:${string}`): void {
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
let userList = null;
if (newSrc.startsWith('userList:')) {
const id = newSrc.substring('userList:'.length);
@ -209,7 +210,6 @@ const headerActions = $computed(() => [{
ref: friendlyEnableNotifications,
action: () => {
friendlyEnableNotifications.value = !friendlyEnableNotifications.value;
reloadAsk;
},
}, {
type: 'switch',
@ -218,7 +218,6 @@ const headerActions = $computed(() => [{
ref: friendlyEnableWidgets,
action: () => {
friendlyEnableWidgets.value = !friendlyEnableWidgets.value;
reloadAsk;
},
}, {
type: 'switch',
@ -235,6 +234,11 @@ const headerActions = $computed(() => [{
text: i18n.ts.fileAttachedOnly,
icon: 'ti ti-photo',
ref: $$(onlyFiles),
}, {
type: 'switch',
text: i18n.ts.showCatOnly,
icon: 'ti ti-cat',
ref: $$(onlyCats),
}], ev.currentTarget ?? ev.target);
},
}]);
@ -259,11 +263,6 @@ const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLis
title: i18n.ts._timelines.social,
icon: 'ti ti-universe',
iconOnly: true,
}] : []), ...(isCatTimelineAvailable && defaultStore.state.enableCatTimeline ? [{
key: 'cat',
title: i18n.ts._timelines.cat,
icon: 'ti ti-cat',
iconOnly: true,
}] : [])] : []), ...(isGlobalTimelineAvailable && defaultStore.state.enableGlobalTimeline ? [{
key: 'global',
title: i18n.ts._timelines.global,
@ -303,7 +302,7 @@ const headerTabsWhenNotLogin = $computed(() => [
definePageMetadata(computed(() => ({
title: i18n.ts.timeline,
icon: src === 'local' ? 'ti ti-planet' : src === 'social' ? 'ti ti-universe' : src === 'cat' ? 'ti ti-cat' : src === 'global' ? 'ti ti-world' : 'ti ti-home',
icon: src === 'local' ? 'ti ti-planet' : src === 'social' ? 'ti ti-universe' : src === 'global' ? 'ti ti-world' : 'ti ti-home',
})));
</script>

View file

@ -61,18 +61,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
tlMediaHintClosed: {
where: 'device',
default: false,
},
tlSocialHintClosed: {
where: 'device',
default: false,
},
tlCatHintClosed: {
where: 'device',
default: false,
},
tlGlobalHintClosed: {
where: 'device',
default: false,
@ -189,7 +181,7 @@ export const defaultStore = markRaw(new Storage('base', {
tl: {
where: 'deviceAccount',
default: {
src: 'home' as 'home' | 'local' | 'media' | 'social' | 'cat' | 'global' | `list:${string}`,
src: 'home' as 'home' | 'local' | 'social' | 'global' | `list:${string}`,
userList: null as Misskey.entities.UserList | null,
},
},
@ -463,10 +455,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: true,
},
enableCatTimeline: {
where: 'device',
default: true,
},
enableGlobalTimeline: {
where: 'device',
default: true,

View file

@ -29,10 +29,11 @@ export type Column = {
channelId?: string;
roleId?: string;
excludeTypes?: typeof notificationTypes[number][];
tl?: 'home' | 'local' | 'media' | 'social' | 'cat' | 'global';
tl?: 'home' | 'local' | 'social' | 'global';
withRenotes?: boolean;
withReplies?: boolean;
onlyFiles?: boolean;
onlyCats?: boolean;
};
export const deckStore = markRaw(new Storage('deck', {

View file

@ -9,12 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-if="column.tl === 'home'" class="ti ti-home"></i>
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
<i v-else-if="column.tl === 'social'" class="ti ti-universe"></i>
<i v-else-if="column.tl === 'cat'" class="ti ti-cat"></i>
<i v-else-if="column.tl === 'global'" class="ti ti-world"></i>
<span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<div v-if="(((column.tl === 'local' || column.tl === 'social' || column.tl === 'cat') && !isLocalTimelineAvailable) || (column.tl === 'cat' && !isCatTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
<div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
<p :class="$style.disabledTitle">
<i class="ti ti-circle-minus"></i>
{{ i18n.ts._disabledTimeline.title }}
@ -24,11 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTimeline
v-else-if="column.tl"
ref="timeline"
:key="column.tl + withRenotes + withReplies + onlyFiles"
:key="column.tl + withRenotes + withReplies + onlyFiles + onlyCats"
:src="column.tl"
:withRenotes="withRenotes"
:withReplies="withReplies"
:onlyFiles="onlyFiles"
:onlyCats="onlyCats"
/>
</XColumn>
</template>
@ -52,10 +52,10 @@ let disabled = $ref(false);
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
const isCatTimelineAvailable = (($i == null && instance.policies.ctlAvailable) || ($i != null && $i.policies.ctlAvailable));
const withRenotes = $ref(props.column.withRenotes ?? true);
const withReplies = $ref(props.column.withReplies ?? false);
const onlyFiles = $ref(props.column.onlyFiles ?? false);
const onlyCats = $ref(props.column.onlyCats ?? false);
watch($$(withRenotes), v => {
updateColumn(props.column.id, {
@ -75,13 +75,18 @@ watch($$(onlyFiles), v => {
});
});
watch($$(onlyCats), v => {
updateColumn(props.column.id, {
onlyCats: v,
});
});
onMounted(() => {
if (props.column.tl == null) {
setType();
} else if ($i) {
disabled = (
(!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) ||
(!((instance.policies.ctlAvailable) || ($i.policies.ctlAvailable)) && ['cat'].includes(props.column.tl)) ||
(!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)));
}
});
@ -95,8 +100,6 @@ async function setType() {
value: 'local' as const, text: i18n.ts._timelines.local,
}, {
value: 'social' as const, text: i18n.ts._timelines.social,
}, {
value: 'cat' as const, text: i18n.ts._timelines.cat,
}, {
value: 'global' as const, text: i18n.ts._timelines.global,
}],
@ -128,6 +131,10 @@ const menu = [{
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: $$(onlyFiles),
}, {
type: 'switch',
text: i18n.ts.showCatOnly,
ref: $$(onlyCats),
}];
</script>

View file

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</template>
<div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social' || widgetProps.src === 'cat') && !isLocalTimelineAvailable) || (widgetProps.src === 'cat' && !isCatTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
<div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
<p :class="$style.disabledTitle">
<i class="ti ti-minus"></i>
{{ i18n.ts._disabledTimeline.title }}
@ -48,7 +48,6 @@ import { instance } from '@/instance.js';
const name = 'timeline';
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
const isCatTimelineAvailable = (($i == null && instance.policies.ctlAvailable) || ($i != null && $i.policies.ctlAvailable));
const widgetPropsDef = {
showHeader: {
@ -128,10 +127,6 @@ const choose = async (ev) => {
text: i18n.ts._timelines.social,
icon: 'ti ti-universe',
action: () => { setSrc('social'); },
}, {
text: i18n.ts._timelines.cat,
icon: 'ti ti-cat',
action: () => { setSrc('cat'); },
}, {
text: i18n.ts._timelines.global,
icon: 'ti ti-world',