feat (client): pull-to-refresh timelines

based on https://github.com/misskey-dev/misskey/pull/12113

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: naskya <m@naskya.net>
Co-authored-by: Nanaka Hiira <hiira@hiira.dev>
This commit is contained in:
Fairy-Phy 2024-02-29 22:21:19 +09:00 committed by naskya
parent f8bc26bd6b
commit 4f72ade656
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
36 changed files with 534 additions and 125 deletions

View file

@ -1176,6 +1176,12 @@ private: "Private"
privateDescription: "Make visible for you only" privateDescription: "Make visible for you only"
makePrivate: "Make private" makePrivate: "Make private"
makePrivateConfirm: "This operation will send a deletion request to remote servers and change the visibility to private. Proceed?" makePrivateConfirm: "This operation will send a deletion request to remote servers and change the visibility to private. Proceed?"
enablePullToRefresh: "Enable \"Pull down to refresh\""
pullToRefreshThreshold: "Pull distance for reloading"
pullDownToReload: "Pull down to reload"
releaseToReload: "Release to reload"
reloading: "Reloading"
enableTimelineStreaming: "Update timelines automatically"
_emojiModPerm: _emojiModPerm:
unauthorized: "None" unauthorized: "None"

View file

@ -997,6 +997,12 @@ detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に
iconSet: "アイコンのスタイル" iconSet: "アイコンのスタイル"
useCdn: "CDNのアセットを利用する" useCdn: "CDNのアセットを利用する"
useCdnDescription: "このFirefishサーバーからではなくJSDelivr CDNからTwiemojiなどのアセットを読み込みます。" useCdnDescription: "このFirefishサーバーからではなくJSDelivr CDNからTwiemojiなどのアセットを読み込みます。"
enablePullToRefresh: "「下に引っ張って再読み込み」を有効にする"
pullToRefreshThreshold: "再読み込みするために引っ張る距離"
pullDownToReload: "下に引っ張って再読み込み"
releaseToReload: "離して再読み込み"
reloading: "読み込み中"
enableTimelineStreaming: "タイムラインを自動で更新する"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"

View file

@ -151,7 +151,7 @@ import XNavFolder from "@/components/MkDrive.navFolder.vue";
import XFolder from "@/components/MkDrive.folder.vue"; import XFolder from "@/components/MkDrive.folder.vue";
import XFile from "@/components/MkDrive.file.vue"; import XFile from "@/components/MkDrive.file.vue";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { uploadFile, uploads } from "@/scripts/upload"; import { uploadFile, uploads } from "@/scripts/upload";
@ -183,6 +183,8 @@ const emit = defineEmits<{
(ev: "open-folder", v: entities.DriveFolder): void; (ev: "open-folder", v: entities.DriveFolder): void;
}>(); }>();
const stream = useStream();
const loadMoreFiles = ref<InstanceType<typeof MkButton>>(); const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
const fileInput = ref<HTMLInputElement>(); const fileInput = ref<HTMLInputElement>();

View file

@ -64,7 +64,7 @@
import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { $i, isSignedIn } from "@/reactiveAccount"; import { $i, isSignedIn } from "@/reactiveAccount";
import { getUserMenu } from "@/scripts/get-user-menu"; import { getUserMenu } from "@/scripts/get-user-menu";
@ -72,6 +72,7 @@ import { useRouter } from "@/router";
import { vibrate } from "@/scripts/vibrate"; import { vibrate } from "@/scripts/vibrate";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
const stream = useStream();
const router = useRouter(); const router = useRouter();
const emit = defineEmits(["refresh"]); const emit = defineEmits(["refresh"]);

View file

@ -185,7 +185,7 @@ import { i18n } from "@/i18n";
import { getNoteMenu } from "@/scripts/get-note-menu"; import { getNoteMenu } from "@/scripts/get-note-menu";
import { useNoteCapture } from "@/scripts/use-note-capture"; import { useNoteCapture } from "@/scripts/use-note-capture";
import { deepClone } from "@/scripts/clone"; import { deepClone } from "@/scripts/clone";
import { stream } from "@/stream"; import { useStream } from "@/stream";
// import icon from "@/scripts/icon"; // import icon from "@/scripts/icon";
const props = defineProps<{ const props = defineProps<{
@ -193,6 +193,8 @@ const props = defineProps<{
pinned?: boolean; pinned?: boolean;
}>(); }>();
const stream = useStream();
const tab = ref("replies"); const tab = ref("replies");
const note = ref(deepClone(props.note)); const note = ref(deepClone(props.note));

View file

@ -1,5 +1,9 @@
<template> <template>
<MkPagination ref="pagingComponent" :pagination="pagination"> <MkPagination
ref="pagingComponent"
:pagination="pagination"
:disable-auto-load="disableAutoLoad"
>
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img <img
@ -48,6 +52,7 @@ const tlEl = ref<HTMLElement>();
defineProps<{ defineProps<{
pagination: Paging; pagination: Paging;
noGap?: boolean; noGap?: boolean;
disableAutoLoad?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<InstanceType<typeof MkPagination>>();

View file

@ -272,7 +272,7 @@ import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { useTooltip } from "@/scripts/use-tooltip"; import { useTooltip } from "@/scripts/use-tooltip";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { instance } from "@/instance"; import { instance } from "@/instance";
@ -290,6 +290,8 @@ const props = withDefaults(
}, },
); );
const stream = useStream();
const elRef = ref<HTMLElement>(null); const elRef = ref<HTMLElement>(null);
const reactionRef = ref(null); const reactionRef = ref(null);

