feat: ability to publish timelines in signed out UI
This commit is contained in:
parent
d7d9e3c323
commit
657a242b90
15 changed files with 147 additions and 71 deletions
|
@ -10,6 +10,8 @@
|
|||
|
||||
## 主要な変更点
|
||||
|
||||
- 非ログインユーザーにもローカルタイムラインとグローバルタイムラインを公開できるように変更
|
||||
- コントロールパネルから設定すると `https://server.example.com/timeline` で公開されます
|
||||
- 検索フィルターを強化中
|
||||
- `from:me` を検索クエリの末尾につけると自分の投稿のみを検索できるように変更
|
||||
- 検索クエリの例: `予定 from:me`
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -2043,3 +2043,5 @@ pullDownToReload: "下に引っ張って再読み込み"
|
|||
enableTimelineStreaming: "タイムラインを自動で更新する"
|
||||
enablePullToRefresh: "「下に引っ張って再読み込み」を有効にする"
|
||||
pullToRefreshThreshold: "再読み込みするために引っ張る距離"
|
||||
publishTimelines: "非ログインユーザーにもタイムラインを公開する"
|
||||
publishTimelinesDescription: "有効にすると、{url} でローカルタイムラインとグローバルタイムラインが公開されます。"
|
||||
|
|
|
@ -61,6 +61,11 @@ export class Meta {
|
|||
})
|
||||
public disableGlobalTimeline: boolean;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public enableGuestTimeline: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 256,
|
||||
default: "⭐",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
@ -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);
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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,36 +206,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: "home",
|
||||
title: i18n.ts._timelines.home,
|
||||
icon: `${icon("ph-house")}`,
|
||||
iconOnly: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(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>
|
||||
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue