Recommended timeline!
This commit is contained in:
parent
494d672920
commit
b3a50187b1
|
@ -34,6 +34,7 @@
|
|||
- Yarn 3
|
||||
- Saner defaults
|
||||
- Star as default reaction
|
||||
- Recommended Instances timeline
|
||||
- Rosé Pine by default (+ non-themable elements made Rosé Pine)
|
||||
- Better sidebar/navbar
|
||||
- MOTD (customizable by admins!)
|
||||
|
|
|
@ -41,7 +41,7 @@ Misskey documentation can be found at [Misskey Hub](https://misskey-hub.net/).
|
|||
|
||||
## 🚚 Migrating from Misskey to Calckey
|
||||
|
||||
You need at least 🐢 NodeJS v16.15.0 (v18.4.0 reccomended!) and *exactly* 🧶 Yarn v3.2.1!
|
||||
You need at least 🐢 NodeJS v16.15.0 (v18.4.0 recommended!) and *exactly* 🧶 Yarn v3.2.1!
|
||||
|
||||
### 📩 Install dependencies
|
||||
|
||||
|
|
|
@ -322,6 +322,7 @@ connectService: "Connect"
|
|||
disconnectService: "Disconnect"
|
||||
enableLocalTimeline: "Enable local timeline"
|
||||
enableGlobalTimeline: "Enable global timeline"
|
||||
enableRecommendedTimeline: "Enable recommended timeline"
|
||||
disablingTimelinesInfo: "Adminstrators and Moderators will always have access to all timelines, even if they are not enabled."
|
||||
registration: "Register"
|
||||
enableRegistration: "Enable new user registration"
|
||||
|
@ -789,6 +790,7 @@ previewNoteText: "Show preview"
|
|||
customCss: "Custom CSS"
|
||||
customCssWarn: "This setting should only be used if you know what it does. Entering improper values may cause the client to stop functioning normally."
|
||||
global: "Global"
|
||||
recommended: "Recommended"
|
||||
squareAvatars: "Display squared avatars"
|
||||
sent: "Sent"
|
||||
received: "Received"
|
||||
|
@ -906,6 +908,8 @@ customMOTDDescription: "Custom messages for the MOTD (splash screen) separated b
|
|||
customSplashIcons: "Custom splash screen icons (urls)"
|
||||
customSplashIconsDescription: "URLs for custom splash screen icons separated by line breaks to be shown randomly every time a user loads/reloads the page. Please make sure the images are on a static URL, preferably all resized to 192x192."
|
||||
showUpdates: "Show a popup when Calckey updates"
|
||||
recommendedInstances: "Recommended instances"
|
||||
recommendedInstancesDescription: "Recommended instances seperated by line breaks to appear in the recommended timeline. Do NOT add `https://`, ONLY the domain."
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||
|
@ -1382,6 +1386,7 @@ _instanceCharts:
|
|||
_timelines:
|
||||
home: "Home"
|
||||
local: "Local"
|
||||
recommended: "Recommended"
|
||||
social: "Social"
|
||||
global: "Global"
|
||||
_pages:
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export class RecommendedTimeline1659042130648 {
|
||||
name = 'RecommendedTimeline1659042130648'
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "disableRecommendedTimeline" boolean NOT NULL DEFAULT true`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "recommendedInstances" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableRecommendedTimeline"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "recommendedInstances"`);
|
||||
}
|
||||
}
|
|
@ -47,6 +47,11 @@ export class Meta {
|
|||
})
|
||||
public disableLocalTimeline: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public disableRecommendedTimeline: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
@ -67,6 +72,11 @@ export class Meta {
|
|||
})
|
||||
public pinnedUsers: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, array: true, default: '{}',
|
||||
})
|
||||
public recommendedInstances: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, array: true, default: '{}',
|
||||
})
|
||||
|
|
|
@ -267,6 +267,7 @@ import * as ep___pages_show from './endpoints/pages/show.js';
|
|||
import * as ep___pages_unlike from './endpoints/pages/unlike.js';
|
||||
import * as ep___pages_update from './endpoints/pages/update.js';
|
||||
import * as ep___ping from './endpoints/ping.js';
|
||||
import * as ep___recommendedInstances from './endpoints/recommended-instances.js';
|
||||
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
||||
import * as ep___customMOTD from './endpoints/custom-motd.js';
|
||||
import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js';
|
||||
|
@ -587,6 +588,7 @@ const eps = [
|
|||
['pages/update', ep___pages_update],
|
||||
['ping', ep___ping],
|
||||
['pinned-users', ep___pinnedUsers],
|
||||
['recommended-instances', ep___recommendedInstances],
|
||||
['custom-motd', ep___customMOTD],
|
||||
['custom-motd', ep___customSplashIcons],
|
||||
['promo/read', ep___promo_read],
|
||||
|
|
|
@ -163,6 +163,14 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
recommendedInstances: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
pinnedUsers: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
|
@ -388,6 +396,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
feedbackUrl: instance.feedbackUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableRecommendedTimeline: instance.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
|
@ -417,6 +426,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
pinnedClipId: instance.pinnedClipId,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
useStarForReactionFallback: instance.useStarForReactionFallback,
|
||||
recommendedInstances: instance.recommendedInstances,
|
||||
pinnedUsers: instance.pinnedUsers,
|
||||
customMOTD: instance.customMOTD,
|
||||
customSplashIcons: instance.customSplashIcons,
|
||||
|
|
|
@ -16,8 +16,12 @@ export const paramDef = {
|
|||
properties: {
|
||||
disableRegistration: { type: 'boolean', nullable: true },
|
||||
disableLocalTimeline: { type: 'boolean', nullable: true },
|
||||
disableRecommendedTimeline: { type: 'boolean', nullable: true },
|
||||
disableGlobalTimeline: { type: 'boolean', nullable: true },
|
||||
useStarForReactionFallback: { type: 'boolean', nullable: true },
|
||||
recommendedInstances: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
pinnedUsers: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
|
@ -129,6 +133,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.disableLocalTimeline = ps.disableLocalTimeline;
|
||||
}
|
||||
|
||||
if (typeof ps.disableRecommendedTimeline === 'boolean') {
|
||||
set.disableRecommendedTimeline = ps.disableRecommendedTimeline;
|
||||
}
|
||||
|
||||
if (typeof ps.disableGlobalTimeline === 'boolean') {
|
||||
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
||||
}
|
||||
|
@ -149,6 +157,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.customSplashIcons = ps.customSplashIcons.filter(Boolean);
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.recommendedInstances)) {
|
||||
set.recommendedInstances = ps.recommendedInstances.filter(Boolean);
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.hiddenTags)) {
|
||||
set.hiddenTags = ps.hiddenTags.filter(Boolean);
|
||||
}
|
||||
|
|
|
@ -80,6 +80,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
disableRecommendedTimeline: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
disableGlobalTimeline: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -248,6 +252,11 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
recommended
|
||||
TimeLine: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
globalTimeLine: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -356,6 +365,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
disableRegistration: instance.disableRegistration,
|
||||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableRecommendedTimeline: instance.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
|
@ -412,6 +422,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
response.features = {
|
||||
registration: !instance.disableRegistration,
|
||||
localTimeLine: !instance.disableLocalTimeline,
|
||||
recommended
|
||||
Timeline: !instance.disableRecommendedTimeline,
|
||||
globalTimeLine: !instance.disableGlobalTimeline,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
elasticsearch: config.elasticsearch ? true : false,
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import { Brackets } from 'typeorm';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Notes, Users } from '@/models/index.js';
|
||||
import { activeUsersChart } from '@/services/chart/index.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
||||
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Note',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
ltlDisabled: {
|
||||
message: 'Recommended timeline has been disabled.',
|
||||
code: 'LTL_DISABLED',
|
||||
id: '45a6eb02-7695-4393-b023-dd3be9aaaefe',
|
||||
},
|
||||
},
|
||||
} 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
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const m = await fetchMeta();
|
||||
if (m.disableLocalTimeline) {
|
||||
if (user == null || (!user.isAdmin && !user.isModerator)) {
|
||||
throw new ApiError(meta.errors.ltlDisabled);
|
||||
}
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost = ANY(meta.recommendedInstances))')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMutedUserQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
if (user) generateBlockedUserQuery(query, user);
|
||||
|
||||
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 (user) {
|
||||
activeUsersChart.read(user);
|
||||
}
|
||||
});
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
// import { IsNull } from 'typeorm';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import define from '../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async () => {
|
||||
const meta = await fetchMeta();
|
||||
const instances = await Promise.all(meta.recommendedInstances.map(x => x));
|
||||
return instances;
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
import Channel from '../channel.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'recommendedTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.disableRecommendedTimeline) {
|
||||
if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return;
|
||||
}
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const meta = await fetchMeta();
|
||||
if (note.user.host !== null && !meta.recommendedInstances.includes(note.user.host)) return;
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !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.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) 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);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
// Unsubscribe events
|
||||
this.subscriber.off('notesStream', this.onNote);
|
||||
}
|
||||
}
|
|
@ -67,6 +67,7 @@ const nodeinfo2 = async () => {
|
|||
feedbackUrl: meta.feedbackUrl,
|
||||
disableRegistration: meta.disableRegistration,
|
||||
disableLocalTimeline: meta.disableLocalTimeline,
|
||||
disableRecommendedTimeline: meta.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: meta.disableGlobalTimeline,
|
||||
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||
enableHcaptcha: meta.enableHcaptcha,
|
||||
|
|
|
@ -57,9 +57,15 @@
|
|||
<FormSection>
|
||||
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="enableRecommendedTimeline" class="_formBlock">{{ i18n.ts.enableRecommendedTimeline }}</FormSwitch>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
|
||||
</FormSection>
|
||||
|
||||
<FormTextarea v-model="recommendedInstances" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.recommendedInstances }}</template>
|
||||
<template #caption>{{ i18n.ts.recommendedInstancesDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.theme }}</template>
|
||||
|
||||
|
@ -185,8 +191,10 @@ let defaultLightTheme: any = $ref(null);
|
|||
let defaultDarkTheme: any = $ref(null);
|
||||
let enableLocalTimeline: boolean = $ref(false);
|
||||
let enableGlobalTimeline: boolean = $ref(false);
|
||||
let enableRecommendedTimeline: boolean = $ref(false);
|
||||
let pinnedUsers: string = $ref('');
|
||||
let customMOTD: string = $ref('');
|
||||
let recommendedInstances: string = $ref('');
|
||||
let customSplashIcons: string = $ref('');
|
||||
let cacheRemoteFiles: boolean = $ref(false);
|
||||
let localDriveCapacityMb: any = $ref(0);
|
||||
|
@ -214,9 +222,11 @@ async function init() {
|
|||
maintainerEmail = meta.maintainerEmail;
|
||||
enableLocalTimeline = !meta.disableLocalTimeline;
|
||||
enableGlobalTimeline = !meta.disableGlobalTimeline;
|
||||
enableRecommendedTimeline = !meta.disableRecommendedTimeline;
|
||||
pinnedUsers = meta.pinnedUsers.join('\n');
|
||||
customMOTD = meta.customMOTD.join('\n');
|
||||
customSplashIcons = meta.customSplashIcons.join('\n');
|
||||
recommendedInstances = meta.recommendedInstances.join('\n');
|
||||
cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||
localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
|
||||
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
||||
|
@ -244,9 +254,11 @@ function save() {
|
|||
maintainerEmail,
|
||||
disableLocalTimeline: !enableLocalTimeline,
|
||||
disableGlobalTimeline: !enableGlobalTimeline,
|
||||
disableRecommendedTimeline: !enableRecommendedTimeline,
|
||||
pinnedUsers: pinnedUsers.split('\n'),
|
||||
customMOTD: customMOTD.split('\n'),
|
||||
customSplashIcons: customSplashIcons.split('\n'),
|
||||
recommendedInstances: recommendedInstances.split('\n'),
|
||||
cacheRemoteFiles,
|
||||
localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
|
||||
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
|
||||
|
|
|
@ -36,6 +36,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
|
||||
|
||||
const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||
const isRecommendedTimelineAvailable = !instance.disableRecommendedTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||
const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||
const keymap = {
|
||||
't': focus,
|
||||
|
@ -143,6 +144,13 @@ const headerTabs = $computed(() => [{
|
|||
title: i18n.ts._timelines.local,
|
||||
icon: 'fas fa-user-group',
|
||||
iconOnly: true,
|
||||
}, ...(isRecommendedTimelineAvailable ? [{
|
||||
key: 'recommended
|
||||
',
|
||||
title: i18n.ts._timelines.recommended
|
||||
,
|
||||
icon: 'fas fa-comet',
|
||||
iconOnly: true,
|
||||
}, {
|
||||
key: 'social',
|
||||
title: i18n.ts._timelines.social,
|
||||
|
@ -172,7 +180,8 @@ const headerTabsWhenNotLogin = $computed(() => [
|
|||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.timeline,
|
||||
icon: src === 'local' ? 'fas fa-user-group' : src === 'social' ? 'fas fa-handshake-simple' : src === 'global' ? 'fas fa-globe' : 'fas fa-home',
|
||||
icon: src === 'local' ? 'fas fa-user-group' : src === 'social' ? 'fas fa-handshake-simple' : src === 'recommended
|
||||
' ? 'fas fa-comet' : src === 'global' ? 'fas fa-globe' : 'fas fa-home',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ onMounted(() => {
|
|||
} else if ($i) {
|
||||
disabled = !$i.isModerator && !$i.isAdmin && (
|
||||
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
|
||||
instance.disableRecommendedTimeline && ['recommended'].includes(props.column.tl) ||
|
||||
instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
|
||||
}
|
||||
});
|
||||
|
@ -60,6 +61,8 @@ async function setType() {
|
|||
value: 'home' as const, text: i18n.ts._timelines.home,
|
||||
}, {
|
||||
value: 'local' as const, text: i18n.ts._timelines.local,
|
||||
}, {
|
||||
value: 'recommended' as const, text: i18n.ts._timelines.recommended,
|
||||
}, {
|
||||
value: 'social' as const, text: i18n.ts._timelines.social,
|
||||
}, {
|
||||
|
|
|
@ -77,7 +77,7 @@ const announcements = {
|
|||
endpoint: 'announcements',
|
||||
limit: 10,
|
||||
};
|
||||
const isTimelineAvailable = !instance.disableLocalTimeline || !instance.disableGlobalTimeline;
|
||||
const isTimelineAvailable = !instance.disableLocalTimeline || !instance.disableRecommendedTimeline || !instance.disableGlobalTimeline;
|
||||
let showMenu = $ref(false);
|
||||
let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
|
||||
let narrow = $ref(window.innerWidth < 1280);
|
||||
|
|
|
@ -60,7 +60,7 @@ export default defineComponent({
|
|||
return {
|
||||
narrow: null,
|
||||
showMenu: false,
|
||||
isTimelineAvailable: !instance.disableLocalTimeline || !instance.disableGlobalTimeline,
|
||||
isTimelineAvailable: !instance.disableLocalTimeline || !instance.disableRecommendedTimeline || !instance.disableGlobalTimeline,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue