parent
d586d1e6f8
commit
2f99c7e9dc
27 changed files with 387 additions and 4 deletions
|
@ -1598,6 +1598,7 @@ _role:
|
||||||
high: "High"
|
high: "High"
|
||||||
_options:
|
_options:
|
||||||
gtlAvailable: "Can view the global timeline"
|
gtlAvailable: "Can view the global timeline"
|
||||||
|
btlAvailable: "Can view the bubble timeline"
|
||||||
ltlAvailable: "Can view the local timeline"
|
ltlAvailable: "Can view the local timeline"
|
||||||
canPublicNote: "Can send public notes"
|
canPublicNote: "Can send public notes"
|
||||||
canImportNotes: "Can import notes"
|
canImportNotes: "Can import notes"
|
||||||
|
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
|
@ -1698,6 +1698,7 @@ export interface Locale {
|
||||||
};
|
};
|
||||||
"_options": {
|
"_options": {
|
||||||
"gtlAvailable": string;
|
"gtlAvailable": string;
|
||||||
|
"btlAvailable": string;
|
||||||
"ltlAvailable": string;
|
"ltlAvailable": string;
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
"canImportNotes": string;
|
"canImportNotes": string;
|
||||||
|
|
|
@ -1607,6 +1607,7 @@ _role:
|
||||||
high: "高"
|
high: "高"
|
||||||
_options:
|
_options:
|
||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
|
btlAvailable: "バブルのタイムラインを見ることができる"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
canImportNotes: "ノートのインポートが可能"
|
canImportNotes: "ノートのインポートが可能"
|
||||||
|
|
11
packages/backend/migration/1701647674000-BubbleInstances.js
Normal file
11
packages/backend/migration/1701647674000-BubbleInstances.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export class BubbleInstances1701647674000 {
|
||||||
|
name = 'BubbleInstances1701647674000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "bubbleInstances" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bubbleInstances"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
|
btlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
canInvite: boolean;
|
canInvite: boolean;
|
||||||
inviteLimit: number;
|
inviteLimit: number;
|
||||||
|
@ -53,6 +54,7 @@ export type RolePolicies = {
|
||||||
export const DEFAULT_POLICIES: RolePolicies = {
|
export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
gtlAvailable: true,
|
gtlAvailable: true,
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
|
btlAvailable: false,
|
||||||
canPublicNote: true,
|
canPublicNote: true,
|
||||||
canInvite: false,
|
canInvite: false,
|
||||||
inviteLimit: 0,
|
inviteLimit: 0,
|
||||||
|
@ -303,6 +305,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||||
|
btlAvailable: calc('btlAvailable', vs => vs.some(v => v === true)),
|
||||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||||
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -544,4 +544,9 @@ export class MiMeta {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public defaultLike: string | null;
|
public defaultLike: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public bubbleInstances: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,7 @@ export class NodeinfoServerService {
|
||||||
disableRegistration: meta.disableRegistration,
|
disableRegistration: meta.disableRegistration,
|
||||||
disableLocalTimeline: !basePolicies.ltlAvailable,
|
disableLocalTimeline: !basePolicies.ltlAvailable,
|
||||||
disableGlobalTimeline: !basePolicies.gtlAvailable,
|
disableGlobalTimeline: !basePolicies.gtlAvailable,
|
||||||
|
disableBubbleTimeline: !basePolicies.btlAvailable,
|
||||||
emailRequiredForSignup: meta.emailRequiredForSignup,
|
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||||
enableHcaptcha: meta.enableHcaptcha,
|
enableHcaptcha: meta.enableHcaptcha,
|
||||||
enableRecaptcha: meta.enableRecaptcha,
|
enableRecaptcha: meta.enableRecaptcha,
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { AntennaChannelService } from './api/stream/channels/antenna.js';
|
||||||
import { ChannelChannelService } from './api/stream/channels/channel.js';
|
import { ChannelChannelService } from './api/stream/channels/channel.js';
|
||||||
import { DriveChannelService } from './api/stream/channels/drive.js';
|
import { DriveChannelService } from './api/stream/channels/drive.js';
|
||||||
import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js';
|
import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js';
|
||||||
|
import { BubbleTimelineChannelService } from './api/stream/channels/bubble-timeline.js';
|
||||||
import { HashtagChannelService } from './api/stream/channels/hashtag.js';
|
import { HashtagChannelService } from './api/stream/channels/hashtag.js';
|
||||||
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
|
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
|
||||||
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
|
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
|
||||||
|
@ -77,6 +78,7 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||||
ChannelChannelService,
|
ChannelChannelService,
|
||||||
DriveChannelService,
|
DriveChannelService,
|
||||||
GlobalTimelineChannelService,
|
GlobalTimelineChannelService,
|
||||||
|
BubbleTimelineChannelService,
|
||||||
HashtagChannelService,
|
HashtagChannelService,
|
||||||
RoleTimelineChannelService,
|
RoleTimelineChannelService,
|
||||||
HomeTimelineChannelService,
|
HomeTimelineChannelService,
|
||||||
|
|
|
@ -277,6 +277,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
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_globalTimeline from './endpoints/notes/global-timeline.js';
|
||||||
|
import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js';
|
||||||
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-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_localTimeline from './endpoints/notes/local-timeline.js';
|
||||||
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
|
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
|
||||||
|
@ -648,6 +649,7 @@ const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create'
|
||||||
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
|
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
|
||||||
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
||||||
const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
|
const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
|
||||||
|
const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default };
|
||||||
const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.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_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default };
|
||||||
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
|
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
|
||||||
|
@ -1023,6 +1025,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
$notes_globalTimeline,
|
$notes_globalTimeline,
|
||||||
|
$notes_bubbleTimeline,
|
||||||
$notes_hybridTimeline,
|
$notes_hybridTimeline,
|
||||||
$notes_localTimeline,
|
$notes_localTimeline,
|
||||||
$notes_mentions,
|
$notes_mentions,
|
||||||
|
@ -1392,6 +1395,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
$notes_globalTimeline,
|
$notes_globalTimeline,
|
||||||
|
$notes_bubbleTimeline,
|
||||||
$notes_hybridTimeline,
|
$notes_hybridTimeline,
|
||||||
$notes_localTimeline,
|
$notes_localTimeline,
|
||||||
$notes_mentions,
|
$notes_mentions,
|
||||||
|
|
|
@ -277,6 +277,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
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_globalTimeline from './endpoints/notes/global-timeline.js';
|
||||||
|
import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js';
|
||||||
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-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_localTimeline from './endpoints/notes/local-timeline.js';
|
||||||
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
|
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
|
||||||
|
@ -646,6 +647,7 @@ const eps = [
|
||||||
['notes/favorites/delete', ep___notes_favorites_delete],
|
['notes/favorites/delete', ep___notes_favorites_delete],
|
||||||
['notes/featured', ep___notes_featured],
|
['notes/featured', ep___notes_featured],
|
||||||
['notes/global-timeline', ep___notes_globalTimeline],
|
['notes/global-timeline', ep___notes_globalTimeline],
|
||||||
|
['notes/bubble-timeline', ep___notes_bubbleTimeline],
|
||||||
['notes/hybrid-timeline', ep___notes_hybridTimeline],
|
['notes/hybrid-timeline', ep___notes_hybridTimeline],
|
||||||
['notes/local-timeline', ep___notes_localTimeline],
|
['notes/local-timeline', ep___notes_localTimeline],
|
||||||
['notes/mentions', ep___notes_mentions],
|
['notes/mentions', ep___notes_mentions],
|
||||||
|
|
|
@ -154,6 +154,13 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bubbleInstances: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
hcaptchaSecretKey: {
|
hcaptchaSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@ -402,6 +409,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
silencedHosts: instance.silencedHosts,
|
silencedHosts: instance.silencedHosts,
|
||||||
sensitiveWords: instance.sensitiveWords,
|
sensitiveWords: instance.sensitiveWords,
|
||||||
preservedUsernames: instance.preservedUsernames,
|
preservedUsernames: instance.preservedUsernames,
|
||||||
|
bubbleInstances: instance.bubbleInstances,
|
||||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||||
turnstileSecretKey: instance.turnstileSecretKey,
|
turnstileSecretKey: instance.turnstileSecretKey,
|
||||||
|
|
|
@ -123,6 +123,7 @@ export const paramDef = {
|
||||||
enableIdenticonGeneration: { type: 'boolean' },
|
enableIdenticonGeneration: { type: 'boolean' },
|
||||||
serverRules: { type: 'array', items: { type: 'string' } },
|
serverRules: { type: 'array', items: { type: 'string' } },
|
||||||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||||
|
bubbleInstances: { type: 'array', items: { type: 'string' } },
|
||||||
manifestJsonOverride: { type: 'string' },
|
manifestJsonOverride: { type: 'string' },
|
||||||
enableFanoutTimeline: { type: 'boolean' },
|
enableFanoutTimeline: { type: 'boolean' },
|
||||||
enableFanoutTimelineDbFallback: { type: 'boolean' },
|
enableFanoutTimelineDbFallback: { type: 'boolean' },
|
||||||
|
@ -482,6 +483,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.preservedUsernames = ps.preservedUsernames;
|
set.preservedUsernames = ps.preservedUsernames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.bubbleInstances !== undefined) {
|
||||||
|
set.bubbleInstances = ps.bubbleInstances;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.manifestJsonOverride !== undefined) {
|
if (ps.manifestJsonOverride !== undefined) {
|
||||||
set.manifestJsonOverride = ps.manifestJsonOverride;
|
set.manifestJsonOverride = ps.manifestJsonOverride;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Brackets } from 'typeorm';
|
||||||
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['notes'],
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'Note',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
btlDisabled: {
|
||||||
|
message: 'Bubble timeline has been disabled.',
|
||||||
|
code: 'BTL_DISABLED',
|
||||||
|
id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6c',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
withFiles: { type: 'boolean', default: false },
|
||||||
|
withBots: { type: 'boolean', default: true },
|
||||||
|
withRenotes: { type: 'boolean', default: true },
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
private noteEntityService: NoteEntityService,
|
||||||
|
private queryService: QueryService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private activeUsersChart: ActiveUsersChart,
|
||||||
|
private cacheService: CacheService,
|
||||||
|
private metaService: MetaService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||||
|
const instance = await this.metaService.fetch();
|
||||||
|
if (!policies.btlAvailable) {
|
||||||
|
throw new ApiError(meta.errors.btlDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
followings,
|
||||||
|
] = me ? await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||||
|
]) : [undefined];
|
||||||
|
|
||||||
|
//#region Construct query
|
||||||
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||||
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
.andWhere('note.visibility = \'public\'')
|
||||||
|
.andWhere('note.channelId IS NULL')
|
||||||
|
.andWhere(
|
||||||
|
`(note.userHost = ANY ('{"${instance.bubbleInstances.join('","')}"}'))`,
|
||||||
|
)
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
|
if (me) {
|
||||||
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.withFiles) {
|
||||||
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||||
|
|
||||||
|
if (ps.withRenotes === false) {
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb.where('note.renoteId IS NULL');
|
||||||
|
qb.orWhere(new Brackets(qb => {
|
||||||
|
qb.where('note.text IS NOT NULL');
|
||||||
|
qb.orWhere('note.fileIds != \'{}\'');
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
let timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
timeline = timeline.filter(note => {
|
||||||
|
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (me) {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
|
import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
|
||||||
import { LocalTimelineChannelService } from './channels/local-timeline.js';
|
import { LocalTimelineChannelService } from './channels/local-timeline.js';
|
||||||
import { HomeTimelineChannelService } from './channels/home-timeline.js';
|
import { HomeTimelineChannelService } from './channels/home-timeline.js';
|
||||||
|
import { BubbleTimelineChannelService } from './channels/bubble-timeline.js';
|
||||||
import { GlobalTimelineChannelService } from './channels/global-timeline.js';
|
import { GlobalTimelineChannelService } from './channels/global-timeline.js';
|
||||||
import { MainChannelService } from './channels/main.js';
|
import { MainChannelService } from './channels/main.js';
|
||||||
import { ChannelChannelService } from './channels/channel.js';
|
import { ChannelChannelService } from './channels/channel.js';
|
||||||
|
@ -28,6 +29,7 @@ export class ChannelsService {
|
||||||
private localTimelineChannelService: LocalTimelineChannelService,
|
private localTimelineChannelService: LocalTimelineChannelService,
|
||||||
private hybridTimelineChannelService: HybridTimelineChannelService,
|
private hybridTimelineChannelService: HybridTimelineChannelService,
|
||||||
private globalTimelineChannelService: GlobalTimelineChannelService,
|
private globalTimelineChannelService: GlobalTimelineChannelService,
|
||||||
|
private bubbleTimelineChannelService: BubbleTimelineChannelService,
|
||||||
private userListChannelService: UserListChannelService,
|
private userListChannelService: UserListChannelService,
|
||||||
private hashtagChannelService: HashtagChannelService,
|
private hashtagChannelService: HashtagChannelService,
|
||||||
private roleTimelineChannelService: RoleTimelineChannelService,
|
private roleTimelineChannelService: RoleTimelineChannelService,
|
||||||
|
@ -48,6 +50,7 @@ export class ChannelsService {
|
||||||
case 'localTimeline': return this.localTimelineChannelService;
|
case 'localTimeline': return this.localTimelineChannelService;
|
||||||
case 'hybridTimeline': return this.hybridTimelineChannelService;
|
case 'hybridTimeline': return this.hybridTimelineChannelService;
|
||||||
case 'globalTimeline': return this.globalTimelineChannelService;
|
case 'globalTimeline': return this.globalTimelineChannelService;
|
||||||
|
case 'bubbleTimeline': return this.bubbleTimelineChannelService;
|
||||||
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 'roleTimeline': return this.roleTimelineChannelService;
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
|
import { isInstanceMuted } from '@/misc/is-instance-muted.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 type { MiMeta } from '@/models/Meta.js';
|
||||||
|
import Channel from '../channel.js';
|
||||||
|
|
||||||
|
class BubbleTimelineChannel extends Channel {
|
||||||
|
public readonly chName = 'bubbleTimeline';
|
||||||
|
public static shouldShare = false;
|
||||||
|
public static requireCredential = false;
|
||||||
|
private withRenotes: boolean;
|
||||||
|
private withFiles: boolean;
|
||||||
|
private withBots: boolean;
|
||||||
|
private instance: MiMeta;
|
||||||
|
|
||||||
|
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.btlAvailable) return;
|
||||||
|
|
||||||
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
this.withFiles = params.withFiles ?? false;
|
||||||
|
this.withBots = params.withBots ?? true;
|
||||||
|
this.instance = await this.metaService.fetch();
|
||||||
|
|
||||||
|
// Subscribe events
|
||||||
|
this.subscriber.on('notesStream', this.onNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onNote(note: Packed<'Note'>) {
|
||||||
|
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||||
|
if (!this.withBots && note.user.isBot) return;
|
||||||
|
|
||||||
|
if (!(note.user.host != null && this.instance.bubbleInstances.includes(note.user.host) && note.visibility === 'public' )) return;
|
||||||
|
|
||||||
|
if (note.channelId != null) return;
|
||||||
|
|
||||||
|
// 関係ない返信は除外
|
||||||
|
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||||
|
const reply = note.reply;
|
||||||
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||||
|
|
||||||
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||||
|
|
||||||
|
// Ignore notes from instances the user has muted
|
||||||
|
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) 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;
|
||||||
|
|
||||||
|
if (this.user && note.renoteId && !note.text) {
|
||||||
|
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
||||||
|
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
|
||||||
|
note.renote.myReaction = myRenoteReaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
this.send('note', note);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose() {
|
||||||
|
// Unsubscribe events
|
||||||
|
this.subscriber.off('notesStream', this.onNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BubbleTimelineChannelService {
|
||||||
|
public readonly shouldShare = BubbleTimelineChannel.shouldShare;
|
||||||
|
public readonly requireCredential = BubbleTimelineChannel.requireCredential;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private metaService: MetaService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private noteEntityService: NoteEntityService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public create(id: string, connection: Channel['connection']): BubbleTimelineChannel {
|
||||||
|
return new BubbleTimelineChannel(
|
||||||
|
this.metaService,
|
||||||
|
this.roleService,
|
||||||
|
this.noteEntityService,
|
||||||
|
id,
|
||||||
|
connection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,6 +117,12 @@ function connectChannel() {
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
withBots: props.withBots,
|
withBots: props.withBots,
|
||||||
});
|
});
|
||||||
|
} else if (props.src === 'bubble') {
|
||||||
|
connection = stream.useChannel('bubbleTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
withBots: props.withBots,
|
||||||
|
});
|
||||||
} else if (props.src === 'global') {
|
} else if (props.src === 'global') {
|
||||||
connection = stream.useChannel('globalTimeline', {
|
connection = stream.useChannel('globalTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
|
@ -188,6 +194,13 @@ function updatePaginationQuery() {
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
withBots: props.withBots,
|
withBots: props.withBots,
|
||||||
};
|
};
|
||||||
|
} else if (props.src === 'bubble') {
|
||||||
|
endpoint = 'notes/bubble-timeline';
|
||||||
|
query = {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
withBots: props.withBots,
|
||||||
|
};
|
||||||
} else if (props.src === 'global') {
|
} else if (props.src === 'global') {
|
||||||
endpoint = 'notes/global-timeline';
|
endpoint = 'notes/global-timeline';
|
||||||
query = {
|
query = {
|
||||||
|
|
|
@ -111,6 +111,7 @@ export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
export const ROLE_POLICIES = [
|
export const ROLE_POLICIES = [
|
||||||
'gtlAvailable',
|
'gtlAvailable',
|
||||||
'ltlAvailable',
|
'ltlAvailable',
|
||||||
|
'btlAvailable',
|
||||||
'canPublicNote',
|
'canPublicNote',
|
||||||
'canImportNotes',
|
'canImportNotes',
|
||||||
'canInvite',
|
'canInvite',
|
||||||
|
|
|
@ -34,6 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
<MkTextarea v-if="bubbleTimelineEnabled" v-model="bubbleTimeline">
|
||||||
|
<template #label>Bubble timeline</template>
|
||||||
|
<template #caption>Choose which instances should be displayed in the bubble.</template>
|
||||||
|
</MkTextarea>
|
||||||
|
|
||||||
<MkTextarea v-model="preservedUsernames">
|
<MkTextarea v-model="preservedUsernames">
|
||||||
<template #label>{{ i18n.ts.preservedUsernames }}</template>
|
<template #label>{{ i18n.ts.preservedUsernames }}</template>
|
||||||
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
||||||
|
@ -76,8 +81,10 @@ import FormLink from '@/components/form/link.vue';
|
||||||
let enableRegistration: boolean = $ref(false);
|
let enableRegistration: boolean = $ref(false);
|
||||||
let emailRequiredForSignup: boolean = $ref(false);
|
let emailRequiredForSignup: boolean = $ref(false);
|
||||||
let approvalRequiredForSignup: boolean = $ref(false);
|
let approvalRequiredForSignup: boolean = $ref(false);
|
||||||
|
let bubbleTimelineEnabled: boolean = $ref(false);
|
||||||
let sensitiveWords: string = $ref('');
|
let sensitiveWords: string = $ref('');
|
||||||
let preservedUsernames: string = $ref('');
|
let preservedUsernames: string = $ref('');
|
||||||
|
let bubbleTimeline: string = $ref('');
|
||||||
let tosUrl: string | null = $ref(null);
|
let tosUrl: string | null = $ref(null);
|
||||||
let privacyPolicyUrl: string | null = $ref(null);
|
let privacyPolicyUrl: string | null = $ref(null);
|
||||||
|
|
||||||
|
@ -90,6 +97,8 @@ async function init() {
|
||||||
preservedUsernames = meta.preservedUsernames.join('\n');
|
preservedUsernames = meta.preservedUsernames.join('\n');
|
||||||
tosUrl = meta.tosUrl;
|
tosUrl = meta.tosUrl;
|
||||||
privacyPolicyUrl = meta.privacyPolicyUrl;
|
privacyPolicyUrl = meta.privacyPolicyUrl;
|
||||||
|
bubbleTimeline = meta.bubbleInstances.join('\n');
|
||||||
|
bubbleTimelineEnabled = meta.policies.btlAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -101,6 +110,7 @@ function save() {
|
||||||
privacyPolicyUrl,
|
privacyPolicyUrl,
|
||||||
sensitiveWords: sensitiveWords.split('\n'),
|
sensitiveWords: sensitiveWords.split('\n'),
|
||||||
preservedUsernames: preservedUsernames.split('\n'),
|
preservedUsernames: preservedUsernames.split('\n'),
|
||||||
|
bubbleInstances: bubbleTimeline.split('\n'),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
|
|
@ -120,6 +120,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.btlAvailable, 'btlAvailable'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.btlAvailable }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.btlAvailable.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.btlAvailable.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.btlAvailable)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.btlAvailable.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.btlAvailable.value" :disabled="role.policies.btlAvailable.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.btlAvailable.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])">
|
||||||
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -32,6 +32,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.btlAvailable, 'btlAvailable'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.btlAvailable }}</template>
|
||||||
|
<template #suffix>{{ policies.btlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkInfo :warn="true">After enabling this option navigate to the Moderation section to configure which instances should be shown.</MkInfo>
|
||||||
|
<MkSwitch v-model="policies.btlAvailable">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])">
|
||||||
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
||||||
<template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
@ -232,6 +243,7 @@ import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkRange from '@/components/MkRange.vue';
|
import MkRange from '@/components/MkRange.vue';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
|
@ -54,6 +54,7 @@ provide('shouldOmitHeaderTitle', true);
|
||||||
|
|
||||||
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
|
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 isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
|
||||||
|
const isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable);
|
||||||
const keymap = {
|
const keymap = {
|
||||||
't': focus,
|
't': focus,
|
||||||
};
|
};
|
||||||
|
@ -207,6 +208,11 @@ const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLis
|
||||||
title: i18n.ts._timelines.social,
|
title: i18n.ts._timelines.social,
|
||||||
icon: 'ph-rocket-launch ph-bold ph-lg',
|
icon: 'ph-rocket-launch ph-bold ph-lg',
|
||||||
iconOnly: true,
|
iconOnly: true,
|
||||||
|
}] : []), ...(isBubbleTimelineAvailable ? [{
|
||||||
|
key: 'bubble',
|
||||||
|
title: 'Bubble',
|
||||||
|
icon: 'ph-drop ph-bold ph-lg',
|
||||||
|
iconOnly: true,
|
||||||
}] : []), ...(isGlobalTimelineAvailable ? [{
|
}] : []), ...(isGlobalTimelineAvailable ? [{
|
||||||
key: 'global',
|
key: 'global',
|
||||||
title: i18n.ts._timelines.global,
|
title: i18n.ts._timelines.global,
|
||||||
|
|
|
@ -29,7 +29,7 @@ export type Column = {
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
roleId?: string;
|
roleId?: string;
|
||||||
excludeTypes?: typeof notificationTypes[number][];
|
excludeTypes?: typeof notificationTypes[number][];
|
||||||
tl?: 'home' | 'local' | 'social' | 'global';
|
tl?: 'home' | 'local' | 'social' | 'global' | 'bubble';
|
||||||
withRenotes?: boolean;
|
withRenotes?: boolean;
|
||||||
withReplies?: boolean;
|
withReplies?: boolean;
|
||||||
onlyFiles?: boolean;
|
onlyFiles?: boolean;
|
||||||
|
|
|
@ -9,11 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-if="column.tl === 'home'" class="ph-house ph-bold ph-lg"></i>
|
<i v-if="column.tl === 'home'" class="ph-house ph-bold ph-lg"></i>
|
||||||
<i v-else-if="column.tl === 'local'" class="ph-planet ph-bold ph-lg"></i>
|
<i v-else-if="column.tl === 'local'" class="ph-planet ph-bold ph-lg"></i>
|
||||||
<i v-else-if="column.tl === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i>
|
<i v-else-if="column.tl === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i>
|
||||||
|
<i v-else-if="column.tl === 'bubble'" class="ph-thumb-up ph-bold ph-lg"></i>
|
||||||
<i v-else-if="column.tl === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></i>
|
<i v-else-if="column.tl === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></i>
|
||||||
<span style="margin-left: 8px;">{{ column.name }}</span>
|
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
|
<div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'bubble' && !isBubbleTimelineAvailable)) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
|
||||||
<p :class="$style.disabledTitle">
|
<p :class="$style.disabledTitle">
|
||||||
<i class="ph-minus-circle ph-bold ph-lg"></i>
|
<i class="ph-minus-circle ph-bold ph-lg"></i>
|
||||||
{{ i18n.ts._disabledTimeline.title }}
|
{{ i18n.ts._disabledTimeline.title }}
|
||||||
|
@ -52,6 +53,7 @@ let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
|
|
||||||
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
|
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 isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
|
||||||
|
const isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable);
|
||||||
const withRenotes = $ref(props.column.withRenotes ?? true);
|
const withRenotes = $ref(props.column.withRenotes ?? true);
|
||||||
const withReplies = $ref(props.column.withReplies ?? false);
|
const withReplies = $ref(props.column.withReplies ?? false);
|
||||||
const onlyFiles = $ref(props.column.onlyFiles ?? false);
|
const onlyFiles = $ref(props.column.onlyFiles ?? false);
|
||||||
|
@ -80,7 +82,8 @@ onMounted(() => {
|
||||||
} else if ($i) {
|
} else if ($i) {
|
||||||
disabled = (
|
disabled = (
|
||||||
(!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) ||
|
(!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) ||
|
||||||
(!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)));
|
(!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)) ||
|
||||||
|
(!((instance.policies.btlAvailable) || ($i.policies.btlAvailable)) && ['bubble'].includes(props.column.tl)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +96,8 @@ async function setType() {
|
||||||
value: 'local' as const, text: i18n.ts._timelines.local,
|
value: 'local' as const, text: i18n.ts._timelines.local,
|
||||||
}, {
|
}, {
|
||||||
value: 'social' as const, text: i18n.ts._timelines.social,
|
value: 'social' as const, text: i18n.ts._timelines.social,
|
||||||
|
}, {
|
||||||
|
value: 'bubble' as const, text: 'Bubble',
|
||||||
}, {
|
}, {
|
||||||
value: 'global' as const, text: i18n.ts._timelines.global,
|
value: 'global' as const, text: i18n.ts._timelines.global,
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-if="widgetProps.src === 'home'" class="ph-house ph-bold ph-lg"></i>
|
<i v-if="widgetProps.src === 'home'" class="ph-house ph-bold ph-lg"></i>
|
||||||
<i v-else-if="widgetProps.src === 'local'" class="ph-planet ph-bold ph-lg"></i>
|
<i v-else-if="widgetProps.src === 'local'" class="ph-planet ph-bold ph-lg"></i>
|
||||||
<i v-else-if="widgetProps.src === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i>
|
<i v-else-if="widgetProps.src === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i>
|
||||||
|
<i v-else-if="widgetProps.src === 'bubble'" class="ph-drop ph-bold ph-lg"></i>
|
||||||
<i v-else-if="widgetProps.src === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></i>
|
<i v-else-if="widgetProps.src === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></i>
|
||||||
<i v-else-if="widgetProps.src === 'list'" class="ph-list ph-bold ph-lg"></i>
|
<i v-else-if="widgetProps.src === 'list'" class="ph-list ph-bold ph-lg"></i>
|
||||||
<i v-else-if="widgetProps.src === 'antenna'" class="ph-flying-saucer ph-bold ph-lg"></i>
|
<i v-else-if="widgetProps.src === 'antenna'" class="ph-flying-saucer ph-bold ph-lg"></i>
|
||||||
|
@ -20,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
|
<div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'bubble' && !isBubbleTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
|
||||||
<p :class="$style.disabledTitle">
|
<p :class="$style.disabledTitle">
|
||||||
<i class="ph-minus ph-bold ph-lg"></i>
|
<i class="ph-minus ph-bold ph-lg"></i>
|
||||||
{{ i18n.ts._disabledTimeline.title }}
|
{{ i18n.ts._disabledTimeline.title }}
|
||||||
|
@ -47,6 +48,7 @@ import { instance } from '@/instance.js';
|
||||||
const name = 'timeline';
|
const name = 'timeline';
|
||||||
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
|
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 isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
|
||||||
|
const isBubbleTimelineAvailable = (($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable));
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
|
@ -126,6 +128,10 @@ const choose = async (ev) => {
|
||||||
text: i18n.ts._timelines.social,
|
text: i18n.ts._timelines.social,
|
||||||
icon: 'ph-rocket-launch ph-bold ph-lg',
|
icon: 'ph-rocket-launch ph-bold ph-lg',
|
||||||
action: () => { setSrc('social'); },
|
action: () => { setSrc('social'); },
|
||||||
|
}, {
|
||||||
|
text: 'Bubble',
|
||||||
|
icon: 'ph-drop ph-bold ph-lg',
|
||||||
|
action: () => { setSrc('bubble'); },
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts._timelines.global,
|
text: i18n.ts._timelines.global,
|
||||||
icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
|
icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
|
||||||
|
|
|
@ -16,6 +16,7 @@ namespace MisskeyEntity {
|
||||||
emojis: Array<Emoji>
|
emojis: Array<Emoji>
|
||||||
policies: {
|
policies: {
|
||||||
gtlAvailable: boolean
|
gtlAvailable: boolean
|
||||||
|
btlAvailable: boolean
|
||||||
ltlAvailable: boolean
|
ltlAvailable: boolean
|
||||||
canPublicNote: boolean
|
canPublicNote: boolean
|
||||||
canImportNotes: boolean
|
canImportNotes: boolean
|
||||||
|
|
|
@ -361,6 +361,7 @@ export type LiteInstanceMetadata = {
|
||||||
privacyPolicyUrl: string | null;
|
privacyPolicyUrl: string | null;
|
||||||
disableRegistration: boolean;
|
disableRegistration: boolean;
|
||||||
disableLocalTimeline: boolean;
|
disableLocalTimeline: boolean;
|
||||||
|
disableBubbleTimeline: boolean;
|
||||||
disableGlobalTimeline: boolean;
|
disableGlobalTimeline: boolean;
|
||||||
driveCapacityPerLocalUserMb: number;
|
driveCapacityPerLocalUserMb: number;
|
||||||
driveCapacityPerRemoteUserMb: number;
|
driveCapacityPerRemoteUserMb: number;
|
||||||
|
|
|
@ -70,6 +70,13 @@ export type Channels = {
|
||||||
};
|
};
|
||||||
receives: null;
|
receives: null;
|
||||||
};
|
};
|
||||||
|
bubbleTimeline: {
|
||||||
|
params: null;
|
||||||
|
events: {
|
||||||
|
note: (payload: Note) => void;
|
||||||
|
};
|
||||||
|
receives: null;
|
||||||
|
};
|
||||||
messaging: {
|
messaging: {
|
||||||
params: {
|
params: {
|
||||||
otherparty?: User['id'] | null;
|
otherparty?: User['id'] | null;
|
||||||
|
|
Loading…
Reference in a new issue