magnetar/fe_calckey/frontend/client/src/components/MkTimeline.vue

257 lines
6.9 KiB
Vue

<template>
<MkInfo
v-if="tlHint && !tlHintClosed"
:closeable="true"
class="_gap"
@close="closeHint"
>
<I18n :src="tlHint">
<template #icon></template>
</I18n>
</MkInfo>
<XNotes
ref="tlComponent"
:no-gap="!$store.state.showGapBetweenNotesInTimeline"
:pagination="pagination"
@queue="emit('queue', $event)"
/>
</template>
<script lang="ts" setup>
import { onUnmounted, ref } from "vue";
import XNotes from "@/components/MkNotes.vue";
import MkInfo from "@/components/MkInfo.vue";
import * as os from "@/os";
import { stream } from "@/stream";
import * as sound from "@/scripts/sound";
import { $i } from "@/account";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import { endpoints, packed } from "magnetar-common";
import { debounce } from "throttle-debounce";
import * as misskey from "calckey-js";
const props = defineProps<{
src: string;
list?: string;
antenna?: string;
sound?: boolean;
}>();
const emit = defineEmits<{
(ev: "note"): void;
(ev: "queue", count: number): void;
}>();
const displayLimit = 30;
const tlComponent = ref<InstanceType<typeof XNotes>>();
let debounceBuffer: Array<string> = [];
const prependMany = async () => {
let items = debounceBuffer;
debounceBuffer = [];
if (!tlComponent.value?.pagingComponent?.isFresh()) {
items = debounceBuffer.slice(-displayLimit);
}
const notes = (
await Promise.allSettled(
items.map((note) =>
os.magApi(
endpoints.GetNoteById,
{ context: true, attachments: true },
{
id: note,
}
)
)
)
)
.filter((p) => p.status === "fulfilled")
.map(
(p) => (p as PromiseFulfilledResult<packed.PackNoteMaybeFull>).value
);
for (const n of notes) {
tlComponent.value?.pagingComponent?.prepend(n);
emit("note");
}
if (props.sound) {
if (notes.some((nn) => nn.user.id !== $i?.id)) sound.play("note");
if (notes.some((nn) => nn.user.id === $i?.id)) sound.play("noteMy");
}
};
const debouncePrepend = debounce(40, prependMany);
const prepend = (note: string) => {
debounceBuffer.push(note);
debouncePrepend();
};
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: keyof misskey.Endpoints;
let query;
let connection;
let connection2;
let tlHint;
let 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") {
endpoint = "notes/local-timeline";
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") {
endpoint = "notes/recommended-timeline";
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") {
endpoint = "notes/hybrid-timeline";
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") {
endpoint = "notes/global-timeline";
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);
}
function closeHint() {
switch (props.src) {
case "home":
defaultStore.set("tlHomeHintClosed", true);
break;
case "local":
defaultStore.set("tlLocalHintClosed", true);
break;
case "recommended":
defaultStore.set("tlRecommendedHintClosed", true);
break;
case "social":
defaultStore.set("tlSocialHintClosed", true);
break;
case "global":
defaultStore.set("tlGlobalHintClosed", true);
break;
}
}
const pagination = {
endpoint: endpoint,
limit: 10,
params: query,
};
onUnmounted(() => {
connection.dispose();
if (connection2) connection2.dispose();
});
/* TODO
const timetravel = (date?: Date) => {
this.date = date;
this.$refs.tl.reload();
};
*/
</script>