feat: pull-to-refresh
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: u1-liquid <17376330+u1-liquid@users.noreply.github.com> Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
parent
b0cc6cd332
commit
b980171c0b
38 changed files with 596 additions and 197 deletions
|
@ -129,6 +129,7 @@
|
|||
|
||||
うまく動いていそうだったら本家に push されます
|
||||
|
||||
- 画面を下に引いてタイムラインなどを更新する機能を追加(Misskey から取り込み)
|
||||
- Docker ユーザーも PWA のアイコンを変更できるように
|
||||
- 以下の画像ファイルを追加して Docker コンテナを再起動し、ブラウザのキャッシュを削除して PWA をインストールしてみてください
|
||||
- `custom/assets/favicon.png`
|
||||
|
|
|
@ -1165,6 +1165,10 @@ detectPostLanguage: "Automatically detect the language and show a translate butt
|
|||
vibrate: "Play vibrations"
|
||||
openServerInfo: "Show server information by clicking the server ticker on a post"
|
||||
iconSet: "Icon set"
|
||||
releaseToReload: "Release to reload"
|
||||
reloading: "Reloading"
|
||||
pullDownToReload: "Pull down to reload"
|
||||
enableTimelineStreaming: "Update timelines automatically"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing
|
||||
|
|
|
@ -2032,3 +2032,7 @@ _iconSets:
|
|||
duotone: "2色"
|
||||
moreUrls: "固定するページ"
|
||||
moreUrlsDescription: "左下のヘルプメニューに固定したいページを以下の形式で、改行区切りで入力してください:\n\"表示名\": https://example.com/"
|
||||
releaseToReload: "離して再読み込み"
|
||||
reloading: "読み込み中"
|
||||
pullDownToReload: "下に引っ張って再読み込み"
|
||||
enableTimelineStreaming: "タイムラインを自動で更新する"
|
||||
|
|
|
@ -145,7 +145,7 @@ import XNavFolder from "@/components/MkDrive.navFolder.vue";
|
|||
import XFolder from "@/components/MkDrive.folder.vue";
|
||||
import XFile from "@/components/MkDrive.file.vue";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { defaultStore } from "@/store";
|
||||
import { i18n } from "@/i18n";
|
||||
import { uploadFile, uploads } from "@/scripts/upload";
|
||||
|
@ -178,6 +178,8 @@ const emit = defineEmits<{
|
|||
(ev: "open-folder", v: firefish.entities.DriveFolder): void;
|
||||
}>();
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
|
||||
const fileInput = ref<HTMLInputElement>();
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import type * as firefish from "firefish-js";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { getUserMenu } from "@/scripts/get-user-menu";
|
||||
|
@ -73,6 +73,7 @@ import { vibrate } from "@/scripts/vibrate";
|
|||
import icon from "@/scripts/icon";
|
||||
|
||||
const router = useRouter();
|
||||
const stream = useStream();
|
||||
|
||||
const emit = defineEmits(["refresh"]);
|
||||
const props = withDefaults(
|
||||
|
|
|
@ -186,7 +186,7 @@ import { i18n } from "@/i18n";
|
|||
import { getNoteMenu } from "@/scripts/get-note-menu";
|
||||
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||
import { deepClone } from "@/scripts/clone";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
// import icon from "@/scripts/icon";
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -194,6 +194,8 @@ const props = defineProps<{
|
|||
pinned?: boolean;
|
||||
}>();
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const tab = ref("replies");
|
||||
|
||||
const note = ref(deepClone(props.note));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination" :disable-auto-load="disableAutoLoad">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img
|
||||
|
@ -48,6 +48,7 @@ const tlEl = ref<HTMLElement>();
|
|||
defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
disableAutoLoad?: boolean;
|
||||
}>();
|
||||
|
||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
|
|
@ -272,7 +272,7 @@ import { notePage } from "@/filters/note";
|
|||
import { userPage } from "@/filters/user";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { useTooltip } from "@/scripts/use-tooltip";
|
||||
import { defaultStore } from "@/store";
|
||||
import { instance } from "@/instance";
|
||||
|
@ -290,6 +290,8 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const elRef = ref<HTMLElement>(null);
|
||||
const reactionRef = ref(null);
|
||||
|
||||
|
|
|
@ -1,59 +1,62 @@
|
|||
<template>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img
|
||||
src="/static-assets/badges/info.webp"
|
||||
class="_ghost"
|
||||
alt="Info"
|
||||
/>
|
||||
<div>{{ i18n.ts.noNotifications }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<MkPullToRefresh :refresher="() => reload()">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img
|
||||
src="/static-assets/badges/info.webp"
|
||||
class="_ghost"
|
||||
alt="Info"
|
||||
/>
|
||||
<div>{{ i18n.ts.noNotifications }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: notifications }">
|
||||
<XList
|
||||
v-slot="{ item: notification }"
|
||||
class="elsfgstc"
|
||||
:items="notifications"
|
||||
:no-gap="true"
|
||||
>
|
||||
<XNote
|
||||
v-if="
|
||||
['reply', 'quote', 'mention'].includes(
|
||||
notification.type,
|
||||
)
|
||||
"
|
||||
:key="notification.id"
|
||||
:note="notification.note"
|
||||
:collapsed-reply="
|
||||
notification.type === 'reply' ||
|
||||
(notification.type === 'mention' &&
|
||||
notification.note.replyId != null)
|
||||
"
|
||||
/>
|
||||
<XNotification
|
||||
v-else
|
||||
:key="notification.id"
|
||||
:notification="notification"
|
||||
:with-time="true"
|
||||
:full="true"
|
||||
class="_panel notification"
|
||||
/>
|
||||
</XList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
<template #default="{ items: notifications }">
|
||||
<XList
|
||||
v-slot="{ item: notification }"
|
||||
class="elsfgstc"
|
||||
:items="notifications"
|
||||
:no-gap="true"
|
||||
>
|
||||
<XNote
|
||||
v-if="
|
||||
['reply', 'quote', 'mention'].includes(
|
||||
notification.type,
|
||||
)
|
||||
"
|
||||
:key="notification.id"
|
||||
:note="notification.note"
|
||||
:collapsed-reply="
|
||||
notification.type === 'reply' ||
|
||||
(notification.type === 'mention' &&
|
||||
notification.note.replyId != null)
|
||||
"
|
||||
/>
|
||||
<XNotification
|
||||
v-else
|
||||
:key="notification.id"
|
||||
:notification="notification"
|
||||
:with-time="true"
|
||||
:full="true"
|
||||
class="_panel notification"
|
||||
/>
|
||||
</XList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkPullToRefresh>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { computed, onActivated, onMounted, onUnmounted, ref } from "vue";
|
||||
import type { notificationTypes } from "firefish-js";
|
||||
import type { Paging } from "@/components/MkPagination.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import XNotification from "@/components/MkNotification.vue";
|
||||
import MkPullToRefresh from "@/components/MkPullToRefresh.vue";
|
||||
import XList from "@/components/MkDateSeparatedList.vue";
|
||||
import XNote from "@/components/MkNote.vue";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
|
@ -62,6 +65,8 @@ const props = defineProps<{
|
|||
unreadOnly?: boolean;
|
||||
}>();
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const pagination: Paging = {
|
||||
|
@ -87,13 +92,21 @@ const onNotification = (notification) => {
|
|||
}
|
||||
|
||||
if (!isMuted) {
|
||||
pagingComponent.value.prepend({
|
||||
pagingComponent.value?.prepend({
|
||||
...notification,
|
||||
isRead: document.visibilityState === "visible",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function reload(): Promise<void> {
|
||||
return new Promise<void>((res) => {
|
||||
pagingComponent.value?.reload().then(() => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let connection;
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -133,6 +146,12 @@ onMounted(() => {
|
|||
});
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
pagingComponent.value?.reload();
|
||||
connection = useStream().useChannel("main");
|
||||
connection.on("notification", onNotification);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
|
|
|
@ -155,6 +155,7 @@ defineExpose({
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.yrolvcoq {
|
||||
overscroll-behavior: none;
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ const props = withDefaults(
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: "queue", count: number): void;
|
||||
(ev: "status", error: boolean): void;
|
||||
}>();
|
||||
|
||||
interface Item {
|
||||
|
@ -189,12 +190,12 @@ const init = async (): Promise<void> => {
|
|||
);
|
||||
};
|
||||
|
||||
const reload = (): void => {
|
||||
const reload = (): Promise<void> => {
|
||||
items.value = [];
|
||||
init();
|
||||
return init();
|
||||
};
|
||||
|
||||
const refresh = async (): void => {
|
||||
const refresh = async (): Promise<void> => {
|
||||
const params = props.pagination.params
|
||||
? isRef(props.pagination.params)
|
||||
? props.pagination.params.value
|
||||
|
@ -259,12 +260,12 @@ const fetchMore = async (): Promise<void> => {
|
|||
offset: offset.value,
|
||||
}
|
||||
: props.pagination.reversed
|
||||
? {
|
||||
sinceId: items.value[0].id,
|
||||
}
|
||||
: {
|
||||
untilId: items.value[items.value.length - 1].id,
|
||||
}),
|
||||
? {
|
||||
sinceId: items.value[0].id,
|
||||
}
|
||||
: {
|
||||
untilId: items.value[items.value.length - 1].id,
|
||||
}),
|
||||
})
|
||||
.then(
|
||||
(res) => {
|
||||
|
@ -320,12 +321,12 @@ const fetchMoreAhead = async (): Promise<void> => {
|
|||
offset: offset.value,
|
||||
}
|
||||
: props.pagination.reversed
|
||||
? {
|
||||
untilId: items.value[0].id,
|
||||
}
|
||||
: {
|
||||
sinceId: items.value[items.value.length - 1].id,
|
||||
}),
|
||||
? {
|
||||
untilId: items.value[0].id,
|
||||
}
|
||||
: {
|
||||
sinceId: items.value[items.value.length - 1].id,
|
||||
}),
|
||||
})
|
||||
.then(
|
||||
(res) => {
|
||||
|
@ -450,6 +451,11 @@ watch(
|
|||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(error, (n, o) => {
|
||||
if (n === o) return;
|
||||
emit("status", n);
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
onActivated(() => {
|
||||
|
|
|
@ -279,7 +279,7 @@ import { extractMentions } from "@/scripts/extract-mentions";
|
|||
import { formatTimeString } from "@/scripts/format-time-string";
|
||||
import { Autocomplete } from "@/scripts/autocomplete";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { selectFiles } from "@/scripts/select-file";
|
||||
import { defaultStore, notePostInterruptors, postFormActions } from "@/store";
|
||||
import MkInfo from "@/components/MkInfo.vue";
|
||||
|
@ -325,6 +325,8 @@ const emit = defineEmits<{
|
|||
(ev: "esc"): void;
|
||||
}>();
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const textareaEl = ref<HTMLTextAreaElement | null>(null);
|
||||
const cwInputEl = ref<HTMLInputElement | null>(null);
|
||||
const hashtagsInputEl = ref<HTMLInputElement | null>(null);
|
||||
|
|
252
packages/client/src/components/MkPullToRefresh.vue
Normal file
252
packages/client/src/components/MkPullToRefresh.vue
Normal file
|
@ -0,0 +1,252 @@
|
|||
<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">
|
||||
<MkLoading
|
||||
v-if="isRefreshing"
|
||||
:class="$style.loader"
|
||||
:em="true"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
:class="[
|
||||
icon('ph-arrow-down'),
|
||||
$style.icon,
|
||||
{ [$style.refresh]: pullEnded },
|
||||
]"
|
||||
></i>
|
||||
<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 icon from "@/scripts/icon";
|
||||
|
||||
const SCROLL_STOP = 10;
|
||||
const MAX_PULL_DISTANCE = Infinity;
|
||||
const FIRE_THRESHOLD = 230;
|
||||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 1.5;
|
||||
const PULL_BRAKE_FACTOR = 170;
|
||||
|
||||
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,39 @@
|
|||
<button
|
||||
class="_buttonPrimary _shadow"
|
||||
:class="{ instant: !defaultStore.state.animation }"
|
||||
@click="tlComponent.scrollTop()"
|
||||
@click="tlComponent?.scrollTop()"
|
||||
>
|
||||
{{ i18n.ts.newNoteRecived }}
|
||||
<i :class="icon('ph-arrow-up', false)"></i>
|
||||
</button>
|
||||
</div>
|
||||
<XNotes
|
||||
ref="tlComponent"
|
||||
:no-gap="!defaultStore.state.showGapBetweenNotesInTimeline"
|
||||
:pagination="pagination"
|
||||
@queue="(x) => (queue = x)"
|
||||
/>
|
||||
<MkPullToRefresh
|
||||
ref="pullToRefreshComponent"
|
||||
:refresher="() => reloadTimeline()"
|
||||
>
|
||||
<XNotes
|
||||
ref="tlComponent"
|
||||
:no-gap="!defaultStore.state.showGapBetweenNotesInTimeline"
|
||||
:pagination="pagination"
|
||||
@queue="(x) => (queue = x)"
|
||||
@status="pullToRefreshComponent?.setDisabled($event)"
|
||||
/>
|
||||
</MkPullToRefresh>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, provide, ref } from "vue";
|
||||
import XNotes from "@/components/MkNotes.vue";
|
||||
import MkPullToRefresh from "@/components/MkPullToRefresh.vue";
|
||||
import MkInfo from "@/components/MkInfo.vue";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import * as sound from "@/scripts/sound";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
import icon from "@/scripts/icon";
|
||||
import type { Paging } from "@/components/MkPagination.vue";
|
||||
import { Endpoints } from "firefish-js";
|
||||
|
||||
const props = defineProps<{
|
||||
src: string;
|
||||
|
@ -47,69 +56,37 @@ const props = defineProps<{
|
|||
fileId?: string;
|
||||
}>();
|
||||
|
||||
const queue = ref(0);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "note"): void;
|
||||
(ev: "queue", count: number): void;
|
||||
}>();
|
||||
|
||||
provide(
|
||||
"inChannel",
|
||||
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) => {
|
||||
tlComponent.value.pagingComponent?.prepend(note);
|
||||
|
||||
tlNotesCount++;
|
||||
tlComponent.value?.pagingComponent?.prepend(note);
|
||||
emit("note");
|
||||
|
||||
if (props.sound) {
|
||||
sound.play($i && note.userId === $i.id ? "noteMy" : "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") {
|
||||
endpoint = "antennas/notes";
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
connection = stream.useChannel("antenna", {
|
||||
antennaId: props.antenna,
|
||||
});
|
||||
connection.on("note", prepend);
|
||||
} else if (props.src === "home") {
|
||||
endpoint = "notes/timeline";
|
||||
query = {
|
||||
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;
|
||||
tlHintClosed = defaultStore.state.tlHomeHintClosed;
|
||||
} else if (props.src === "local") {
|
||||
|
@ -117,11 +94,6 @@ if (props.src === "antenna") {
|
|||
query = {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
};
|
||||
connection = stream.useChannel("localTimeline", {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
});
|
||||
connection.on("note", prepend);
|
||||
|
||||
tlHint = i18n.ts._tutorial.step5_4;
|
||||
tlHintClosed = defaultStore.state.tlLocalHintClosed;
|
||||
} else if (props.src === "recommended") {
|
||||
|
@ -129,11 +101,6 @@ if (props.src === "antenna") {
|
|||
query = {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
};
|
||||
connection = stream.useChannel("recommendedTimeline", {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
});
|
||||
connection.on("note", prepend);
|
||||
|
||||
tlHint = i18n.ts._tutorial.step5_6;
|
||||
tlHintClosed = defaultStore.state.tlRecommendedHintClosed;
|
||||
} else if (props.src === "social") {
|
||||
|
@ -141,11 +108,6 @@ if (props.src === "antenna") {
|
|||
query = {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
};
|
||||
connection = stream.useChannel("hybridTimeline", {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
});
|
||||
connection.on("note", prepend);
|
||||
|
||||
tlHint = i18n.ts._tutorial.step5_5;
|
||||
tlHintClosed = defaultStore.state.tlSocialHintClosed;
|
||||
} else if (props.src === "global") {
|
||||
|
@ -153,49 +115,25 @@ if (props.src === "antenna") {
|
|||
query = {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
};
|
||||
connection = stream.useChannel("globalTimeline", {
|
||||
withReplies: defaultStore.state.showTimelineReplies,
|
||||
});
|
||||
connection.on("note", prepend);
|
||||
|
||||
tlHint = i18n.ts._tutorial.step5_7;
|
||||
tlHintClosed = defaultStore.state.tlGlobalHintClosed;
|
||||
} else if (props.src === "mentions") {
|
||||
endpoint = "notes/mentions";
|
||||
connection = stream.useChannel("main");
|
||||
connection.on("mention", prepend);
|
||||
} else if (props.src === "directs") {
|
||||
endpoint = "notes/mentions";
|
||||
query = {
|
||||
visibility: "specified",
|
||||
};
|
||||
const onNote = (note) => {
|
||||
if (note.visibility === "specified") {
|
||||
prepend(note);
|
||||
}
|
||||
};
|
||||
connection = stream.useChannel("main");
|
||||
connection.on("mention", onNote);
|
||||
} else if (props.src === "list") {
|
||||
endpoint = "notes/user-list-timeline";
|
||||
query = {
|
||||
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") {
|
||||
endpoint = "channels/timeline";
|
||||
query = {
|
||||
channelId: props.channel,
|
||||
};
|
||||
connection = stream.useChannel("channel", {
|
||||
channelId: props.channel,
|
||||
});
|
||||
connection.on("note", prepend);
|
||||
} else if (props.src === "file") {
|
||||
endpoint = "drive/files/attached-notes";
|
||||
query = {
|
||||
|
@ -203,6 +141,66 @@ 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"),
|
||||
);
|
||||
|
||||
const pullToRefreshComponent = ref<InstanceType<typeof MkPullToRefresh>>();
|
||||
const tlComponent = ref<InstanceType<typeof XNotes>>();
|
||||
|
||||
function closeHint() {
|
||||
switch (props.src) {
|
||||
case "home":
|
||||
|
@ -223,15 +221,34 @@ function closeHint() {
|
|||
}
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
endpoint,
|
||||
|
||||
|
||||
if (defaultStore.state.enableTimelineStreaming) {
|
||||
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,
|
||||
params: query,
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
if (connection2 != null) connection2.dispose();
|
||||
});
|
||||
|
||||
/* TODO
|
||||
|
@ -240,7 +257,12 @@ const timetravel = (date?: Date) => {
|
|||
this.$refs.tl.reload();
|
||||
};
|
||||
*/
|
||||
|
||||
defineExpose({
|
||||
reloadTimeline,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes slideUp {
|
||||
to {
|
||||
|
|
|
@ -53,7 +53,7 @@ import * as sound from "@/scripts/sound";
|
|||
import { applyTheme } from "@/scripts/theme";
|
||||
import { reloadChannel } from "@/scripts/unison-reload";
|
||||
import { ColdDeviceStorage, defaultStore } from "@/store";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream, isReloading } from "@/stream";
|
||||
import widgets from "@/widgets";
|
||||
|
||||
function checkForSplash() {
|
||||
|
@ -376,7 +376,9 @@ function checkForSplash() {
|
|||
);
|
||||
|
||||
let reloadDialogShowing = false;
|
||||
const stream = useStream();
|
||||
stream.on("_disconnected_", async () => {
|
||||
if (isReloading) return;
|
||||
if (defaultStore.state.serverDisconnectedBehavior === "reload") {
|
||||
location.reload();
|
||||
} else if (defaultStore.state.serverDisconnectedBehavior === "dialog") {
|
||||
|
|
|
@ -55,11 +55,13 @@
|
|||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import XPie from "../../widgets/server-metric/pie.vue";
|
||||
import bytes from "@/filters/bytes";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const meta = await os.api("server-info", {});
|
||||
const serverStats = await os.api("stats");
|
||||
|
||||
|
|
|
@ -45,8 +45,9 @@
|
|||
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from "vue";
|
||||
import XChart from "./overview.queue.chart.vue";
|
||||
import number from "@/filters/number";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
|
||||
const strema = useStream();
|
||||
const connection = markRaw(stream.useChannel("queueStats"));
|
||||
|
||||
const activeSincePrevTick = ref(0);
|
||||
|
|
|
@ -69,12 +69,14 @@ import XModerators from "./overview.moderators.vue";
|
|||
import XHeatmap from "./overview.heatmap.vue";
|
||||
// import XMetrics from "./overview.metrics.vue";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import MkFolder from "@/components/MkFolder.vue";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const serverInfo = 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 number from "@/filters/number";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const stream = useStream();
|
||||
const connection = markRaw(stream.useChannel("queueStats"));
|
||||
|
||||
const activeSincePrevTick = ref(0);
|
||||
|
|
|
@ -96,7 +96,7 @@ import MkButton from "@/components/MkButton.vue";
|
|||
import MkChatPreview from "@/components/MkChatPreview.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { useRouter } from "@/router";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
@ -107,6 +107,7 @@ import icon from "@/scripts/icon";
|
|||
import "swiper/scss";
|
||||
import "swiper/scss/virtual";
|
||||
|
||||
const stream = useStream();
|
||||
const router = useRouter();
|
||||
|
||||
const messages = ref([]);
|
||||
|
|
|
@ -60,7 +60,7 @@ import { Autocomplete } from "@/scripts/autocomplete";
|
|||
import { formatTimeString } from "@/scripts/format-time-string";
|
||||
import { selectFile } from "@/scripts/select-file";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { defaultStore } from "@/store";
|
||||
import { i18n } from "@/i18n";
|
||||
import { uploadFile } from "@/scripts/upload";
|
||||
|
@ -71,6 +71,8 @@ const props = defineProps<{
|
|||
group?: firefish.entities.UserGroup | null;
|
||||
}>();
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const textEl = ref<HTMLTextAreaElement>();
|
||||
const fileEl = ref<HTMLInputElement>();
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ import {
|
|||
scrollToBottom,
|
||||
} from "@/scripts/scroll";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import * as sound from "@/scripts/sound";
|
||||
import { vibrate } from "@/scripts/vibrate";
|
||||
import { i18n } from "@/i18n";
|
||||
|
@ -132,6 +132,8 @@ const props = defineProps<{
|
|||
groupId?: string;
|
||||
}>();
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const rootEl = ref<HTMLDivElement>();
|
||||
const formEl = ref<InstanceType<typeof XForm>>();
|
||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
|
|
@ -6,9 +6,13 @@
|
|||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
:display-my-avatar="true"
|
||||
class="xytnxiau"
|
||||
/>
|
||||
</template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkSpacer
|
||||
:content-max="800"
|
||||
:style="`padding-top: ${isMobile ? 67 : 79}px`"
|
||||
>
|
||||
<swiper
|
||||
:round-lengths="true"
|
||||
:touch-angle="25"
|
||||
|
@ -187,3 +191,11 @@ function syncSlide(index) {
|
|||
swiperRef.slideTo(index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xytnxiau {
|
||||
overflow-y: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -93,6 +93,9 @@
|
|||
class="_formBlock"
|
||||
>{{ i18n.ts.swipeOnDesktop }}</FormSwitch
|
||||
>
|
||||
<FormSwitch v-model="enableTimelineStreaming" class="_formBlock">{{
|
||||
i18n.ts.enableTimelineStreaming
|
||||
}}</FormSwitch>
|
||||
<FormSwitch v-model="enterSendsMessage" class="_formBlock">{{
|
||||
i18n.ts.enterSendsMessage
|
||||
}}</FormSwitch>
|
||||
|
@ -539,6 +542,9 @@ const openServerInfo = computed(
|
|||
);
|
||||
const searchURL = computed(defaultStore.makeGetterSetter("searchURL"));
|
||||
const iconSet = computed(defaultStore.makeGetterSetter("iconSet"));
|
||||
const enableTimelineStreaming = computed(
|
||||
defaultStore.makeGetterSetter("enableTimelineStreaming"),
|
||||
);
|
||||
|
||||
// This feature (along with injectPromo) is currently disabled
|
||||
// function onChangeInjectFeaturedNote(v) {
|
||||
|
@ -603,6 +609,7 @@ watch(
|
|||
autoplayMfm,
|
||||
expandOnNoteClick,
|
||||
iconSet,
|
||||
enableTimelineStreaming,
|
||||
],
|
||||
async () => {
|
||||
await reloadAsk();
|
||||
|
|
|
@ -64,13 +64,14 @@ import MkInfo from "@/components/MkInfo.vue";
|
|||
import * as os from "@/os";
|
||||
import { ColdDeviceStorage, defaultStore } from "@/store";
|
||||
import { unisonReload } from "@/scripts/unison-reload";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { i18n } from "@/i18n";
|
||||
import { host, version } from "@/config";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
const stream = useStream();
|
||||
useCssModule();
|
||||
|
||||
const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
|
||||
|
|
|
@ -6,9 +6,13 @@
|
|||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
:display-my-avatar="true"
|
||||
class="xytnxiau"
|
||||
/>
|
||||
</template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkSpacer
|
||||
:content-max="800"
|
||||
:style="`padding-top: ${isMobile ? 67 : 79}px`"
|
||||
>
|
||||
<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
|
||||
<XPostForm
|
||||
v-if="defaultStore.reactiveState.showFixedPostForm.value"
|
||||
|
@ -270,12 +274,12 @@ definePageMetadata(
|
|||
src.value === "local"
|
||||
? "ph-users ph-lg"
|
||||
: src.value === "social"
|
||||
? "ph-handshake ph-lg"
|
||||
: src.value === "recommended"
|
||||
? "ph-thumbs-up ph-lg"
|
||||
: src.value === "global"
|
||||
? "ph-planet ph-lg"
|
||||
: "ph-house ph-lg",
|
||||
? "ph-handshake ph-lg"
|
||||
: src.value === "recommended"
|
||||
? "ph-thumbs-up ph-lg"
|
||||
: src.value === "global"
|
||||
? "ph-planet ph-lg"
|
||||
: "ph-house ph-lg",
|
||||
})),
|
||||
);
|
||||
|
||||
|
@ -300,6 +304,12 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xytnxiau {
|
||||
overflow-y: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.cmuxhskf {
|
||||
--swiper-theme-color: var(--accent);
|
||||
> .tl {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { $i } from "@/reactiveAccount";
|
|||
import type { Ref } from "vue";
|
||||
import { onUnmounted, ref, watch } from "vue";
|
||||
import { api } from "./os";
|
||||
import { stream } from "./stream";
|
||||
import { useStream } from "./stream";
|
||||
|
||||
type StateDef = Record<
|
||||
string,
|
||||
|
@ -16,6 +16,7 @@ type StateDef = Record<
|
|||
|
||||
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
|
||||
|
||||
const stream = useStream();
|
||||
const connection = $i && stream.useChannel("main");
|
||||
|
||||
export class Storage<T extends StateDef> {
|
||||
|
|
|
@ -3,10 +3,12 @@ import * as os from "@/os";
|
|||
import icon from "@/scripts/icon";
|
||||
import { uploadFile } from "@/scripts/upload";
|
||||
import { defaultStore } from "@/store";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import type { DriveFile } from "firefish-js/built/entities";
|
||||
import { ref } from "vue";
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
function select(
|
||||
src: any,
|
||||
label: string | null,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as os from "@/os";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import type * as firefish from "firefish-js";
|
||||
import type { Ref } from "vue";
|
||||
import { onUnmounted } from "vue";
|
||||
|
@ -11,6 +11,7 @@ export function useNoteCapture(props: {
|
|||
isDeletedRef: Ref<boolean>;
|
||||
}) {
|
||||
const note = props.note;
|
||||
const stream = useStream();
|
||||
const connection = $i ? stream : null;
|
||||
|
||||
async function onStreamNoteUpdated(noteData): Promise<void> {
|
||||
|
|
|
@ -367,7 +367,7 @@ export const defaultStore = markRaw(
|
|||
},
|
||||
replaceWidgetsButtonWithReloadButton: {
|
||||
where: "device",
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
addRe: {
|
||||
where: "account",
|
||||
|
@ -414,6 +414,10 @@ export const defaultStore = markRaw(
|
|||
| "ph" // this is ph-regular
|
||||
| "ph-fill",
|
||||
},
|
||||
enableTimelineStreaming: {
|
||||
where: "deviceAccount",
|
||||
default: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -3,22 +3,44 @@ import { $i } from "@/reactiveAccount";
|
|||
import * as firefish from "firefish-js";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export const stream = markRaw(
|
||||
new firefish.Stream(
|
||||
url,
|
||||
$i
|
||||
? {
|
||||
token: $i.token,
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
let stream: firefish.Stream | null = null;
|
||||
let timeoutHeartBeat: number | null = null;
|
||||
export let isReloading: boolean = false;
|
||||
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
export function useStream() {
|
||||
if (stream != null) return stream;
|
||||
|
||||
stream = markRaw(
|
||||
new firefish.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 {
|
||||
if (stream != null && document.visibilityState === "visible") {
|
||||
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 * as sound from "@/scripts/sound";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const XStreamIndicator = defineAsyncComponent(
|
||||
() => import("./stream-indicator.vue"),
|
||||
|
|
|
@ -19,13 +19,14 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream, isReloading } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
|
||||
const hasDisconnected = ref(false);
|
||||
|
||||
function onDisconnected() {
|
||||
if (isReloading) return;
|
||||
hasDisconnected.value = true;
|
||||
}
|
||||
|
||||
|
@ -37,6 +38,7 @@ function reload() {
|
|||
location.reload();
|
||||
}
|
||||
|
||||
const stream = useStream();
|
||||
stream.on("_disconnected_", onDisconnected);
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -684,7 +684,7 @@ console.log(mainRouter.currentRoute.value.name);
|
|||
padding: var(--margin);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
overscroll-behavior: none;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ import { onUnmounted, reactive } from "vue";
|
|||
import type { Widget, WidgetComponentExpose } from "./widget";
|
||||
import { useWidgetPropsManager } from "./widget";
|
||||
import type { GetFormResultType } from "@/scripts/form";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import number from "@/filters/number";
|
||||
import * as sound from "@/scripts/sound";
|
||||
import { deepClone } from "@/scripts/clone";
|
||||
|
@ -161,6 +161,7 @@ const { widgetProps, configure } = useWidgetPropsManager(
|
|||
emit,
|
||||
);
|
||||
|
||||
const stream = useStream();
|
||||
const connection = stream.useChannel("queueStats");
|
||||
const current = reactive({
|
||||
inbox: {
|
||||
|
|
|
@ -30,7 +30,7 @@ import { onUnmounted, ref } from "vue";
|
|||
import type { Widget, WidgetComponentExpose } from "./widget";
|
||||
import { useWidgetPropsManager } from "./widget";
|
||||
import type { GetFormResultType } from "@/scripts/form";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
|
||||
import * as os from "@/os";
|
||||
import MkContainer from "@/components/MkContainer.vue";
|
||||
|
@ -66,6 +66,7 @@ const { widgetProps, configure } = useWidgetPropsManager(
|
|||
emit,
|
||||
);
|
||||
|
||||
const stream = useStream();
|
||||
const connection = stream.useChannel("main");
|
||||
const images = ref([]);
|
||||
const fetching = ref(true);
|
||||
|
|
|
@ -70,7 +70,7 @@ import XMeili from "./meilisearch.vue";
|
|||
import MkContainer from "@/components/MkContainer.vue";
|
||||
import type { GetFormResultType } from "@/scripts/form";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { useStream } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
import { instance } from "@/instance";
|
||||
import icon from "@/scripts/icon";
|
||||
|
@ -126,6 +126,7 @@ const toggleView = () => {
|
|||
save();
|
||||
};
|
||||
|
||||
const stream = useStream();
|
||||
const connection = stream.useChannel("serverStats");
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
|
|
|
@ -50,7 +50,7 @@ type StreamEvents = {
|
|||
* Firefish stream connection
|
||||
*/
|
||||
export default class Stream extends EventEmitter<StreamEvents> {
|
||||
private stream: ReconnectingWebsocket;
|
||||
public stream: ReconnectingWebsocket;
|
||||
public state: "initializing" | "reconnecting" | "connected" = "initializing";
|
||||
private sharedConnectionPools: Pool[] = [];
|
||||
private sharedConnections: SharedConnection[] = [];
|
||||
|
|
Loading…
Reference in a new issue