1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-27 14:28:53 +09:00

メディアタイムライン追加

This commit is contained in:
Nyamori 2023-05-15 00:07:23 +09:00 committed by NoriDev
parent 25029ac671
commit f45745af5a
28 changed files with 302 additions and 1 deletions

View File

@ -1927,6 +1927,7 @@ _instanceCharts:
_timelines:
home: "Startseite"
local: "Lokal"
media: "Medien"
social: "Sozial"
global: "Global"
_play:

View File

@ -1979,6 +1979,7 @@ _instanceCharts:
_timelines:
home: "Home"
local: "Local"
media: "Media"
social: "Social"
global: "Global"
_play:

View File

@ -1831,6 +1831,7 @@ _instanceCharts:
_timelines:
home: "Inicio"
local: "Local"
media: "Media"
social: "Social"
global: "Global"
_play:

View File

@ -1491,6 +1491,7 @@ _instanceCharts:
_timelines:
home: "Principal"
local: "Local"
media: "Média"
social: "Social"
global: "Global"
_play:

View File

@ -1750,6 +1750,7 @@ _instanceCharts:
_timelines:
home: "Beranda"
local: "Lokal"
media: "Media"
social: "Sosial"
global: "Global"
_play:

View File

@ -1854,6 +1854,7 @@ _instanceCharts:
_timelines:
home: "Home"
local: "Locale"
media: "Contenuti multimediali"
social: "Sociale"
global: "Federata"
_play:

View File

@ -2029,6 +2029,7 @@ _instanceCharts:
_timelines:
home: "ホーム"
local: "ローカル"
media: "メディア"
social: "ソーシャル"
global: "グローバル"

View File

@ -1927,6 +1927,7 @@ _instanceCharts:
_timelines:
home: "ホーム"
local: "ローカル"
media: "メディア"
social: "ソーシャル"
global: "グローバル"
_play:

View File

@ -1980,6 +1980,7 @@ _instanceCharts:
_timelines:
home: "홈"
local: "로컬"
media: "미디어"
social: "소셜"
global: "글로벌"
_play:

View File

@ -1835,6 +1835,7 @@ _instanceCharts:
_timelines:
home: "Персональная"
local: "Местная"
media: "Медиа"
social: "Социальная"
global: "Всеобщая"
_play:

View File

@ -1415,6 +1415,7 @@ _instanceCharts:
_timelines:
home: "Domov"
local: "Lokálne"
media: "Médiá"
social: "Sociálne"
global: "Globálne"
_play:

View File

@ -1880,6 +1880,7 @@ _instanceCharts:
_timelines:
home: "หน้าแรก"
local: "ในพื้นที่"
media: "สื่อ"
social: "โซเชี่ยล"
global: "ทั่วโลก"
_play:

View File

@ -1578,6 +1578,7 @@ _instanceCharts:
_timelines:
home: "Домівка"
local: "Локальна"
media: "Медіафайли"
social: "Соціальна"
global: "Глобальна"
_play:

View File

@ -1632,6 +1632,7 @@ _instanceCharts:
_timelines:
home: "Trang chính"
local: "Máy chủ này"
media: "Phương tiện"
social: "Xã hội"
global: "Liên hợp"
_play:

View File

@ -1926,6 +1926,7 @@ _instanceCharts:
_timelines:
home: "首页"
local: "本地"
media: "媒体"
social: "社交"
global: "全局"
_play:

View File

@ -1927,6 +1927,7 @@ _instanceCharts:
_timelines:
home: "首頁"
local: "本地"
media: "媒體"
social: "社交"
global: "公開"
_play:

View File

@ -30,6 +30,7 @@ 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 { MediaTimelineChannelService } from './api/stream/channels/media-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';
@ -76,6 +77,7 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.
HomeTimelineChannelService,
HybridTimelineChannelService,
LocalTimelineChannelService,
MediaTimelineChannelService,
MessagingIndexChannelService,
MessagingChannelService,
QueueStatsChannelService,

View File

@ -260,6 +260,7 @@ 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_mediaTimeline from './endpoints/notes/media-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';
@ -620,6 +621,7 @@ 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_mediaTimeline: Provider = { provide: 'ep:notes/media-timeline', useClass: ep___notes_mediaTimeline.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 };
@ -984,6 +986,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_mediaTimeline,
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,
@ -1341,6 +1344,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_mediaTimeline,
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,

View File

@ -260,6 +260,7 @@ 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_mediaTimeline from './endpoints/notes/media-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';
@ -618,6 +619,7 @@ const eps = [
['notes/global-timeline', ep___notes_globalTimeline],
['notes/hybrid-timeline', ep___notes_hybridTimeline],
['notes/local-timeline', ep___notes_localTimeline],
['notes/media-timeline', ep___notes_mediaTimeline],
['notes/mentions', ep___notes_mentions],
['notes/polls/recommendation', ep___notes_polls_recommendation],
['notes/polls/vote', ep___notes_polls_vote],

View File

@ -0,0 +1,128 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.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'],
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
errors: {
ltlDisabled: {
message: 'Media timeline has been disabled.',
code: 'MTL_DISABLED',
id: '45a6eb02-7695-4393-b023-dd4be9aaaefd',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
fileType: { type: 'array', items: {
type: 'string',
} },
excludeNsfw: { 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' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private metaService: MetaService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
if (!policies.ltlAvailable) {
throw new ApiError(meta.errors.ltlDisabled);
}
//#region Construct query
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('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.andWhere('note.fileIds != \'{}\'')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, me);
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.fileType != null) {
query.andWhere('note.fileIds != \'{}\'');
query.andWhere(new Brackets(qb => {
for (const type of ps.fileType!) {
const i = ps.fileType!.indexOf(type);
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
}
}));
if (ps.excludeNsfw) {
query.andWhere('note.cw IS NULL');
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
}
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => {
if (me) {
this.activeUsersChart.read(me);
}
});
return await this.noteEntityService.packMany(timeline, me);
});
}
}

