Recommended timeline!
This commit is contained in:
parent
494d672920
commit
b3a50187b1
18 changed files with 305 additions and 4 deletions
|
@ -34,6 +34,7 @@
|
||||||
- Yarn 3
|
- Yarn 3
|
||||||
- Saner defaults
|
- Saner defaults
|
||||||
- Star as default reaction
|
- Star as default reaction
|
||||||
|
- Recommended Instances timeline
|
||||||
- Rosé Pine by default (+ non-themable elements made Rosé Pine)
|
- Rosé Pine by default (+ non-themable elements made Rosé Pine)
|
||||||
- Better sidebar/navbar
|
- Better sidebar/navbar
|
||||||
- MOTD (customizable by admins!)
|
- 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
|
## 🚚 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
|
### 📩 Install dependencies
|
||||||
|
|
||||||
|
|
|
@ -322,6 +322,7 @@ connectService: "Connect"
|
||||||
disconnectService: "Disconnect"
|
disconnectService: "Disconnect"
|
||||||
enableLocalTimeline: "Enable local timeline"
|
enableLocalTimeline: "Enable local timeline"
|
||||||
enableGlobalTimeline: "Enable global 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."
|
disablingTimelinesInfo: "Adminstrators and Moderators will always have access to all timelines, even if they are not enabled."
|
||||||
registration: "Register"
|
registration: "Register"
|
||||||
enableRegistration: "Enable new user registration"
|
enableRegistration: "Enable new user registration"
|
||||||
|
@ -789,6 +790,7 @@ previewNoteText: "Show preview"
|
||||||
customCss: "Custom CSS"
|
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."
|
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"
|
global: "Global"
|
||||||
|
recommended: "Recommended"
|
||||||
squareAvatars: "Display squared avatars"
|
squareAvatars: "Display squared avatars"
|
||||||
sent: "Sent"
|
sent: "Sent"
|
||||||
received: "Received"
|
received: "Received"
|
||||||
|
@ -906,6 +908,8 @@ customMOTDDescription: "Custom messages for the MOTD (splash screen) separated b
|
||||||
customSplashIcons: "Custom splash screen icons (urls)"
|
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."
|
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"
|
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:
|
_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."
|
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:
|
_timelines:
|
||||||
home: "Home"
|
home: "Home"
|
||||||
local: "Local"
|
local: "Local"
|
||||||
|
recommended: "Recommended"
|
||||||
social: "Social"
|
social: "Social"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
_pages:
|
_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;
|
public disableLocalTimeline: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public disableRecommendedTimeline: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
@ -67,6 +72,11 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public pinnedUsers: string[];
|
public pinnedUsers: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public recommendedInstances: string[];
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 256, array: true, default: '{}',
|
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_unlike from './endpoints/pages/unlike.js';
|
||||||
import * as ep___pages_update from './endpoints/pages/update.js';
|
import * as ep___pages_update from './endpoints/pages/update.js';
|
||||||
import * as ep___ping from './endpoints/ping.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___pinnedUsers from './endpoints/pinned-users.js';
|
||||||
import * as ep___customMOTD from './endpoints/custom-motd.js';
|
import * as ep___customMOTD from './endpoints/custom-motd.js';
|
||||||
import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js';
|
import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js';
|
||||||
|
@ -587,6 +588,7 @@ const eps = [
|
||||||
['pages/update', ep___pages_update],
|
['pages/update', ep___pages_update],
|
||||||
['ping', ep___ping],
|
['ping', ep___ping],
|
||||||
['pinned-users', ep___pinnedUsers],
|
['pinned-users', ep___pinnedUsers],
|
||||||
|
['recommended-instances', ep___recommendedInstances],
|
||||||
['custom-motd', ep___customMOTD],
|
['custom-motd', ep___customMOTD],
|
||||||
['custom-motd', ep___customSplashIcons],
|
['custom-motd', ep___customSplashIcons],
|
||||||
['promo/read', ep___promo_read],
|
['promo/read', ep___promo_read],
|
||||||
|
|
|
@ -163,6 +163,14 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
recommendedInstances: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
pinnedUsers: {
|
pinnedUsers: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
@ -388,6 +396,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
feedbackUrl: instance.feedbackUrl,
|
feedbackUrl: instance.feedbackUrl,
|
||||||
disableRegistration: instance.disableRegistration,
|
disableRegistration: instance.disableRegistration,
|
||||||
disableLocalTimeline: instance.disableLocalTimeline,
|
disableLocalTimeline: instance.disableLocalTimeline,
|
||||||
|
disableRecommendedTimeline: instance.disableRecommendedTimeline,
|
||||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||||
|
@ -417,6 +426,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
pinnedClipId: instance.pinnedClipId,
|
pinnedClipId: instance.pinnedClipId,
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
useStarForReactionFallback: instance.useStarForReactionFallback,
|
useStarForReactionFallback: instance.useStarForReactionFallback,
|
||||||
|
recommendedInstances: instance.recommendedInstances,
|
||||||
pinnedUsers: instance.pinnedUsers,
|
pinnedUsers: instance.pinnedUsers,
|
||||||
customMOTD: instance.customMOTD,
|
customMOTD: instance.customMOTD,
|
||||||
customSplashIcons: instance.customSplashIcons,
|
customSplashIcons: instance.customSplashIcons,
|
||||||
|
|
|
@ -16,8 +16,12 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
disableRegistration: { type: 'boolean', nullable: true },
|
disableRegistration: { type: 'boolean', nullable: true },
|
||||||
disableLocalTimeline: { type: 'boolean', nullable: true },
|
disableLocalTimeline: { type: 'boolean', nullable: true },
|
||||||
|
disableRecommendedTimeline: { type: 'boolean', nullable: true },
|
||||||
disableGlobalTimeline: { type: 'boolean', nullable: true },
|
disableGlobalTimeline: { type: 'boolean', nullable: true },
|
||||||
useStarForReactionFallback: { type: 'boolean', nullable: true },
|
useStarForReactionFallback: { type: 'boolean', nullable: true },
|
||||||
|
recommendedInstances: { type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
} },
|
||||||
pinnedUsers: { type: 'array', nullable: true, items: {
|
pinnedUsers: { type: 'array', nullable: true, items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
@ -129,6 +133,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.disableLocalTimeline = ps.disableLocalTimeline;
|
set.disableLocalTimeline = ps.disableLocalTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof ps.disableRecommendedTimeline === 'boolean') {
|
||||||
|
set.disableRecommendedTimeline = ps.disableRecommendedTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof ps.disableGlobalTimeline === 'boolean') {
|
if (typeof ps.disableGlobalTimeline === 'boolean') {
|
||||||
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
||||||
}
|
}
|
||||||
|
@ -149,6 +157,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.customSplashIcons = ps.customSplashIcons.filter(Boolean);
|
set.customSplashIcons = ps.customSplashIcons.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ps.recommendedInstances)) {
|
||||||
|
set.recommendedInstances = ps.recommendedInstances.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(ps.hiddenTags)) {
|
if (Array.isArray(ps.hiddenTags)) {
|
||||||
set.hiddenTags = ps.hiddenTags.filter(Boolean);
|
set.hiddenTags = ps.hiddenTags.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
disableRecommendedTimeline: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
disableGlobalTimeline: {
|
disableGlobalTimeline: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -248,6 +252,11 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
recommended
|
||||||
|
TimeLine: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
globalTimeLine: {
|
globalTimeLine: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -356,6 +365,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
|
||||||
disableRegistration: instance.disableRegistration,
|
disableRegistration: instance.disableRegistration,
|
||||||
disableLocalTimeline: instance.disableLocalTimeline,
|
disableLocalTimeline: instance.disableLocalTimeline,
|
||||||
|
disableRecommendedTimeline: instance.disableRecommendedTimeline,
|
||||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||||
|
@ -412,6 +422,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
response.features = {
|
response.features = {
|
||||||
registration: !instance.disableRegistration,
|
registration: !instance.disableRegistration,
|
||||||
localTimeLine: !instance.disableLocalTimeline,
|
localTimeLine: !instance.disableLocalTimeline,
|
||||||
|
recommended
|
||||||
|
Timeline: !instance.disableRecommendedTimeline,
|
||||||
globalTimeLine: !instance.disableGlobalTimeline,
|
globalTimeLine: !instance.disableGlobalTimeline,
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
elasticsearch: config.elasticsearch ? true : false,
|
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,
|
feedbackUrl: meta.feedbackUrl,
|
||||||
disableRegistration: meta.disableRegistration,
|
disableRegistration: meta.disableRegistration,
|
||||||
disableLocalTimeline: meta.disableLocalTimeline,
|
disableLocalTimeline: meta.disableLocalTimeline,
|
||||||
|
disableRecommendedTimeline: meta.disableRecommendedTimeline,
|
||||||
disableGlobalTimeline: meta.disableGlobalTimeline,
|
disableGlobalTimeline: meta.disableGlobalTimeline,
|
||||||
emailRequiredForSignup: meta.emailRequiredForSignup,
|
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||||
enableHcaptcha: meta.enableHcaptcha,
|
enableHcaptcha: meta.enableHcaptcha,
|
||||||
|
|
|
@ -57,9 +57,15 @@
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
|
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
|
||||||
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</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>
|
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
<FormTextarea v-model="recommendedInstances" class="_formBlock">
|
||||||
|
<template #label>{{ i18n.ts.recommendedInstances }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.recommendedInstancesDescription }}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts.theme }}</template>
|
<template #label>{{ i18n.ts.theme }}</template>
|
||||||
|
|
||||||
|
@ -185,8 +191,10 @@ let defaultLightTheme: any = $ref(null);
|
||||||
let defaultDarkTheme: any = $ref(null);
|
let defaultDarkTheme: any = $ref(null);
|
||||||
let enableLocalTimeline: boolean = $ref(false);
|
let enableLocalTimeline: boolean = $ref(false);
|
||||||
let enableGlobalTimeline: boolean = $ref(false);
|
let enableGlobalTimeline: boolean = $ref(false);
|
||||||
|
let enableRecommendedTimeline: boolean = $ref(false);
|
||||||
let pinnedUsers: string = $ref('');
|
let pinnedUsers: string = $ref('');
|
||||||
let customMOTD: string = $ref('');
|
let customMOTD: string = $ref('');
|
||||||
|
let recommendedInstances: string = $ref('');
|
||||||
let customSplashIcons: string = $ref('');
|
let customSplashIcons: string = $ref('');
|
||||||
let cacheRemoteFiles: boolean = $ref(false);
|
let cacheRemoteFiles: boolean = $ref(false);
|
||||||
let localDriveCapacityMb: any = $ref(0);
|
let localDriveCapacityMb: any = $ref(0);
|
||||||
|
@ -214,9 +222,11 @@ async function init() {
|
||||||
maintainerEmail = meta.maintainerEmail;
|
maintainerEmail = meta.maintainerEmail;
|
||||||
enableLocalTimeline = !meta.disableLocalTimeline;
|
enableLocalTimeline = !meta.disableLocalTimeline;
|
||||||
enableGlobalTimeline = !meta.disableGlobalTimeline;
|
enableGlobalTimeline = !meta.disableGlobalTimeline;
|
||||||
|
enableRecommendedTimeline = !meta.disableRecommendedTimeline;
|
||||||
pinnedUsers = meta.pinnedUsers.join('\n');
|
pinnedUsers = meta.pinnedUsers.join('\n');
|
||||||
customMOTD = meta.customMOTD.join('\n');
|
customMOTD = meta.customMOTD.join('\n');
|
||||||
customSplashIcons = meta.customSplashIcons.join('\n');
|
customSplashIcons = meta.customSplashIcons.join('\n');
|
||||||
|
recommendedInstances = meta.recommendedInstances.join('\n');
|
||||||
cacheRemoteFiles = meta.cacheRemoteFiles;
|
cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||||
localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
|
localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
|
||||||
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
||||||
|
@ -244,9 +254,11 @@ function save() {
|
||||||
maintainerEmail,
|
maintainerEmail,
|
||||||
disableLocalTimeline: !enableLocalTimeline,
|
disableLocalTimeline: !enableLocalTimeline,
|
||||||
disableGlobalTimeline: !enableGlobalTimeline,
|
disableGlobalTimeline: !enableGlobalTimeline,
|
||||||
|
disableRecommendedTimeline: !enableRecommendedTimeline,
|
||||||
pinnedUsers: pinnedUsers.split('\n'),
|
pinnedUsers: pinnedUsers.split('\n'),
|
||||||
customMOTD: customMOTD.split('\n'),
|
customMOTD: customMOTD.split('\n'),
|
||||||
customSplashIcons: customSplashIcons.split('\n'),
|
customSplashIcons: customSplashIcons.split('\n'),
|
||||||
|
recommendedInstances: recommendedInstances.split('\n'),
|
||||||
cacheRemoteFiles,
|
cacheRemoteFiles,
|
||||||
localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
|
localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
|
||||||
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
|
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
|
const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
|
||||||
|
|
||||||
const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
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 isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||||
const keymap = {
|
const keymap = {
|
||||||
't': focus,
|
't': focus,
|
||||||
|
@ -143,6 +144,13 @@ const headerTabs = $computed(() => [{
|
||||||
title: i18n.ts._timelines.local,
|
title: i18n.ts._timelines.local,
|
||||||
icon: 'fas fa-user-group',
|
icon: 'fas fa-user-group',
|
||||||
iconOnly: true,
|
iconOnly: true,
|
||||||
|
}, ...(isRecommendedTimelineAvailable ? [{
|
||||||
|
key: 'recommended
|
||||||
|
',
|
||||||
|
title: i18n.ts._timelines.recommended
|
||||||
|
,
|
||||||
|
icon: 'fas fa-comet',
|
||||||
|
iconOnly: true,
|
||||||
}, {
|
}, {
|
||||||
key: 'social',
|
key: 'social',
|
||||||
title: i18n.ts._timelines.social,
|
title: i18n.ts._timelines.social,
|
||||||
|
@ -172,7 +180,8 @@ const headerTabsWhenNotLogin = $computed(() => [
|
||||||
|
|
||||||
definePageMetadata(computed(() => ({
|
definePageMetadata(computed(() => ({
|
||||||
title: i18n.ts.timeline,
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ onMounted(() => {
|
||||||
} else if ($i) {
|
} else if ($i) {
|
||||||
disabled = !$i.isModerator && !$i.isAdmin && (
|
disabled = !$i.isModerator && !$i.isAdmin && (
|
||||||
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
|
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
|
||||||
|
instance.disableRecommendedTimeline && ['recommended'].includes(props.column.tl) ||
|
||||||
instance.disableGlobalTimeline && ['global'].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: 'home' as const, text: i18n.ts._timelines.home,
|
||||||
}, {
|
}, {
|
||||||
value: 'local' as const, text: i18n.ts._timelines.local,
|
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,
|
value: 'social' as const, text: i18n.ts._timelines.social,
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -77,7 +77,7 @@ const announcements = {
|
||||||
endpoint: 'announcements',
|
endpoint: 'announcements',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
const isTimelineAvailable = !instance.disableLocalTimeline || !instance.disableGlobalTimeline;
|
const isTimelineAvailable = !instance.disableLocalTimeline || !instance.disableRecommendedTimeline || !instance.disableGlobalTimeline;
|
||||||
let showMenu = $ref(false);
|
let showMenu = $ref(false);
|
||||||
let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
|
let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
|
||||||
let narrow = $ref(window.innerWidth < 1280);
|
let narrow = $ref(window.innerWidth < 1280);
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
narrow: null,
|
narrow: null,
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
isTimelineAvailable: !instance.disableLocalTimeline || !instance.disableGlobalTimeline,
|
isTimelineAvailable: !instance.disableLocalTimeline || !instance.disableRecommendedTimeline || !instance.disableGlobalTimeline,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue