From ec18c532ca65c69332771bb40d1f88276acdad94 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 2 Mar 2024 00:19:05 +0900 Subject: [PATCH] feat: ability to publish timelines on signed out page --- docs/api-change.md | 1 + docs/changelog.md | 1 + locales/en-US.yml | 2 + locales/ja-JP.yml | 2 + packages/backend/src/models/entities/meta.ts | 5 + .../src/server/api/endpoints/admin/meta.ts | 1 + .../server/api/endpoints/admin/update-meta.ts | 5 + .../backend/src/server/api/endpoints/meta.ts | 8 +- .../api/stream/channels/global-timeline.ts | 2 + .../api/stream/channels/local-timeline.ts | 2 + packages/client/src/components/MkTimeline.vue | 2 +- packages/client/src/pages/admin/settings.vue | 20 +++- packages/client/src/pages/timeline.vue | 93 +++++++++++-------- packages/client/src/router.ts | 6 ++ packages/client/src/store.ts | 8 +- 15 files changed, 115 insertions(+), 43 deletions(-) diff --git a/docs/api-change.md b/docs/api-change.md index c024b00d90..1fac31a7a9 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -10,6 +10,7 @@ Breaking changes are indicated by the :warning: icon. - `untilDate` - `withFiles` - `searchCwAndAlt` +- Added `enableGuestTimeline` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional). ## v20240301 diff --git a/docs/changelog.md b/docs/changelog.md index 865ea86050..16fe645f19 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,7 @@ Critical security updates are indicated by the :warning: icon. - Introduce new full-text search engine and post search filters - Refactoring - Show unlisted posts from following users in antennas (similar to [Fedibird](https://github.com/fedibird/mastodon/tree/fedibird) and [kmyblue](https://github.com/kmycode/mastodon), unlisted posts from people you don't follow won't be shown) +- Add ability to publish the Local and Global timelines on `/timeline` page ## v20240301 diff --git a/locales/en-US.yml b/locales/en-US.yml index 0bca5e0c19..5d81f535b0 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1189,6 +1189,8 @@ searchRange: "Posted within (optional)" searchRangeDescription: "If you want to filter the time period, enter it in this format: 20220615-20231031\n\nIf you leave out the year (like 0105-0106 or 20231105-0110), it's interpreted as the current year.\n\nYou can also omit either the start or end date. For example, -0102 will filter the search results to show only posts made before 2 January this year, and 20231026- will filter the results to show only posts made after 26 October 2023." searchPostsWithFiles: "Only posts with files" searchCwAndAlt: "Include content warnings and file descriptions" +publishTimelines: "Publish timelines for visitors" +publishTimelinesDescription: "If enabled, the Local and Global timeline will be shown on {url} even when signed out." _emojiModPerm: unauthorized: "None" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a79cda6706..033efd642e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1009,6 +1009,8 @@ searchRange: "投稿期間(オプション)" searchRangeDescription: "投稿検索で投稿期間を絞りたい場合、20220615-20231031 のような形式で投稿期間を入力してください。今年の日付を指定する場合には年の指定を省略できます(0105-0106 や 20231105-0110 のように)。\n\n開始日と終了日のどちらか一方は省略可能です。例えば -0102 とすると今年1月2日までの投稿のみを、20231026- とすると2023年10月26日以降の投稿のみを検索します。" searchPostsWithFiles: "添付ファイルのある投稿のみ" searchCwAndAlt: "閲覧注意の注釈と添付ファイルの代替テキストも検索する" +publishTimelines: "非ログインユーザーにもタイムラインを公開する" +publishTimelinesDescription: "有効にすると、{url} でローカルタイムラインとグローバルタイムラインが公開されます。" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。" diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index a14967185f..aebe97c437 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -61,6 +61,11 @@ export class Meta { }) public disableGlobalTimeline: boolean; + @Column("boolean", { + default: false, + }) + public enableGuestTimeline: boolean; + @Column("varchar", { length: 256, default: "⭐", diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index b8b645bc32..bb86042b0c 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -479,6 +479,7 @@ export default define(meta, paramDef, async () => { disableLocalTimeline: instance.disableLocalTimeline, disableRecommendedTimeline: instance.disableRecommendedTimeline, disableGlobalTimeline: instance.disableGlobalTimeline, + enableGuestTimeline: instance.enableGuestTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, emailRequiredForSignup: instance.emailRequiredForSignup, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 765219fcdc..955a1d95d5 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -17,6 +17,7 @@ export const paramDef = { disableLocalTimeline: { type: "boolean", nullable: true }, disableRecommendedTimeline: { type: "boolean", nullable: true }, disableGlobalTimeline: { type: "boolean", nullable: true }, + enableGuestTimeline: { type: "boolean", nullable: true }, defaultReaction: { type: "string", nullable: true }, recommendedInstances: { type: "array", @@ -206,6 +207,10 @@ export default define(meta, paramDef, async (ps, me) => { set.disableGlobalTimeline = ps.disableGlobalTimeline; } + if (typeof ps.enableGuestTimeline === "boolean") { + set.enableGuestTimeline = ps.enableGuestTimeline; + } + if (typeof ps.defaultReaction === "string") { set.defaultReaction = ps.defaultReaction; } diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 819fccc826..bdfcd30dd9 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -111,6 +111,11 @@ export const meta = { optional: false, nullable: false, }, + enableGuestTimeline: { + type: "boolean", + optional: false, + nullable: false, + }, driveCapacityPerLocalUserMb: { type: "number", optional: false, @@ -432,6 +437,7 @@ export default define(meta, paramDef, async (ps, me) => { disableLocalTimeline: instance.disableLocalTimeline, disableRecommendedTimeline: instance.disableRecommendedTimeline, disableGlobalTimeline: instance.disableGlobalTimeline, + enableGuestTimeline: instance.enableGuestTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, emailRequiredForSignup: instance.emailRequiredForSignup, @@ -506,8 +512,8 @@ export default define(meta, paramDef, async (ps, me) => { localTimeLine: !instance.disableLocalTimeline, recommendedTimeline: !instance.disableRecommendedTimeline, globalTimeLine: !instance.disableGlobalTimeline, + guestTimeline: instance.enableGuestTimeline, emailRequiredForSignup: instance.emailRequiredForSignup, - searchFilters: config.meilisearch ? true : false, hcaptcha: instance.enableHcaptcha, recaptcha: instance.enableRecaptcha, objectStorage: instance.useObjectStorage, diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 39d0719dd3..79d2fe90ec 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -23,6 +23,8 @@ export default class extends Channel { return; } + if (!meta.enableGuestTimeline && this.user == null) return; + this.withReplies = params != null ? !!params.withReplies : true; // Subscribe events diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 88eabe991e..1df87dbfc8 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -22,6 +22,8 @@ export default class extends Channel { return; } + if (!meta.enableGuestTimeline && this.user == null) return; + this.withReplies = params != null ? !!params.withReplies : true; // Subscribe events diff --git a/packages/client/src/components/MkTimeline.vue b/packages/client/src/components/MkTimeline.vue index 0c7bb68734..ade27ced52 100644 --- a/packages/client/src/components/MkTimeline.vue +++ b/packages/client/src/components/MkTimeline.vue @@ -1,6 +1,6 @@ <template> <MkInfo - v-if="tlHint && !tlHintClosed" + v-if="tlHint && !tlHintClosed && isSignedIn" :closeable="true" class="_gap" @close="closeHint" diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index b54e5349b3..32a2dd9688 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -125,6 +125,9 @@ </FormSection> <FormSection> + <FormInfo class="_formBlock">{{ + i18n.ts.disablingTimelinesInfo + }}</FormInfo> <FormSwitch v-model="enableLocalTimeline" class="_formBlock" @@ -135,9 +138,19 @@ class="_formBlock" >{{ i18n.ts.enableGlobalTimeline }}</FormSwitch > + </FormSection> + + <FormSection> <FormInfo class="_formBlock">{{ - i18n.ts.disablingTimelinesInfo + i18n.t("publishTimelinesDescription", { + url: `${instanceDomain}/timelime`, + }) }}</FormInfo> + <FormSwitch + v-model="enableGuestTimeline" + class="_formBlock" + >{{ i18n.ts.publishTimelines }}</FormSwitch + > </FormSection> <FormSection> @@ -467,6 +480,7 @@ const defaultDarkTheme: any = ref(null); const enableLocalTimeline = ref(false); const enableGlobalTimeline = ref(false); const enableRecommendedTimeline = ref(false); +const enableGuestTimeline = ref(false); const pinnedUsers = ref(""); const customMOTD = ref(""); const recommendedInstances = ref(""); @@ -487,6 +501,7 @@ const defaultReaction = ref(""); const defaultReactionCustom = ref(""); const enableServerMachineStats = ref(false); const enableIdenticonGeneration = ref(false); +const instanceDomain = ref(""); function isValidHttpUrl(src: string) { let url: URL; @@ -522,6 +537,7 @@ function stringifyMoreUrls(src: { name: string; url: string }[]): string { async function init() { const meta = await os.api("admin/meta"); if (!meta) throw new Error("No meta"); + instanceDomain.value = meta.uri; name.value = meta.name; description.value = meta.description; tosUrl.value = meta.tosUrl; @@ -539,6 +555,7 @@ async function init() { enableLocalTimeline.value = !meta.disableLocalTimeline; enableGlobalTimeline.value = !meta.disableGlobalTimeline; enableRecommendedTimeline.value = !meta.disableRecommendedTimeline; + enableGuestTimeline.value = meta.enableGuestTimeline; pinnedUsers.value = meta.pinnedUsers.join("\n"); customMOTD.value = meta.customMOTD.join("\n"); customSplashIcons.value = meta.customSplashIcons.join("\n"); @@ -591,6 +608,7 @@ function save() { disableLocalTimeline: !enableLocalTimeline.value, disableGlobalTimeline: !enableGlobalTimeline.value, disableRecommendedTimeline: !enableRecommendedTimeline.value, + enableGuestTimeline: enableGuestTimeline.value, pinnedUsers: pinnedUsers.value.split("\n"), customMOTD: customMOTD.value.split("\n"), customSplashIcons: customSplashIcons.value.split("\n"), diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index aaf5142035..e2aeca8917 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -75,7 +75,7 @@ import * as os from "@/os"; import { defaultStore } from "@/store"; import { i18n } from "@/i18n"; import { instance } from "@/instance"; -import { $i, isSignedIn, isModerator } from "@/reactiveAccount"; +import { isModerator, isSignedIn } from "@/reactiveAccount"; import { definePageMetadata } from "@/scripts/page-metadata"; import { deviceKind } from "@/scripts/device-kind"; import icon from "@/scripts/icon"; @@ -86,6 +86,7 @@ if (isSignedIn && defaultStore.reactiveState.tutorial.value !== -1) { os.popup(XTutorial, {}, {}, "closed"); } +const isHomeTimelineAvailable = isSignedIn; const isLocalTimelineAvailable = (!instance.disableLocalTimeline && (isSignedIn || instance.enableGuestTimeline)) || @@ -101,17 +102,20 @@ const keymap = { t: focus, }; -const timelines = ["home"]; +const timelines = []; -if (isLocalTimelineAvailable) { - timelines.push("local"); +if (isHomeTimelineAvailable) { + timelines.push("home"); } -if (isLocalTimelineAvailable) { +if (isSocialTimelineAvailable) { timelines.push("social"); } if (isRecommendedTimelineAvailable) { timelines.push("recommended"); } +if (isLocalTimelineAvailable) { + timelines.push("local"); +} if (isGlobalTimelineAvailable) { timelines.push("global"); } @@ -130,17 +134,21 @@ window.addEventListener("resize", () => { const tlComponent = ref<InstanceType<typeof XTimeline>>(); const rootEl = ref<HTMLElement>(); +const timelineIndex = (timeline: string): number => { + const index = timelines.indexOf(timeline); + return index === -1 ? 0 : index; +}; + const src = computed({ get: () => defaultStore.reactiveState.tl.value.src, set: (x) => { saveSrc(x); - syncSlide(timelines.indexOf(x)); + syncSlide(timelineIndex(x)); }, }); -const lists = os.api("users/lists/list"); async function chooseList(ev: MouseEvent) { - await lists.then((res) => { + await os.api("users/lists/list").then((res) => { const items = [ { type: "link" as const, @@ -160,9 +168,8 @@ async function chooseList(ev: MouseEvent) { }); } -const antennas = os.api("antennas/list"); async function chooseAntenna(ev: MouseEvent) { - await antennas.then((res) => { + await os.api("antennas/list").then((res) => { const items = [ { type: "link" as const, @@ -197,46 +204,44 @@ function focus(): void { tlComponent.value.focus(); } -const headerActions = computed(() => [ - { - icon: `${icon("ph-list-bullets")}`, - title: i18n.ts.lists, - text: i18n.ts.lists, - iconOnly: true, - handler: chooseList, - }, - { - icon: `${icon("ph-flying-saucer")}`, - title: i18n.ts.antennas, - text: i18n.ts.antennas, - iconOnly: true, - handler: chooseAntenna, - } /* **TODO: fix timetravel** { +const headerActions = computed(() => + isSignedIn + ? [ + { + icon: `${icon("ph-list-bullets")}`, + title: i18n.ts.lists, + text: i18n.ts.lists, + iconOnly: true, + handler: chooseList, + }, + { + icon: `${icon("ph-flying-saucer")}`, + title: i18n.ts.antennas, + text: i18n.ts.antennas, + iconOnly: true, + handler: chooseAntenna, + } /* **TODO: fix timetravel** { icon: `${icon('ph-calendar-blank')}`, title: i18n.ts.jumpToSpecifiedDate, iconOnly: true, handler: timetravel, } */, -]); + ] + : [], +); const headerTabs = computed(() => [ - { - key: "home", - title: i18n.ts._timelines.home, - icon: `${icon("ph-house")}`, - iconOnly: true, - }, - ...(isLocalTimelineAvailable + ...(isHomeTimelineAvailable ? [ { - key: "local", - title: i18n.ts._timelines.local, - icon: `${icon("ph-users")}`, + key: "home", + title: i18n.ts._timelines.home, + icon: `${icon("ph-house")}`, iconOnly: true, }, ] : []), - ...(isLocalTimelineAvailable + ...(isSocialTimelineAvailable ? [ { key: "social", @@ -256,6 +261,16 @@ const headerTabs = computed(() => [ }, ] : []), + ...(isLocalTimelineAvailable + ? [ + { + key: "local", + title: i18n.ts._timelines.local, + icon: `${icon("ph-users")}`, + iconOnly: true, + }, + ] + : []), ...(isGlobalTimelineAvailable ? [ { @@ -288,7 +303,7 @@ let swiperRef: any = null; function setSwiperRef(swiper) { swiperRef = swiper; - syncSlide(timelines.indexOf(src.value)); + syncSlide(timelineIndex(src.value)); } function onSlideChange() { @@ -300,7 +315,7 @@ function syncSlide(index) { } onMounted(() => { - syncSlide(timelines.indexOf(swiperRef.activeIndex)); + syncSlide(timelineIndex(swiperRef.activeIndex)); }); </script> diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index deca773d60..d547d7177e 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -638,6 +638,12 @@ export const routes = [ component: page(() => import("./pages/my-antennas/index.vue")), loginRequired: true, }, + { + path: "/timeline", + // TODO: show not-found page if meta.enableGuestTimeline is false + // (currently it shows nothing if guest timelines are unavailable) + component: page(() => import("./pages/timeline.vue")), + }, { path: "/timeline/list/:listId", component: page(() => import("./pages/user-list-timeline.vue")), diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 23600581ef..0adaa9401b 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -1,5 +1,6 @@ import { markRaw, ref } from "vue"; import { Storage } from "./pizzax"; +import { isSignedIn } from "./reactiveAccount"; export const postFormActions = []; export const userActions = []; @@ -156,7 +157,12 @@ export const defaultStore = markRaw( tl: { where: "deviceAccount", default: { - src: "home" as "home" | "local" | "social" | "global" | "recommended", + src: (isSignedIn ? "home" : "local") as + | "home" + | "local" + | "social" + | "global" + | "recommended", arg: null, }, },