View file

@ -53,7 +53,7 @@ import MkPagination from "@/components/MkPagination.vue";
import XNotification from "@/components/MkNotification.vue"; import XNotification from "@/components/MkNotification.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MkNote.vue"; import XNote from "@/components/MkNote.vue";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { $i } from "@/reactiveAccount"; import { $i } from "@/reactiveAccount";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -62,6 +62,8 @@ const props = defineProps<{
unreadOnly?: boolean; unreadOnly?: boolean;
}>(); }>();
const stream = useStream();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<InstanceType<typeof MkPagination>>();
const pagination: Paging = { const pagination: Paging = {
@ -87,7 +89,7 @@ const onNotification = (notification) => {
} }
if (!isMuted) { if (!isMuted) {
pagingComponent.value.prepend({ pagingComponent.value?.prepend({
...notification, ...notification,
isRead: document.visibilityState === "visible", isRead: document.visibilityState === "visible",
}); });

View file

@ -155,6 +155,7 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
.yrolvcoq { .yrolvcoq {
overscroll-behavior: none;
min-height: 100%; min-height: 100%;
background: var(--bg); background: var(--bg);
} }

View file

@ -115,6 +115,7 @@ const props = withDefaults(
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "queue", count: number): void; (ev: "queue", count: number): void;
(ev: "status", error: boolean): void;
}>(); }>();
interface Item { interface Item {
@ -185,12 +186,12 @@ const init = async (): Promise<void> => {
); );
}; };
const reload = (): void => { const reload = (): Promise<void> => {
items.value = []; items.value = [];
init(); return init();
}; };
const refresh = async (): void => { const refresh = async (): Promise<void> => {
const params = props.pagination.params const params = props.pagination.params
? isRef(props.pagination.params) ? isRef(props.pagination.params)
? props.pagination.params.value ? props.pagination.params.value
@ -446,6 +447,11 @@ watch(
{ deep: true }, { deep: true },
); );
watch(error, (n, o) => {
if (n === o) return;
emit("status", n);
});
init(); init();
onActivated(() => { onActivated(() => {

View file

@ -301,7 +301,7 @@ import { extractMentions } from "@/scripts/extract-mentions";
import { formatTimeString } from "@/scripts/format-time-string"; import { formatTimeString } from "@/scripts/format-time-string";
import { Autocomplete } from "@/scripts/autocomplete"; import { Autocomplete } from "@/scripts/autocomplete";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { selectFiles } from "@/scripts/select-file"; import { selectFiles } from "@/scripts/select-file";
import { defaultStore, notePostInterruptors, postFormActions } from "@/store"; import { defaultStore, notePostInterruptors, postFormActions } from "@/store";
import MkInfo from "@/components/MkInfo.vue"; import MkInfo from "@/components/MkInfo.vue";
@ -354,6 +354,8 @@ const emit = defineEmits<{
(ev: "esc"): void; (ev: "esc"): void;
}>(); }>();
const stream = useStream();
const textareaEl = ref<HTMLTextAreaElement | null>(null); const textareaEl = ref<HTMLTextAreaElement | null>(null);
const cwInputEl = ref<HTMLInputElement | null>(null); const cwInputEl = ref<HTMLInputElement | null>(null);
const hashtagsInputEl = ref<HTMLInputElement | null>(null); const hashtagsInputEl = ref<HTMLInputElement | null>(null);

View file

@ -0,0 +1,239 @@
<template>
<div ref="rootEl">
<div
v-if="pullStarted"
:class="$style.frame"
:style="`--frame-min-height: ${
pullDistance /
(PULL_BRAKE_BASE + pullDistance / PULL_BRAKE_FACTOR)
}px;`"
>
<div :class="$style.frameContent">
<div :class="$style.text">
<template v-if="pullEnded">{{
i18n.ts.releaseToReload
}}</template>
<template v-else-if="isRefreshing">{{
i18n.ts.reloading
}}</template>
<template v-else>{{ i18n.ts.pullDownToReload }}</template>
</div>
</div>
</div>
<div :class="{ [$style.slotClip]: pullStarted }">
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, shallowRef } from "vue";
// import { deviceKind } from "@/scripts/device-kind";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity;
const FIRE_THRESHOLD = defaultStore.state.pullToRefreshThreshold;
const RELEASE_TRANSITION_DURATION = 120;
const PULL_BRAKE_BASE = 1.5;
const PULL_BRAKE_FACTOR = 100;
const pullStarted = ref(false);
const pullEnded = ref(false);
const isRefreshing = ref(false);
const pullDistance = ref(0);
let disabled = false;
let supportPointerDesktop = false;
let startScreenY: number | null = null;
const rootEl = shallowRef<HTMLDivElement>();
let scrollEl: HTMLElement | null = null;
const props = withDefaults(
defineProps<{
refresher: () => Promise<void>;
}>(),
{
refresher: () => Promise.resolve(),
},
);
const emits = defineEmits<{
(ev: "refresh"): void;
}>();
function getScrollableParentElement(node) {
if (node == null) return null;
if (node.scrollHeight > node.clientHeight) return node;
return getScrollableParentElement(node.parentNode);
}
function getScreenY(event) {
if (supportPointerDesktop) return event.screenY;
return event.touches[0].screenY;
}
function moveStart(event) {
if (!pullStarted.value && !isRefreshing.value && !disabled) {
pullStarted.value = true;
startScreenY = getScreenY(event);
pullDistance.value = 0;
}
}
function moveBySystem(to: number): Promise<void> {
return new Promise((r) => {
const initialHeight = pullDistance.value;
const overHeight = pullDistance.value - to;
if (overHeight < 1) {
r();
return;
}
const startTime = Date.now();
let intervalId = setInterval(() => {
const time = Date.now() - startTime;
if (time > RELEASE_TRANSITION_DURATION) {
pullDistance.value = to;
clearInterval(intervalId);
r();
return;
}
const nextHeight =
initialHeight -
(overHeight / RELEASE_TRANSITION_DURATION) * time;
if (pullDistance.value < nextHeight) return;
pullDistance.value = nextHeight;
}, 1);
});
}
async function fixOverContent() {
if (pullDistance.value > FIRE_THRESHOLD) await moveBySystem(FIRE_THRESHOLD);
}
async function closeContent() {
if (pullDistance.value > 0) await moveBySystem(0);
}
function moveEnd() {
if (pullStarted.value && !isRefreshing.value) {
startScreenY = null;
if (pullEnded.value) {
pullEnded.value = false;
isRefreshing.value = true;
fixOverContent().then(() => {
emits("refresh");
props.refresher().then(() => {
refreshFinished();
});
});
} else {
closeContent().then(() => (pullStarted.value = false));
}
}
}
function moving(event) {
if (!pullStarted.value || isRefreshing.value || disabled) return;
if (scrollEl == null) scrollEl = getScrollableParentElement(rootEl);
if (
(scrollEl?.scrollTop ?? 0) >
(supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)
) {
pullDistance.value = 0;
pullEnded.value = false;
moveEnd();
return;
}
if (startScreenY === null) {
startScreenY = getScreenY(event);
}
const moveScreenY = getScreenY(event);
const moveHeight = moveScreenY - startScreenY!;
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
pullEnded.value = pullDistance.value >= FIRE_THRESHOLD;
}
function refreshFinished() {
closeContent().then(() => {
pullStarted.value = false;
isRefreshing.value = false;
});
}
function setDisabled(value) {
disabled = value;
}
onMounted(() => {
// supportPointerDesktop = !!window.PointerEvent && deviceKind === "desktop";
if (supportPointerDesktop) {
rootEl.value?.addEventListener("pointerdown", moveStart);
// "up" event won't be emmitted by mouse pointer on desktop
window.addEventListener("pointerup", moveEnd);
rootEl.value?.addEventListener("pointermove", moving, {
passive: true,
});
} else {
rootEl.value?.addEventListener("touchstart", moveStart);
rootEl.value?.addEventListener("touchend", moveEnd);
rootEl.value?.addEventListener("touchmove", moving, { passive: true });
}
});
onUnmounted(() => {
if (supportPointerDesktop) window.removeEventListener("pointerup", moveEnd);
});
defineExpose({
setDisabled,
});
</script>
<style lang="scss" module>
.frame {
position: relative;
overflow: clip;
width: 100%;
min-height: var(--frame-min-height, 0px);
mask-image: linear-gradient(90deg, #000 0%, #000 80%, transparent);
-webkit-mask-image: -webkit-linear-gradient(
90deg,
#000 0%,
#000 80%,
transparent
);
pointer-events: none;
}
.frameContent {
position: absolute;
bottom: 0;
width: 100%;
margin: 5px 0;
display: flex;
flex-direction: column;
align-items: center;
font-size: 14px;
> .icon,
> .loader {
margin: 6px 0;
}
> .icon {
transition: transform 0.25s;
&.refresh {
transform: rotate(180deg);
}
}
> .text {
margin: 5px 0;
}
}
.slotClip {
overflow-y: clip;
}
</style>

View file

@ -13,30 +13,48 @@
<button <button
class="_buttonPrimary _shadow" class="_buttonPrimary _shadow"
:class="{ instant: !defaultStore.state.animation }" :class="{ instant: !defaultStore.state.animation }"
@click="tlComponent.scrollTop()" @click="tlComponent?.scrollTop()"
> >
{{ i18n.ts.newNoteRecived }} {{ i18n.ts.newNoteRecived }}
<i :class="icon('ph-arrow-up', false)"></i> <i :class="icon('ph-arrow-up', false)"></i>
</button> </button>
</div> </div>
<MkPullToRefresh
v-if="defaultStore.state.enablePullToRefresh"
ref="pullToRefreshComponent"
:refresher="() => reloadTimeline()"
>
<XNotes
ref="tlComponent"
:no-gap="!defaultStore.state.showGapBetweenNotesInTimeline"
:pagination="pagination"
@queue="(x) => (queue = x)"
@status="pullToRefreshComponent?.setDisabled($event)"
/>
</MkPullToRefresh>
<XNotes <XNotes
v-else
ref="tlComponent" ref="tlComponent"
:no-gap="!defaultStore.state.showGapBetweenNotesInTimeline" :no-gap="!defaultStore.state.showGapBetweenNotesInTimeline"
:pagination="pagination" :pagination="pagination"
@queue="(x) => (queue = x)" @queue="(x) => (queue = x)"
@status="pullToRefreshComponent?.setDisabled($event)"
/> />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onUnmounted, provide, ref } from "vue"; import { computed, onUnmounted, provide, ref } from "vue";
import MkPullToRefresh from "@/components/MkPullToRefresh.vue";
import XNotes from "@/components/MkNotes.vue"; import XNotes from "@/components/MkNotes.vue";
import MkInfo from "@/components/MkInfo.vue"; import MkInfo from "@/components/MkInfo.vue";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import * as sound from "@/scripts/sound"; import * as sound from "@/scripts/sound";
import { $i, isSignedIn } from "@/reactiveAccount"; import { $i, isSignedIn } from "@/reactiveAccount";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { Paging } from "@/components/MkPagination.vue";
import type { Endpoints } from "firefish-js";
const props = defineProps<{ const props = defineProps<{
src: string; src: string;
@ -47,22 +65,24 @@ const props = defineProps<{
fileId?: string; fileId?: string;
}>(); }>();
const queue = ref(0);
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "note"): void; (ev: "note"): void;
(ev: "queue", count: number): void; (ev: "queue", count: number): void;
}>(); }>();
provide( const tlComponent = ref<InstanceType<typeof XNotes>>();
"inChannel", const pullToRefreshComponent = ref<InstanceType<typeof MkPullToRefresh>>();
computed(() => props.src === "channel"),
);
const tlComponent: InstanceType<typeof XNotes> = ref(); let endpoint = ""; // keyof Endpoints
let query, connection, connection2;
let tlHint: string;
let tlHintClosed: boolean;
let tlNotesCount = 0;
const queue = ref(0);
const prepend = (note) => { const prepend = (note) => {
tlComponent.value.pagingComponent?.prepend(note); tlNotesCount++;
tlComponent.value?.pagingComponent?.prepend(note);
emit("note"); emit("note");
@ -71,45 +91,16 @@ const prepend = (note) => {
} }
}; };
const onUserAdded = () => {
tlComponent.value.pagingComponent?.reload();
};
const onUserRemoved = () => {
tlComponent.value.pagingComponent?.reload();
};
const onChangeFollowing = () => {
if (!tlComponent.value.pagingComponent?.backed) {
tlComponent.value.pagingComponent?.reload();
}
};
let endpoint, query, connection, connection2, tlHint, tlHintClosed;
if (props.src === "antenna") { if (props.src === "antenna") {
endpoint = "antennas/notes"; endpoint = "antennas/notes";
query = { query = {
antennaId: props.antenna, antennaId: props.antenna,
}; };
connection = stream.useChannel("antenna", {
antennaId: props.antenna,
});
connection.on("note", prepend);
} else if (props.src === "home") { } else if (props.src === "home") {
endpoint = "notes/timeline"; endpoint = "notes/timeline";
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withReplies: defaultStore.state.showTimelineReplies,
}; };
connection = stream.useChannel("homeTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
connection2 = stream.useChannel("main");
connection2.on("follow", onChangeFollowing);
connection2.on("unfollow", onChangeFollowing);
tlHint = i18n.ts._tutorial.step5_3; tlHint = i18n.ts._tutorial.step5_3;
tlHintClosed = defaultStore.state.tlHomeHintClosed; tlHintClosed = defaultStore.state.tlHomeHintClosed;
} else if (props.src === "local") { } else if (props.src === "local") {
@ -117,11 +108,6 @@ if (props.src === "antenna") {
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withReplies: defaultStore.state.showTimelineReplies,
}; };
connection = stream.useChannel("localTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_4; tlHint = i18n.ts._tutorial.step5_4;
tlHintClosed = defaultStore.state.tlLocalHintClosed; tlHintClosed = defaultStore.state.tlLocalHintClosed;
} else if (props.src === "recommended") { } else if (props.src === "recommended") {
@ -129,11 +115,6 @@ if (props.src === "antenna") {
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withReplies: defaultStore.state.showTimelineReplies,
}; };
connection = stream.useChannel("recommendedTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_6; tlHint = i18n.ts._tutorial.step5_6;
tlHintClosed = defaultStore.state.tlRecommendedHintClosed; tlHintClosed = defaultStore.state.tlRecommendedHintClosed;
} else if (props.src === "social") { } else if (props.src === "social") {
@ -141,11 +122,6 @@ if (props.src === "antenna") {
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withReplies: defaultStore.state.showTimelineReplies,
}; };
connection = stream.useChannel("hybridTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_5; tlHint = i18n.ts._tutorial.step5_5;
tlHintClosed = defaultStore.state.tlSocialHintClosed; tlHintClosed = defaultStore.state.tlSocialHintClosed;
} else if (props.src === "global") { } else if (props.src === "global") {
@ -153,49 +129,25 @@ if (props.src === "antenna") {
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withReplies: defaultStore.state.showTimelineReplies,
}; };
connection = stream.useChannel("globalTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_7; tlHint = i18n.ts._tutorial.step5_7;
tlHintClosed = defaultStore.state.tlGlobalHintClosed; tlHintClosed = defaultStore.state.tlGlobalHintClosed;
} else if (props.src === "mentions") { } else if (props.src === "mentions") {
endpoint = "notes/mentions"; endpoint = "notes/mentions";
connection = stream.useChannel("main");
connection.on("mention", prepend);
} else if (props.src === "directs") { } else if (props.src === "directs") {
endpoint = "notes/mentions"; endpoint = "notes/mentions";
query = { query = {
visibility: "specified", visibility: "specified",
}; };
const onNote = (note) => {
if (note.visibility === "specified") {
prepend(note);
}
};
connection = stream.useChannel("main");
connection.on("mention", onNote);
} else if (props.src === "list") { } else if (props.src === "list") {
endpoint = "notes/user-list-timeline"; endpoint = "notes/user-list-timeline";
query = { query = {
listId: props.list, listId: props.list,
}; };
connection = stream.useChannel("userList", {
listId: props.list,
});
connection.on("note", prepend);
connection.on("userAdded", onUserAdded);
connection.on("userRemoved", onUserRemoved);
} else if (props.src === "channel") { } else if (props.src === "channel") {
endpoint = "channels/timeline"; endpoint = "channels/timeline";
query = { query = {
channelId: props.channel, channelId: props.channel,
}; };
connection = stream.useChannel("channel", {
channelId: props.channel,
});
connection.on("note", prepend);
} else if (props.src === "file") { } else if (props.src === "file") {
endpoint = "drive/files/attached-notes"; endpoint = "drive/files/attached-notes";
query = { query = {
@ -203,6 +155,63 @@ if (props.src === "antenna") {
}; };
} }
const stream = useStream();
function connectChannel() {
if (props.src === "antenna") {
connection = stream.useChannel("antenna", {
antennaId: props.antenna!,
});
} else if (props.src === "home") {
connection = stream.useChannel("homeTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection2 = stream.useChannel("main");
} else if (props.src === "local") {
connection = stream.useChannel("localTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
} else if (props.src === "recommended") {
connection = stream.useChannel("recommendedTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
} else if (props.src === "social") {
connection = stream.useChannel("hybridTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
} else if (props.src === "global") {
connection = stream.useChannel("globalTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
} else if (props.src === "mentions") {
connection = stream.useChannel("main");
connection.on("mention", prepend);
} else if (props.src === "directs") {
const onNote = (note) => {
if (note.visibility === "specified") {
prepend(note);
}
};
connection = stream.useChannel("main");
connection.on("mention", onNote);
} else if (props.src === "list") {
connection = stream.useChannel("userList", {
listId: props.list,
});
} else if (props.src === "channel") {
connection = stream.useChannel("channel", {
channelId: props.channel,
});
}
if (props.src !== "directs" && props.src !== "mentions")
connection.on("note", prepend);
}
provide(
"inChannel",
computed(() => props.src === "channel"),
);
function closeHint() { function closeHint() {
switch (props.src) { switch (props.src) {
case "home": case "home":
@ -223,15 +232,32 @@ function closeHint() {
} }
} }
const pagination = { if (defaultStore.state.enableTimelineStreaming) {
endpoint, connectChannel();
onUnmounted(() => {
connection.dispose();
if (connection2) connection2.dispose();
});
}
function reloadTimeline() {
return new Promise<void>((res) => {
tlNotesCount = 0;
tlComponent.value?.pagingComponent?.reload().then(() => {
res();
});
});
}
const pagination: Paging = {
endpoint: endpoint as keyof Endpoints,
limit: 10, limit: 10,
params: query, params: query,
}; };
onUnmounted(() => { onUnmounted(() => {
connection.dispose(); connection.dispose();
if (connection2) connection2.dispose(); if (connection2 != null) connection2.dispose();
}); });
/* TODO /* TODO
@ -240,6 +266,10 @@ const timetravel = (date?: Date) => {
this.$refs.tl.reload(); this.$refs.tl.reload();
}; };
*/ */
defineExpose({
reloadTimeline,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@keyframes slideUp { @keyframes slideUp {

View file

@ -52,7 +52,7 @@ import * as sound from "@/scripts/sound";
import { applyTheme } from "@/scripts/theme"; import { applyTheme } from "@/scripts/theme";
import { reloadChannel } from "@/scripts/unison-reload"; import { reloadChannel } from "@/scripts/unison-reload";
import { ColdDeviceStorage, defaultStore } from "@/store"; import { ColdDeviceStorage, defaultStore } from "@/store";
import { stream } from "@/stream"; import { useStream, isReloading } from "@/stream";
import widgets from "@/widgets"; import widgets from "@/widgets";
function checkForSplash() { function checkForSplash() {
@ -399,7 +399,10 @@ function checkForSplash() {
); );
let reloadDialogShowing = false; let reloadDialogShowing = false;
const stream = useStream();
stream.on("_disconnected_", async () => { stream.on("_disconnected_", async () => {
if (isReloading) return;
if (defaultStore.state.serverDisconnectedBehavior === "reload") { if (defaultStore.state.serverDisconnectedBehavior === "reload") {
location.reload(); location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === "dialog") { } else if (defaultStore.state.serverDisconnectedBehavior === "dialog") {

View file

@ -55,11 +55,13 @@
import { computed, onMounted, onUnmounted, ref } from "vue"; import { computed, onMounted, onUnmounted, ref } from "vue";
import XPie from "../../widgets/server-metric/pie.vue"; import XPie from "../../widgets/server-metric/pie.vue";
import bytes from "@/filters/bytes"; import bytes from "@/filters/bytes";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
const stream = useStream();
const meta = await os.api("server-info", {}); const meta = await os.api("server-info", {});
const serverStats = await os.api("stats"); const serverStats = await os.api("stats");

View file

@ -45,8 +45,9 @@
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from "vue"; import { markRaw, onMounted, onUnmounted, ref, shallowRef } from "vue";
import XChart from "./overview.queue.chart.vue"; import XChart from "./overview.queue.chart.vue";
import number from "@/filters/number"; import number from "@/filters/number";
import { stream } from "@/stream"; import { useStream } from "@/stream";
const stream = useStream();
const connection = markRaw(stream.useChannel("queueStats")); const connection = markRaw(stream.useChannel("queueStats"));
const activeSincePrevTick = ref(0); const activeSincePrevTick = ref(0);

View file

@ -69,12 +69,14 @@ import XModerators from "./overview.moderators.vue";
import XHeatmap from "./overview.heatmap.vue"; import XHeatmap from "./overview.heatmap.vue";
// import XMetrics from "./overview.metrics.vue"; // import XMetrics from "./overview.metrics.vue";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import MkFolder from "@/components/MkFolder.vue"; import MkFolder from "@/components/MkFolder.vue";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
const stream = useStream();
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const serverInfo = ref<any>(null); const serverInfo = ref<any>(null);
const topSubInstancesForPie = ref<any>(null); const topSubInstancesForPie = ref<any>(null);

View file

@ -62,9 +62,10 @@ import { markRaw, onMounted, onUnmounted, ref } from "vue";
import XChart from "./queue.chart.chart.vue"; import XChart from "./queue.chart.chart.vue";
import number from "@/filters/number"; import number from "@/filters/number";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const stream = useStream();
const connection = markRaw(stream.useChannel("queueStats")); const connection = markRaw(stream.useChannel("queueStats"));
const activeSincePrevTick = ref(0); const activeSincePrevTick = ref(0);

View file

@ -96,7 +96,7 @@ import MkButton from "@/components/MkButton.vue";
import MkChatPreview from "@/components/MkChatPreview.vue"; import MkChatPreview from "@/components/MkChatPreview.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
@ -107,6 +107,7 @@ import icon from "@/scripts/icon";
import "swiper/scss"; import "swiper/scss";
import "swiper/scss/virtual"; import "swiper/scss/virtual";
const stream = useStream();
const router = useRouter(); const router = useRouter();
const messages = ref([]); const messages = ref([]);

View file

@ -60,7 +60,7 @@ import { Autocomplete } from "@/scripts/autocomplete";
import { formatTimeString } from "@/scripts/format-time-string"; import { formatTimeString } from "@/scripts/format-time-string";
import { selectFile } from "@/scripts/select-file"; import { selectFile } from "@/scripts/select-file";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { uploadFile } from "@/scripts/upload"; import { uploadFile } from "@/scripts/upload";
@ -71,6 +71,8 @@ const props = defineProps<{
group?: entities.UserGroup | null; group?: entities.UserGroup | null;
}>(); }>();
const stream = useStream();
const textEl = ref<HTMLTextAreaElement>(); const textEl = ref<HTMLTextAreaElement>();
const fileEl = ref<HTMLInputElement>(); const fileEl = ref<HTMLInputElement>();

View file

@ -118,7 +118,7 @@ import {
scrollToBottom, scrollToBottom,
} from "@/scripts/scroll"; } from "@/scripts/scroll";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import * as sound from "@/scripts/sound"; import * as sound from "@/scripts/sound";
import { vibrate } from "@/scripts/vibrate"; import { vibrate } from "@/scripts/vibrate";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -132,6 +132,8 @@ const props = defineProps<{
groupId?: string; groupId?: string;
}>(); }>();
const stream = useStream();
const rootEl = ref<HTMLDivElement>(); const rootEl = ref<HTMLDivElement>();
const formEl = ref<InstanceType<typeof XForm>>(); const formEl = ref<InstanceType<typeof XForm>>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<InstanceType<typeof MkPagination>>();

View file

@ -163,6 +163,26 @@
{{ i18n.ts.postSearch }} {{ i18n.ts.postSearch }}
</option> </option>
</FormSelect> </FormSelect>
<FormSwitch v-model="enableTimelineStreaming" class="_formBlock">{{
i18n.ts.enableTimelineStreaming
}}</FormSwitch>
<FormSwitch v-model="enablePullToRefresh" class="_formBlock">{{
i18n.ts.enablePullToRefresh
}}</FormSwitch>
<FormRange
v-if="enablePullToRefresh"
v-model="pullToRefreshThreshold"
:min="100"
:max="300"
:step="10"
easing
class="_formBlock"
>
<template #label>{{ i18n.ts.pullToRefreshThreshold }}</template>
<template #caption>{{
i18n.ts.pullToRefreshThreshold
}}</template>
</FormRange>
</FormSection> </FormSection>
<FormSection> <FormSection>
@ -502,6 +522,15 @@ const searchURL = computed(defaultStore.makeGetterSetter("searchURL"));
const showBigPostButton = computed( const showBigPostButton = computed(
defaultStore.makeGetterSetter("showBigPostButton"), defaultStore.makeGetterSetter("showBigPostButton"),
); );
const enableTimelineStreaming = computed(
defaultStore.makeGetterSetter("enableTimelineStreaming"),
);
const enablePullToRefresh = computed(
defaultStore.makeGetterSetter("enablePullToRefresh"),
);
const pullToRefreshThreshold = computed(
defaultStore.makeGetterSetter("pullToRefreshThreshold"),
);
// This feature (along with injectPromo) is currently disabled // This feature (along with injectPromo) is currently disabled
// function onChangeInjectFeaturedNote(v) { // function onChangeInjectFeaturedNote(v) {
@ -567,6 +596,9 @@ watch(
expandOnNoteClick, expandOnNoteClick,
iconSet, iconSet,
useEmojiCdn, useEmojiCdn,
enableTimelineStreaming,
enablePullToRefresh,
pullToRefreshThreshold,
], ],
async () => { async () => {
await reloadAsk(); await reloadAsk();

View file

@ -64,13 +64,14 @@ import MkInfo from "@/components/MkInfo.vue";
import * as os from "@/os"; import * as os from "@/os";
import { ColdDeviceStorage, defaultStore } from "@/store"; import { ColdDeviceStorage, defaultStore } from "@/store";
import { unisonReload } from "@/scripts/unison-reload"; import { unisonReload } from "@/scripts/unison-reload";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { isSignedIn } from "@/reactiveAccount"; import { isSignedIn } from "@/reactiveAccount";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { host, version } from "@/config"; import { host, version } from "@/config";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
const stream = useStream();
useCssModule(); useCssModule();
const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
@ -120,6 +121,9 @@ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
"detectPostLanguage", "detectPostLanguage",
"openServerInfo", "openServerInfo",
"iconSet", "iconSet",
"enableTimelineStreaming",
"enablePullToRefresh",
"pullToRefreshThreshold",
]; ];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"lightTheme", "lightTheme",

View file

@ -6,9 +6,10 @@
:actions="headerActions" :actions="headerActions"
:tabs="headerTabs" :tabs="headerTabs"
:display-my-avatar="true" :display-my-avatar="true"
:class="{ isMobile: 'xytnxiau' }"
/> />
</template> </template>
<MkSpacer :content-max="800"> <MkSpacer :content-max="800" :class="{ isMobile: 'upsvvhaz' }">
<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf"> <div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
<XPostForm <XPostForm
v-if="defaultStore.reactiveState.showFixedPostForm.value" v-if="defaultStore.reactiveState.showFixedPostForm.value"
@ -304,6 +305,16 @@ onMounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.xytnxiau {
overflow-y: hidden;
position: absolute;
top: 0;
}
.upsvvhaz {
padding-top: 67px;
}
.cmuxhskf { .cmuxhskf {
--swiper-theme-color: var(--accent); --swiper-theme-color: var(--accent);
> .tl { > .tl {

View file

@ -3,7 +3,7 @@
import type { Ref } from "vue"; import type { Ref } from "vue";
import { onUnmounted, ref, watch } from "vue"; import { onUnmounted, ref, watch } from "vue";
import { api } from "./os"; import { api } from "./os";
import { stream } from "./stream"; import { useStream } from "./stream";
import { $i, isSignedIn } from "@/reactiveAccount"; import { $i, isSignedIn } from "@/reactiveAccount";
type StateDef = Record< type StateDef = Record<
@ -16,6 +16,7 @@ type StateDef = Record<
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never; type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
const stream = useStream();
const connection = isSignedIn && stream.useChannel("main"); const connection = isSignedIn && stream.useChannel("main");
export class Storage<T extends StateDef> { export class Storage<T extends StateDef> {

View file

@ -1,12 +1,14 @@
import { ref } from "vue"; import { ref } from "vue";
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { uploadFile } from "@/scripts/upload"; import { uploadFile } from "@/scripts/upload";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
const stream = useStream();
function select( function select(
src: any, src: any,
label: string | null, label: string | null,

View file

@ -1,7 +1,7 @@
import type { Ref } from "vue"; import type { Ref } from "vue";
import { onUnmounted } from "vue"; import { onUnmounted } from "vue";
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { $i, isSignedIn } from "@/reactiveAccount"; import { $i, isSignedIn } from "@/reactiveAccount";
import * as os from "@/os"; import * as os from "@/os";
@ -11,7 +11,7 @@ export function useNoteCapture(props: {
isDeletedRef: Ref<boolean>; isDeletedRef: Ref<boolean>;
}) { }) {
const note = props.note; const note = props.note;
const connection = isSignedIn ? stream : null; const connection = isSignedIn ? useStream() : null;
async function onStreamNoteUpdated(noteData): Promise<void> { async function onStreamNoteUpdated(noteData): Promise<void> {
const { type, id, body } = noteData; const { type, id, body } = noteData;

View file

@ -410,6 +410,18 @@ export const defaultStore = markRaw(
where: "device", where: "device",
default: false, default: false,
}, },
enableTimelineStreaming: {
where: "deviceAccount",
default: true,
},
enablePullToRefresh: {
where: "deviceAccount",
default: true,
},
pullToRefreshThreshold: {
where: "device",
default: 150,
},
}), }),
); );

View file

@ -3,22 +3,44 @@ import { markRaw } from "vue";
import { url } from "@/config"; import { url } from "@/config";
import { $i } from "@/reactiveAccount"; import { $i } from "@/reactiveAccount";
export const stream = markRaw( let stream: Stream | null = null;
new Stream( let timeoutHeartBeat: number | null = null;
url, export let isReloading: boolean = false;
$i
? {
token: $i.token,
}
: null,
),
);
window.setTimeout(heartbeat, 1000 * 60); export function useStream() {
if (stream != null) return stream;
stream = markRaw(
new Stream(
url,
$i
? {
token: $i.token,
}
: null,
),
);
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
return stream;
}
export function reloadStream() {
if (stream == null) return useStream();
if (timeoutHeartBeat != null) window.clearTimeout(timeoutHeartBeat);
isReloading = true;
stream.close();
stream.once("_connected_", () => (isReloading = false));
stream.stream.reconnect();
isReloading = false;
return stream;
}
function heartbeat(): void { function heartbeat(): void {
if (stream != null && document.visibilityState === "visible") { if (stream != null && document.visibilityState === "visible") {
stream.send("ping"); stream.send("ping");
} }
window.setTimeout(heartbeat, 1000 * 60); timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
} }

View file

@ -23,7 +23,9 @@ import { popup, popups } from "@/os";
import { uploads } from "@/scripts/upload"; import { uploads } from "@/scripts/upload";
import * as sound from "@/scripts/sound"; import * as sound from "@/scripts/sound";
import { $i, isSignedIn } from "@/reactiveAccount"; import { $i, isSignedIn } from "@/reactiveAccount";
import { stream } from "@/stream"; import { useStream } from "@/stream";
const stream = useStream();
const XStreamIndicator = defineAsyncComponent( const XStreamIndicator = defineAsyncComponent(
() => import("./stream-indicator.vue"), () => import("./stream-indicator.vue"),

View file

@ -19,13 +19,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onUnmounted, ref } from "vue"; import { onUnmounted, ref } from "vue";
import { stream } from "@/stream"; import { useStream, isReloading } from "@/stream";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
const hasDisconnected = ref(false); const hasDisconnected = ref(false);
function onDisconnected() { function onDisconnected() {
if (isReloading) return;
hasDisconnected.value = true; hasDisconnected.value = true;
} }
@ -37,6 +38,7 @@ function reload() {
location.reload(); location.reload();
} }
const stream = useStream();
stream.on("_disconnected_", onDisconnected); stream.on("_disconnected_", onDisconnected);
onUnmounted(() => { onUnmounted(() => {

View file

@ -676,7 +676,7 @@ console.log(mainRouter.currentRoute.value.name);
padding: var(--margin); padding: var(--margin);
box-sizing: border-box; box-sizing: border-box;
overflow: auto; overflow: auto;
overscroll-behavior: contain; overscroll-behavior: none;
background: var(--bg); background: var(--bg);
} }

View file

@ -127,7 +127,7 @@ import { onUnmounted, reactive } from "vue";
import type { Widget, WidgetComponentExpose } from "./widget"; import type { Widget, WidgetComponentExpose } from "./widget";
import { useWidgetPropsManager } from "./widget"; import { useWidgetPropsManager } from "./widget";
import type { GetFormResultType } from "@/scripts/form"; import type { GetFormResultType } from "@/scripts/form";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import number from "@/filters/number"; import number from "@/filters/number";
import * as sound from "@/scripts/sound"; import * as sound from "@/scripts/sound";
import { deepClone } from "@/scripts/clone"; import { deepClone } from "@/scripts/clone";
@ -161,6 +161,7 @@ const { widgetProps, configure } = useWidgetPropsManager(
emit, emit,
); );
const stream = useStream();
const connection = stream.useChannel("queueStats"); const connection = stream.useChannel("queueStats");
const current = reactive({ const current = reactive({
inbox: { inbox: {

View file

@ -30,7 +30,7 @@ import { onUnmounted, ref } from "vue";
import type { Widget, WidgetComponentExpose } from "./widget"; import type { Widget, WidgetComponentExpose } from "./widget";
import { useWidgetPropsManager } from "./widget"; import { useWidgetPropsManager } from "./widget";
import type { GetFormResultType } from "@/scripts/form"; import type { GetFormResultType } from "@/scripts/form";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import * as os from "@/os"; import * as os from "@/os";
import MkContainer from "@/components/MkContainer.vue"; import MkContainer from "@/components/MkContainer.vue";
@ -66,6 +66,7 @@ const { widgetProps, configure } = useWidgetPropsManager(
emit, emit,
); );
const stream = useStream();
const connection = stream.useChannel("main"); const connection = stream.useChannel("main");
const images = ref([]); const images = ref([]);
const fetching = ref(true); const fetching = ref(true);

View file

@ -70,7 +70,7 @@ import XMeili from "./meilisearch.vue";
import MkContainer from "@/components/MkContainer.vue"; import MkContainer from "@/components/MkContainer.vue";
import type { GetFormResultType } from "@/scripts/form"; import type { GetFormResultType } from "@/scripts/form";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { useStream } from "@/stream";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { instance } from "@/instance"; import { instance } from "@/instance";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
@ -126,6 +126,7 @@ const toggleView = () => {
save(); save();
}; };
const stream = useStream();
const connection = stream.useChannel("serverStats"); const connection = stream.useChannel("serverStats");
onUnmounted(() => { onUnmounted(() => {
connection.dispose(); connection.dispose();

View file

@ -50,7 +50,7 @@ type StreamEvents = {
* Firefish stream connection * Firefish stream connection
*/ */
export default class Stream extends EventEmitter<StreamEvents> { export default class Stream extends EventEmitter<StreamEvents> {
private stream: ReconnectingWebsocket; public stream: ReconnectingWebsocket;
public state: "initializing" | "reconnecting" | "connected" = "initializing"; public state: "initializing" | "reconnecting" | "connected" = "initializing";
private sharedConnectionPools: Pool[] = []; private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = []; private sharedConnections: SharedConnection[] = [];