feat: ability to publish timelines on signed out page
This commit is contained in:
parent
9ce6a23266
commit
ec18c532ca
15 changed files with 115 additions and 43 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||
|
|
|
@ -61,6 +61,11 @@ export class Meta {
|
|||
})
|
||||
public disableGlobalTimeline: boolean;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public enableGuestTimeline: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 256,
|
||||
default: "⭐",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkInfo
|
||||
v-if="tlHint && !tlHintClosed"
|
||||
v-if="tlHint && !tlHintClosed && isSignedIn"
|
||||
:closeable="true"
|
||||
class="_gap"
|
||||
@close="closeHint"
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue