feat: ability to publish timelines in signed out UI

This commit is contained in:
naskya 2024-01-05 10:15:01 +09:00
parent d7d9e3c323
commit 657a242b90
Signed by: naskya
GPG key ID: 712D413B3A9FED5C
15 changed files with 147 additions and 71 deletions

View file

@ -10,6 +10,8 @@
## 主要な変更点
- 非ログインユーザーにもローカルタイムラインとグローバルタイムラインを公開できるように変更
- コントロールパネルから設定すると `https://server.example.com/timeline` で公開されます
- 検索フィルターを強化中
- `from:me` を検索クエリの末尾につけると自分の投稿のみを検索できるように変更
- 検索クエリの例: `予定 from:me`

View file

@ -2201,3 +2201,5 @@ moreUrls: "Pinned pages"
moreUrlsDescription: "Enter the pages you want to pin to the help menu in the lower left corner using this notation:\n\"Display name\": https://example.com/"
enablePullToRefresh: "Enable \"Pull down to refresh\""
pullToRefreshThreshold: "Pull distance for reloading"
publishTimelines: "Publish timelines for visitors"
publishTimelinesDescription: "If enabled, the Local and Global timeline will be shown on {url} even when signed out."

View file

@ -2043,3 +2043,5 @@ pullDownToReload: "下に引っ張って再読み込み"
enableTimelineStreaming: "タイムラインを自動で更新する"
enablePullToRefresh: "「下に引っ張って再読み込み」を有効にする"
pullToRefreshThreshold: "再読み込みするために引っ張る距離"
publishTimelines: "非ログインユーザーにもタイムラインを公開する"
publishTimelinesDescription: "有効にすると、{url} でローカルタイムラインとグローバルタイムラインが公開されます。"

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

