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:
parent
f8bc26bd6b
commit
4f72ade656
36 changed files with 534 additions and 125 deletions
|
@ -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"
|
||||||
|
|
|
@ -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: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
||||||
|
|
|
@ -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"]);
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>>();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
239
packages/client/src/components/MkPullToRefresh.vue
Normal file
239
packages/client/src/components/MkPullToRefresh.vue
Normal 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>
|
|
@ -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 {
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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([]);
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
||||||
|
|
|
@ -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>>();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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[] = [];
|
||||||
|
|
Loading…
Reference in a new issue