View File

@ -2,6 +2,7 @@ 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 { MediaTimelineChannelService } from './channels/media-timeline.js';
import { HomeTimelineChannelService } from './channels/home-timeline.js';
import { GlobalTimelineChannelService } from './channels/global-timeline.js';
import { MainChannelService } from './channels/main.js';
@ -23,6 +24,7 @@ export class ChannelsService {
private mainChannelService: MainChannelService,
private homeTimelineChannelService: HomeTimelineChannelService,
private localTimelineChannelService: LocalTimelineChannelService,
private mediaTimelineChannelService: MediaTimelineChannelService,
private hybridTimelineChannelService: HybridTimelineChannelService,
private globalTimelineChannelService: GlobalTimelineChannelService,
private userListChannelService: UserListChannelService,
@ -45,6 +47,7 @@ export class ChannelsService {
case 'main': return this.mainChannelService;
case 'homeTimeline': return this.homeTimelineChannelService;
case 'localTimeline': return this.localTimelineChannelService;
case 'mediaTimeline': return this.mediaTimelineChannelService;
case 'hybridTimeline': return this.hybridTimelineChannelService;
case 'globalTimeline': return this.globalTimelineChannelService;
case 'userList': return this.userListChannelService;

View File

@ -0,0 +1,111 @@
import { Injectable } from '@nestjs/common';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.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 MediaTimelineChannel extends Channel {
public readonly chName = 'mediaTimeline';
public static shouldShare = true;
public static requireCredential = false;
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) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@bindThis
private async onNote(note: Packed<'Note'>) {
if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
// リプライなら再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,
});
}
// 関係ない返信は除外
if (note.reply && this.user && !this.user.showTimelineReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) 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() {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
}
}
@Injectable()
export class MediaTimelineChannelService {
public readonly shouldShare = MediaTimelineChannel.shouldShare;
public readonly requireCredential = MediaTimelineChannel.requireCredential;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@bindThis
public create(id: string, connection: Channel['connection']): MediaTimelineChannel {
return new MediaTimelineChannel(
this.metaService,
this.roleService,
this.noteEntityService,
id,
connection,
);
}
}

View File

@ -38,6 +38,18 @@ const prepend = note => {
}
};
const prependFilterdMedia = note => {
if (note.files !== null && note.files.length > 0) {
tlComponent.pagingComponent?.prepend(note);
}
emit('note');
if (props.sound) {
sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note');
}
};
const onUserAdded = () => {
tlComponent.pagingComponent?.reload();
};
@ -82,6 +94,10 @@ if (props.src === 'antenna') {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
} else if (props.src === 'media') {
endpoint = 'notes/media-timeline';
connection = stream.useChannel('mediaTimeline');
connection.on('note', prependFilterdMedia);
} else if (props.src === 'social') {
endpoint = 'notes/hybrid-timeline';
query = {

View File

@ -191,6 +191,11 @@ const headerTabs = $computed(() => [{
title: i18n.ts._timelines.local,
icon: 'ti ti-planet',
iconOnly: true,
}, {
key: 'media',
title: i18n.ts._timelines.media,
icon: 'ti ti-photo',
iconOnly: true,
}, {
key: 'social',
title: i18n.ts._timelines.social,

View File

@ -24,7 +24,7 @@ export type Column = {
channelId?: string;
roleId?: string;
includingTypes?: typeof notificationTypes[number][];
tl?: 'home' | 'local' | 'social' | 'global';
tl?: 'home' | 'local' | 'media' | 'social' | 'global';
};
export const deckStore = markRaw(new Storage('deck', {

View File

@ -4,6 +4,7 @@
<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-rocket"></i>
<i v-else-if="column.tl === 'media'" class="ti ti-photo"></i>
<i v-else-if="column.tl === 'global'" class="ti ti-whirl"></i>
<span style="margin-left: 8px;">{{ column.name }}</span>
</template>
@ -56,6 +57,8 @@ async function setType() {
value: 'home' as const, text: i18n.ts._timelines.home,
}, {
value: 'local' as const, text: i18n.ts._timelines.local,
}, {
value: 'media' as const, text: i18n.ts._timelines.media,
}, {
value: 'social' as const, text: i18n.ts._timelines.social,
}, {

View File

@ -117,6 +117,10 @@ const choose = async (ev) => {
text: i18n.ts._timelines.local,
icon: 'ti ti-planet',
action: () => { setSrc('local'); },
}, {
text: i18n.ts._timelines.media,
icon: 'ti ti-photo',
action: () => { setSrc('media'); },
}, {
text: i18n.ts._timelines.social,
icon: 'ti ti-rocket',

View File

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