feat: ability to publish timelines on signed out page

This commit is contained in:
naskya 2024-03-02 00:19:05 +09:00
parent 9ce6a23266
commit ec18c532ca
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
15 changed files with 115 additions and 43 deletions

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"

View file

@ -61,6 +61,11 @@ export class Meta {
})
public disableGlobalTimeline: boolean;
@Column("boolean", {
default: false,
})
public enableGuestTimeline: boolean;
@Column("varchar", {
length: 256,
default: "⭐",

View file

@ -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,

View file

@ -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;
}

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -1,6 +1,6 @@
<template>
<MkInfo
v-if="tlHint && !tlHintClosed"
v-if="tlHint && !tlHintClosed && isSignedIn"
:closeable="true"
class="_gap"
@close="closeHint"

View file

@ -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"),

View file

@ -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>

View file

@ -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")),

View file

@ -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,
},
},