@ -460,7 +460,7 @@ export const paramDef = {
required: [],
} as const;
export default define(meta, paramDef, async (ps, me) => {
export default define(meta, paramDef, async (ps) => {
const instance = await fetchMeta(true);
return {
@ -479,6 +479,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,

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",
@ -216,6 +217,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,6 +512,7 @@ export default define(meta, paramDef, async (ps, me) => {
localTimeLine: !instance.disableLocalTimeline,
recommendedTimeline: !instance.disableRecommendedTimeline,
globalTimeLine: !instance.disableGlobalTimeline,
gusstTimeline: instance.enableGuestTimeline,
emailRequiredForSignup: instance.emailRequiredForSignup,
searchFilters: false, // TODO: implement search filters
hcaptcha: instance.enableHcaptcha,

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"
@ -77,6 +77,8 @@ let tlHintClosed: boolean;
let tlNotesCount = 0;
const queue = ref(0);
const isSignedIn = $i != null;
const prepend = (note) => {
tlNotesCount++;
tlComponent.value?.pagingComponent?.prepend(note);

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,8 @@ 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 +556,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 +609,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

@ -82,24 +82,34 @@ import icon from "@/scripts/icon";
import "swiper/scss";
import "swiper/scss/virtual";
if (defaultStore.reactiveState.tutorial.value !== -1) {
const isSignedIn = $i != null;
if (isSignedIn && defaultStore.reactiveState.tutorial.value !== -1) {
os.popup(XTutorial, {}, {}, "closed");
}
const isHomeTimelineAvailable = isSignedIn;
const isLocalTimelineAvailable =
!instance.disableLocalTimeline ||
(!instance.disableLocalTimeline &&
(isSignedIn || instance.enableGuestTimeline)) ||
($i != null && ($i.isModerator || $i.isAdmin));
const isRecommendedTimelineAvailable = !instance.disableRecommendedTimeline;
const isSocialTimelineAvailable = isLocalTimelineAvailable && isSignedIn;
const isRecommendedTimelineAvailable =
!instance.disableRecommendedTimeline && isSignedIn;
const isGlobalTimelineAvailable =
!instance.disableGlobalTimeline ||
(!instance.disableGlobalTimeline &&
(isSignedIn || instance.enableGuestTimeline)) ||
($i != null && ($i.isModerator || $i.isAdmin));
const keymap = {
t: focus,
};
const timelines = ["home"];
const timelines = [];
if (isLocalTimelineAvailable) {
if (isHomeTimelineAvailable) {
timelines.push("home");
}
if (isSocialTimelineAvailable) {
timelines.push("social");
}
if (isRecommendedTimelineAvailable) {
@ -126,17 +136,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,
@ -156,9 +170,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,
@ -193,7 +206,9 @@ function focus(): void {
tlComponent.value.focus();
}
const headerActions = computed(() => [
const headerActions = computed(() =>
isSignedIn
? [
{
icon: `${icon("ph-list-bullets")}`,
title: i18n.ts.lists,
@ -213,16 +228,22 @@ const headerActions = computed(() => [
iconOnly: true,
handler: timetravel,
} */,
]);
]
: [],
);
const headerTabs = computed(() => [
...(isHomeTimelineAvailable
? [
{
key: "home",
title: i18n.ts._timelines.home,
icon: `${icon("ph-house")}`,
iconOnly: true,
},
...(isLocalTimelineAvailable
]
: []),
...(isSocialTimelineAvailable
? [
{
key: "social",
@ -284,7 +305,7 @@ let swiperRef: any = null;
function setSwiperRef(swiper) {
swiperRef = swiper;
syncSlide(timelines.indexOf(src.value));
syncSlide(timelineIndex(src.value));
}
function onSlideChange() {
@ -296,7 +317,7 @@ function syncSlide(index) {
}
onMounted(() => {
syncSlide(timelines.indexOf(swiperRef.activeIndex));
syncSlide(timelineIndex(swiperRef.activeIndex));
});
</script>

View file

@ -5,17 +5,17 @@ import { Router } from "@/nirax";
import MkError from "@/pages/_error_.vue";
import MkLoading from "@/pages/_loading_.vue";
import { $i } from "@/reactiveAccount";
// import { api } from "@/os";
import { api } from "@/os";
// function getGuestTimelineStatus() {
// api("meta", {
// detail: false,
// }).then((meta) => {
// return meta.enableGuestTimeline;
// });
// }
function getGuestTimelineStatus() {
api("meta", {
detail: false,
}).then((meta) => {
return meta.enableGuestTimeline;
});
}
// const guestTimeline = getGuestTimelineStatus();
const guestTimeline = getGuestTimelineStatus();
const page = (loader: AsyncComponentLoader<any>) =>
defineAsyncComponent({
@ -63,10 +63,6 @@ export const routes = [
path: "/instance-info/:host",
component: page(() => import("./pages/instance-info.vue")),
},
{
path: "/public/local",
component: page(() => import("./pages/no-graze.vue")),
},
{
name: "settings",
path: "/settings",
@ -642,6 +638,10 @@ export const routes = [
component: page(() => import("./pages/my-antennas/index.vue")),
loginRequired: true,
},
{
path: "/timeline",
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 { $i } 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: ($i != null ? "home" : "local") as
| "home"
| "local"
| "social"
| "global"
| "recommended",
arg: null,
},
},

View file

@ -59,10 +59,10 @@
</button>
</div>
<div class="right">
<button class="_button search" @click="search()">
<!-- <button class="_button search" @click="search()">
<i :class="icon('ph-magnifying-glass icon')"></i
><span>{{ i18n.ts.search }}</span>
</button>
</button> -->
<button class="_buttonPrimary signup" @click="signup()">
{{ i18n.ts.signup }}
</button>
@ -110,7 +110,7 @@ import XSigninDialog from "@/components/MkSigninDialog.vue";
import XSignupDialog from "@/components/MkSignupDialog.vue";
import * as os from "@/os";
import { instance } from "@/instance";
import { search } from "@/scripts/search";
// import { search } from "@/scripts/search";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
@ -161,7 +161,7 @@ export default defineComponent({
);
},
search,
// search,
},
});
</script>
@ -245,22 +245,22 @@ export default defineComponent({
> .right {
margin-inline-start: auto;
> .search {
background: var(--bg);
border-radius: 999px;
width: 230px;
line-height: $height - 20px;
margin-inline-end: 16px;
text-align: initial;
// > .search {
// background: var(--bg);
// border-radius: 999px;
// width: 230px;
// line-height: $height - 20px;
// margin-inline-end: 16px;
// text-align: initial;
> * {
opacity: 0.7;
}
// > * {
// opacity: 0.7;
// }
> .icon {
padding: 0 16px;
}
}
// > .icon {
// padding: 0 16px;
// }
// }
> .signup {
border-radius: 999px;