mirror of
https://iceshrimp.dev/limepotato/jormungandr-patches.git
synced 2024-11-22 02:57:25 -07:00
1689 lines
37 KiB
Diff
1689 lines
37 KiB
Diff
|
From 7993396dbc19ccac8e2502b7c27114b481e26dfa Mon Sep 17 00:00:00 2001
|
||
|
From: limepotato <limepot@protonmail.ch>
|
||
|
Date: Fri, 5 Jul 2024 22:57:38 -0600
|
||
|
Subject: [PATCH] Remove messaging!
|
||
|
|
||
|
---
|
||
|
packages/client/src/pages/messaging/index.vue | 312 ------------
|
||
|
.../pages/messaging/messaging-room.form.vue | 415 ----------------
|
||
|
.../messaging/messaging-room.message.vue | 377 --------------
|
||
|
.../src/pages/messaging/messaging-room.vue | 464 ------------------
|
||
|
packages/client/src/ui/universal.vue | 21 -
|
||
|
packages/client/src/router.ts | 16 ----------------
|
||
|
packages/client/src/navbar.ts | 7 -------
|
||
|
7 files changed, 1612 deletions(-)
|
||
|
delete mode 100644 packages/client/src/pages/messaging/index.vue
|
||
|
delete mode 100644 packages/client/src/pages/messaging/messaging-room.form.vue
|
||
|
delete mode 100644 packages/client/src/pages/messaging/messaging-room.message.vue
|
||
|
delete mode 100644 packages/client/src/pages/messaging/messaging-room.vue
|
||
|
|
||
|
diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue
|
||
|
deleted file mode 100644
|
||
|
index 17bdd3786..000000000
|
||
|
--- a/packages/client/src/pages/messaging/index.vue
|
||
|
+++ /dev/null
|
||
|
@@ -1,312 +0,0 @@
|
||
|
-<template>
|
||
|
- <MkStickyContainer>
|
||
|
- <template #header
|
||
|
- ><MkPageHeader
|
||
|
- v-model:tab="tab"
|
||
|
- :actions="headerActions"
|
||
|
- :tabs="headerTabs"
|
||
|
- /></template>
|
||
|
- <div>
|
||
|
- <MkSpacer :content-max="800">
|
||
|
- <swiper
|
||
|
- :round-lengths="true"
|
||
|
- :touch-angle="25"
|
||
|
- :threshold="10"
|
||
|
- :centeredSlides="true"
|
||
|
- :modules="[Virtual]"
|
||
|
- :space-between="20"
|
||
|
- :virtual="true"
|
||
|
- :allow-touch-move="
|
||
|
- defaultStore.state.swipeOnMobile &&
|
||
|
- (deviceKind !== 'desktop' ||
|
||
|
- defaultStore.state.swipeOnDesktop)
|
||
|
- "
|
||
|
- @swiper="setSwiperRef"
|
||
|
- @slide-change="onSlideChange"
|
||
|
- >
|
||
|
- <swiper-slide>
|
||
|
- <div class="_content yweeujhr dms">
|
||
|
- <MkButton
|
||
|
- primary
|
||
|
- class="start"
|
||
|
- v-if="!isMobile"
|
||
|
- @click="startUser"
|
||
|
- ><i class="ph-plus ph-bold ph-lg"></i>
|
||
|
- {{ i18n.ts.startMessaging }}</MkButton
|
||
|
- >
|
||
|
- <MkPagination
|
||
|
- v-slot="{ items }"
|
||
|
- :pagination="dmsPagination"
|
||
|
- >
|
||
|
- <MkChatPreview
|
||
|
- v-for="message in items"
|
||
|
- :key="message.id"
|
||
|
- class="yweeujhr message _block"
|
||
|
- :message="message"
|
||
|
- />
|
||
|
- </MkPagination>
|
||
|
- </div>
|
||
|
- </swiper-slide>
|
||
|
- <swiper-slide>
|
||
|
- <div class="_content yweeujhr groups">
|
||
|
- <div v-if="!isMobile" class="groupsbuttons">
|
||
|
- <MkButton
|
||
|
- primary
|
||
|
- class="start"
|
||
|
- :link="true"
|
||
|
- to="/my/groups"
|
||
|
- ><i
|
||
|
- class="ph-user-circle-gear ph-bold ph-lg"
|
||
|
- ></i>
|
||
|
- {{ i18n.ts.manageGroups }}</MkButton
|
||
|
- >
|
||
|
- <MkButton
|
||
|
- primary
|
||
|
- class="start"
|
||
|
- @click="startGroup"
|
||
|
- ><i class="ph-plus ph-bold ph-lg"></i>
|
||
|
- {{ i18n.ts.startMessaging }}</MkButton
|
||
|
- >
|
||
|
- </div>
|
||
|
- <MkPagination
|
||
|
- v-slot="{ items }"
|
||
|
- :pagination="groupsPagination"
|
||
|
- >
|
||
|
- <MkChatPreview
|
||
|
- v-for="message in items"
|
||
|
- :key="message.id"
|
||
|
- class="yweeujhr message _block"
|
||
|
- :message="message"
|
||
|
- />
|
||
|
- </MkPagination>
|
||
|
- </div>
|
||
|
- </swiper-slide>
|
||
|
- </swiper>
|
||
|
- </MkSpacer>
|
||
|
- </div>
|
||
|
- </MkStickyContainer>
|
||
|
-</template>
|
||
|
-
|
||
|
-<script lang="ts" setup>
|
||
|
-import { ref, markRaw, onMounted, onUnmounted, watch } from "vue";
|
||
|
-import * as Acct from "iceshrimp-js/built/acct";
|
||
|
-import { Virtual } from "swiper/modules";
|
||
|
-import { Swiper, SwiperSlide } from "swiper/vue";
|
||
|
-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 { useRouter } from "@/router";
|
||
|
-import { i18n } from "@/i18n";
|
||
|
-import { definePageMetadata } from "@/scripts/page-metadata";
|
||
|
-import { $i } from "@/account";
|
||
|
-import { deviceKind } from "@/scripts/device-kind";
|
||
|
-import { defaultStore } from "@/store";
|
||
|
-import "swiper/scss";
|
||
|
-import "swiper/scss/virtual";
|
||
|
-
|
||
|
-const router = useRouter();
|
||
|
-
|
||
|
-let messages = $ref([]);
|
||
|
-let connection = $ref(null);
|
||
|
-
|
||
|
-const tabs = ["dms", "groups"];
|
||
|
-let tab = $ref(tabs[0]);
|
||
|
-watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
|
||
|
-
|
||
|
-const MOBILE_THRESHOLD = 500;
|
||
|
-const isMobile = ref(
|
||
|
- deviceKind === "smartphone" || window.innerWidth <= MOBILE_THRESHOLD,
|
||
|
-);
|
||
|
-window.addEventListener("resize", () => {
|
||
|
- isMobile.value =
|
||
|
- deviceKind === "smartphone" || window.innerWidth <= MOBILE_THRESHOLD;
|
||
|
-});
|
||
|
-
|
||
|
-async function readAllMessagingMessages() {
|
||
|
- await os.apiWithDialog("i/read-all-messaging-messages");
|
||
|
-}
|
||
|
-
|
||
|
-const headerActions = $computed(() => [
|
||
|
- {
|
||
|
- icon: "ph-checks ph-bold ph-lg",
|
||
|
- text: i18n.ts.markAllAsRead,
|
||
|
- handler: readAllMessagingMessages,
|
||
|
- },
|
||
|
-]);
|
||
|
-
|
||
|
-const headerTabs = $computed(() => [
|
||
|
- {
|
||
|
- key: "dms",
|
||
|
- title: i18n.ts._messaging.dms,
|
||
|
- icon: "ph-user ph-bold ph-lg",
|
||
|
- },
|
||
|
- {
|
||
|
- key: "groups",
|
||
|
- title: i18n.ts._messaging.groups,
|
||
|
- icon: "ph-users-three ph-bold ph-lg",
|
||
|
- },
|
||
|
-]);
|
||
|
-
|
||
|
-definePageMetadata({
|
||
|
- title: i18n.ts.messaging,
|
||
|
- icon: "ph-chats-teardrop ph-bold ph-lg",
|
||
|
-});
|
||
|
-
|
||
|
-const dmsPagination = {
|
||
|
- endpoint: "messaging/history" as const,
|
||
|
- limit: 15,
|
||
|
- params: {
|
||
|
- group: false,
|
||
|
- },
|
||
|
-};
|
||
|
-const groupsPagination = {
|
||
|
- endpoint: "messaging/history" as const,
|
||
|
- limit: 5,
|
||
|
- params: {
|
||
|
- group: true,
|
||
|
- },
|
||
|
-};
|
||
|
-
|
||
|
-function onMessage(message): void {
|
||
|
- if (message.recipientId) {
|
||
|
- messages = messages.filter(
|
||
|
- (m) =>
|
||
|
- !(
|
||
|
- (m.recipientId === message.recipientId &&
|
||
|
- m.userId === message.userId) ||
|
||
|
- (m.recipientId === message.userId &&
|
||
|
- m.userId === message.recipientId)
|
||
|
- ),
|
||
|
- );
|
||
|
-
|
||
|
- messages.unshift(message);
|
||
|
- } else if (message.groupId) {
|
||
|
- messages = messages.filter((m) => m.groupId !== message.groupId);
|
||
|
- messages.unshift(message);
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onRead(ids): void {
|
||
|
- for (const id of ids) {
|
||
|
- const found = messages.find((m) => m.id === id);
|
||
|
- if (found) {
|
||
|
- if (found.recipientId) {
|
||
|
- found.isRead = true;
|
||
|
- } else if (found.groupId) {
|
||
|
- found.reads.push($i.id);
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function startMenu(ev) {
|
||
|
- os.popupMenu(
|
||
|
- [
|
||
|
- {
|
||
|
- text: i18n.ts.messagingWithUser,
|
||
|
- icon: "ph-user ph-bold ph-lg",
|
||
|
- action: () => {
|
||
|
- startUser();
|
||
|
- },
|
||
|
- },
|
||
|
- {
|
||
|
- text: i18n.ts.messagingWithGroup,
|
||
|
- icon: "ph-users-three ph-bold ph-lg",
|
||
|
- action: () => {
|
||
|
- startGroup();
|
||
|
- },
|
||
|
- },
|
||
|
- ],
|
||
|
- ev.currentTarget ?? ev.target,
|
||
|
- );
|
||
|
-}
|
||
|
-
|
||
|
-async function startUser(): void {
|
||
|
- os.selectUser().then((user) => {
|
||
|
- router.push(`/my/messaging/${Acct.toString(user)}`);
|
||
|
- });
|
||
|
-}
|
||
|
-
|
||
|
-async function startGroup(): void {
|
||
|
- const groups1 = await os.api("users/groups/owned");
|
||
|
- const groups2 = await os.api("users/groups/joined");
|
||
|
- if (groups1.length === 0 && groups2.length === 0) {
|
||
|
- os.alert({
|
||
|
- type: "warning",
|
||
|
- title: i18n.ts.youHaveNoGroups,
|
||
|
- text: i18n.ts.joinOrCreateGroup,
|
||
|
- });
|
||
|
- return;
|
||
|
- }
|
||
|
- const { canceled, result: group } = await os.select({
|
||
|
- title: i18n.ts.group,
|
||
|
- items: groups1.concat(groups2).map((group) => ({
|
||
|
- value: group,
|
||
|
- text: group.name,
|
||
|
- })),
|
||
|
- });
|
||
|
- if (canceled) return;
|
||
|
- router.push(`/my/messaging/group/${group.id}`);
|
||
|
-}
|
||
|
-
|
||
|
-let swiperRef = null;
|
||
|
-
|
||
|
-function setSwiperRef(swiper) {
|
||
|
- swiperRef = swiper;
|
||
|
- syncSlide(tabs.indexOf(tab));
|
||
|
-}
|
||
|
-
|
||
|
-function onSlideChange() {
|
||
|
- tab = tabs[swiperRef.activeIndex];
|
||
|
-}
|
||
|
-
|
||
|
-function syncSlide(index) {
|
||
|
- swiperRef.slideTo(index);
|
||
|
-}
|
||
|
-
|
||
|
-onMounted(() => {
|
||
|
- syncSlide(tabs.indexOf(swiperRef.activeIndex));
|
||
|
-
|
||
|
- connection = markRaw(stream.useChannel("messagingIndex"));
|
||
|
-
|
||
|
- connection.on("message", onMessage);
|
||
|
- connection.on("read", onRead);
|
||
|
-
|
||
|
- os.api("messaging/history", { group: false, limit: 5 }).then(
|
||
|
- (userMessages) => {
|
||
|
- os.api("messaging/history", { group: true, limit: 5 }).then(
|
||
|
- (groupMessages) => {
|
||
|
- const _messages = userMessages.concat(groupMessages);
|
||
|
- _messages.sort(
|
||
|
- (a, b) =>
|
||
|
- new Date(b.createdAt).getTime() -
|
||
|
- new Date(a.createdAt).getTime(),
|
||
|
- );
|
||
|
- messages = _messages;
|
||
|
- },
|
||
|
- );
|
||
|
- },
|
||
|
- );
|
||
|
-});
|
||
|
-
|
||
|
-onUnmounted(() => {
|
||
|
- if (connection) connection.dispose();
|
||
|
-});
|
||
|
-</script>
|
||
|
-
|
||
|
-<style lang="scss" scoped>
|
||
|
-.yweeujhr {
|
||
|
- > .start {
|
||
|
- margin: 0 auto var(--margin) auto;
|
||
|
- }
|
||
|
-
|
||
|
- > .groupsbuttons {
|
||
|
- max-width: 100%;
|
||
|
- display: flex;
|
||
|
- justify-content: center;
|
||
|
- margin-bottom: 1rem;
|
||
|
- }
|
||
|
-}
|
||
|
-</style>
|
||
|
diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue
|
||
|
deleted file mode 100644
|
||
|
index 1ea9bb869..000000000
|
||
|
--- a/packages/client/src/pages/messaging/messaging-room.form.vue
|
||
|
+++ /dev/null
|
||
|
@@ -1,415 +0,0 @@
|
||
|
-<template>
|
||
|
- <div
|
||
|
- class="pemppnzi _block"
|
||
|
- @dragover.stop="onDragover"
|
||
|
- @drop.stop="onDrop"
|
||
|
- >
|
||
|
- <textarea
|
||
|
- ref="textEl"
|
||
|
- v-model="text"
|
||
|
- :placeholder="i18n.ts.inputMessageHere"
|
||
|
- @keydown="onKeydown"
|
||
|
- @compositionupdate="onCompositionUpdate"
|
||
|
- @paste="onPaste"
|
||
|
- ></textarea>
|
||
|
- <footer>
|
||
|
- <div v-if="file" class="file" @click="file = null">
|
||
|
- {{ file.name }}
|
||
|
- </div>
|
||
|
- <div class="buttons">
|
||
|
- <button
|
||
|
- class="_button"
|
||
|
- @click="chooseFile"
|
||
|
- :aria-label="i18n.t('attachFile')"
|
||
|
- >
|
||
|
- <i class="ph-upload ph-bold ph-lg"></i>
|
||
|
- </button>
|
||
|
- <button
|
||
|
- class="_button"
|
||
|
- @click="insertEmoji"
|
||
|
- :aria-label="i18n.t('chooseEmoji')"
|
||
|
- >
|
||
|
- <i class="ph-smiley ph-bold ph-lg"></i>
|
||
|
- </button>
|
||
|
- <button
|
||
|
- class="send _button"
|
||
|
- :disabled="!canSend || sending"
|
||
|
- :title="i18n.ts.send"
|
||
|
- :aria-label="i18n.ts.send"
|
||
|
- @click="send"
|
||
|
- >
|
||
|
- <template v-if="!sending"
|
||
|
- ><i
|
||
|
- class="ph-paper-plane-tilt ph-bold ph-lg"
|
||
|
- ></i></template
|
||
|
- ><template v-if="sending"
|
||
|
- ><i
|
||
|
- class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"
|
||
|
- ></i
|
||
|
- ></template>
|
||
|
- </button>
|
||
|
- </div>
|
||
|
- </footer>
|
||
|
- <input ref="fileEl" type="file" @change="onChangeFile" />
|
||
|
- </div>
|
||
|
-</template>
|
||
|
-
|
||
|
-<script lang="ts" setup>
|
||
|
-import { onMounted, watch } from "vue";
|
||
|
-import * as Misskey from "iceshrimp-js";
|
||
|
-import autosize from "autosize";
|
||
|
-//import insertTextAtCursor from 'insert-text-at-cursor';
|
||
|
-import { throttle } from "throttle-debounce";
|
||
|
-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 { defaultStore } from "@/store";
|
||
|
-import { i18n } from "@/i18n";
|
||
|
-import { uploadFile } from "@/scripts/upload";
|
||
|
-
|
||
|
-const props = defineProps<{
|
||
|
- user?: Misskey.entities.UserDetailed | null;
|
||
|
- group?: Misskey.entities.UserGroup | null;
|
||
|
-}>();
|
||
|
-
|
||
|
-let textEl = $ref<HTMLTextAreaElement>();
|
||
|
-let fileEl = $ref<HTMLInputElement>();
|
||
|
-
|
||
|
-let text = $ref<string>("");
|
||
|
-let file = $ref<Misskey.entities.DriveFile | null>(null);
|
||
|
-let sending = $ref(false);
|
||
|
-const typing = throttle(3000, () => {
|
||
|
- stream.send(
|
||
|
- "typingOnMessaging",
|
||
|
- props.user ? { partner: props.user.id } : { group: props.group?.id },
|
||
|
- );
|
||
|
-});
|
||
|
-
|
||
|
-let draftKey = $computed(() =>
|
||
|
- props.user ? "user:" + props.user.id : "group:" + props.group?.id,
|
||
|
-);
|
||
|
-let canSend = $computed(
|
||
|
- () => (text != null && text.trim() !== "") || file != null,
|
||
|
-);
|
||
|
-
|
||
|
-watch([$$(text), $$(file)], saveDraft);
|
||
|
-
|
||
|
-async function onPaste(ev: ClipboardEvent) {
|
||
|
- if (!ev.clipboardData) return;
|
||
|
-
|
||
|
- const clipboardData = ev.clipboardData;
|
||
|
- const items = clipboardData.items;
|
||
|
-
|
||
|
- if (items.length === 1) {
|
||
|
- if (items[0].kind === "file") {
|
||
|
- const pastedFile = items[0].getAsFile();
|
||
|
- if (!pastedFile) return;
|
||
|
- const lio = pastedFile.name.lastIndexOf(".");
|
||
|
- const ext = lio >= 0 ? pastedFile.name.slice(lio) : "";
|
||
|
- const formatted =
|
||
|
- formatTimeString(
|
||
|
- new Date(pastedFile.lastModified),
|
||
|
- defaultStore.state.pastedFileName,
|
||
|
- ).replace(/{{number}}/g, "1") + ext;
|
||
|
- if (formatted) upload(pastedFile, formatted);
|
||
|
- }
|
||
|
- } else {
|
||
|
- if (items[0].kind === "file") {
|
||
|
- os.alert({
|
||
|
- type: "error",
|
||
|
- text: i18n.ts.onlyOneFileCanBeAttached,
|
||
|
- });
|
||
|
- }
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onDragover(ev: DragEvent) {
|
||
|
- if (!ev.dataTransfer) return;
|
||
|
-
|
||
|
- const isFile = ev.dataTransfer.items[0].kind === "file";
|
||
|
- const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||
|
- if (isFile || isDriveFile) {
|
||
|
- ev.preventDefault();
|
||
|
- ev.dataTransfer.dropEffect =
|
||
|
- ev.dataTransfer.effectAllowed === "all" ? "copy" : "move";
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onDrop(ev: DragEvent): void {
|
||
|
- if (!ev.dataTransfer) return;
|
||
|
-
|
||
|
- // ファイルだったら
|
||
|
- if (ev.dataTransfer.files.length === 1) {
|
||
|
- ev.preventDefault();
|
||
|
- upload(ev.dataTransfer.files[0]);
|
||
|
- return;
|
||
|
- } else if (ev.dataTransfer.files.length > 1) {
|
||
|
- ev.preventDefault();
|
||
|
- os.alert({
|
||
|
- type: "error",
|
||
|
- text: i18n.ts.onlyOneFileCanBeAttached,
|
||
|
- });
|
||
|
- return;
|
||
|
- }
|
||
|
-
|
||
|
- //#region ドライブのファイル
|
||
|
- const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||
|
- if (driveFile != null && driveFile !== "") {
|
||
|
- file = JSON.parse(driveFile);
|
||
|
- ev.preventDefault();
|
||
|
- }
|
||
|
- //#endregion
|
||
|
-}
|
||
|
-
|
||
|
-function onKeydown(ev: KeyboardEvent) {
|
||
|
- typing();
|
||
|
- let sendOnEnter =
|
||
|
- localStorage.getItem("enterSendsMessage") === "true" ||
|
||
|
- defaultStore.state.enterSendsMessage;
|
||
|
- if (sendOnEnter) {
|
||
|
- if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey)) {
|
||
|
- textEl.value += "\n";
|
||
|
- } else if (
|
||
|
- ev.key === "Enter" &&
|
||
|
- !ev.shiftKey &&
|
||
|
- !("ontouchstart" in document.documentElement) &&
|
||
|
- canSend
|
||
|
- ) {
|
||
|
- ev.preventDefault();
|
||
|
- send();
|
||
|
- }
|
||
|
- } else {
|
||
|
- if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey) && canSend) {
|
||
|
- ev.preventDefault();
|
||
|
- send();
|
||
|
- }
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onCompositionUpdate() {
|
||
|
- typing();
|
||
|
-}
|
||
|
-
|
||
|
-function chooseFile(ev: MouseEvent) {
|
||
|
- selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(
|
||
|
- (selectedFile) => {
|
||
|
- file = selectedFile;
|
||
|
- },
|
||
|
- );
|
||
|
-}
|
||
|
-
|
||
|
-function onChangeFile() {
|
||
|
- if (fileEl.files![0]) upload(fileEl.files[0]);
|
||
|
-}
|
||
|
-
|
||
|
-function upload(fileToUpload: File, name?: string) {
|
||
|
- uploadFile(fileToUpload, defaultStore.state.uploadFolder, name).then(
|
||
|
- (res) => {
|
||
|
- file = res;
|
||
|
- },
|
||
|
- );
|
||
|
-}
|
||
|
-
|
||
|
-function send() {
|
||
|
- sending = true;
|
||
|
- os.api("messaging/messages/create", {
|
||
|
- userId: props.user ? props.user.id : undefined,
|
||
|
- groupId: props.group ? props.group.id : undefined,
|
||
|
- text: text ? text : undefined,
|
||
|
- fileId: file ? file.id : undefined,
|
||
|
- })
|
||
|
- .then((message) => {
|
||
|
- clear();
|
||
|
- })
|
||
|
- .catch((err) => {
|
||
|
- console.error(err);
|
||
|
- })
|
||
|
- .then(() => {
|
||
|
- sending = false;
|
||
|
- });
|
||
|
-}
|
||
|
-
|
||
|
-function clear() {
|
||
|
- text = "";
|
||
|
- file = null;
|
||
|
- deleteDraft();
|
||
|
-}
|
||
|
-
|
||
|
-function saveDraft() {
|
||
|
- const drafts = JSON.parse(localStorage.getItem("message_drafts") || "{}");
|
||
|
-
|
||
|
- drafts[draftKey] = {
|
||
|
- updatedAt: new Date(),
|
||
|
- data: {
|
||
|
- text: text,
|
||
|
- file: file,
|
||
|
- },
|
||
|
- };
|
||
|
-
|
||
|
- localStorage.setItem("message_drafts", JSON.stringify(drafts));
|
||
|
-}
|
||
|
-
|
||
|
-function deleteDraft() {
|
||
|
- const drafts = JSON.parse(localStorage.getItem("message_drafts") || "{}");
|
||
|
-
|
||
|
- delete drafts[draftKey];
|
||
|
-
|
||
|
- localStorage.setItem("message_drafts", JSON.stringify(drafts));
|
||
|
-}
|
||
|
-
|
||
|
-async function insertEmoji(ev: MouseEvent) {
|
||
|
- os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textEl);
|
||
|
-}
|
||
|
-
|
||
|
-onMounted(() => {
|
||
|
- autosize(textEl);
|
||
|
-
|
||
|
- // TODO: detach when unmount
|
||
|
- new Autocomplete(textEl, $$(text));
|
||
|
-
|
||
|
- // 書きかけの投稿を復元
|
||
|
- const draft = JSON.parse(localStorage.getItem("message_drafts") || "{}")[
|
||
|
- draftKey
|
||
|
- ];
|
||
|
- if (draft) {
|
||
|
- text = draft.data.text;
|
||
|
- file = draft.data.file;
|
||
|
- }
|
||
|
-});
|
||
|
-
|
||
|
-defineExpose({
|
||
|
- file,
|
||
|
- upload,
|
||
|
-});
|
||
|
-</script>
|
||
|
-
|
||
|
-<style lang="scss" scoped>
|
||
|
-.pemppnzi {
|
||
|
- position: relative;
|
||
|
- margin-top: 1rem;
|
||
|
-
|
||
|
- > textarea {
|
||
|
- cursor: auto;
|
||
|
- display: block;
|
||
|
- width: 100%;
|
||
|
- min-width: 100%;
|
||
|
- max-width: 100%;
|
||
|
- min-height: 80px;
|
||
|
- margin: 0;
|
||
|
- padding: 16px 16px 0 16px;
|
||
|
- resize: none;
|
||
|
- font-size: 1em;
|
||
|
- font-family: inherit;
|
||
|
- outline: none;
|
||
|
- border: none;
|
||
|
- border-radius: 0;
|
||
|
- box-shadow: none;
|
||
|
- background: transparent;
|
||
|
- box-sizing: border-box;
|
||
|
- color: var(--fg);
|
||
|
- }
|
||
|
-
|
||
|
- footer {
|
||
|
- position: sticky;
|
||
|
- bottom: 0;
|
||
|
- background: var(--panel);
|
||
|
-
|
||
|
- > .file {
|
||
|
- padding: 8px;
|
||
|
- color: var(--fg);
|
||
|
- background: transparent;
|
||
|
- cursor: pointer;
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- .files {
|
||
|
- display: block;
|
||
|
- margin: 0;
|
||
|
- padding: 0 8px;
|
||
|
- list-style: none;
|
||
|
-
|
||
|
- &:after {
|
||
|
- content: "";
|
||
|
- display: block;
|
||
|
- clear: both;
|
||
|
- }
|
||
|
-
|
||
|
- > li {
|
||
|
- display: block;
|
||
|
- float: left;
|
||
|
- margin: 4px;
|
||
|
- padding: 0;
|
||
|
- width: 64px;
|
||
|
- height: 64px;
|
||
|
- background-color: #eee;
|
||
|
- background-repeat: no-repeat;
|
||
|
- background-position: center center;
|
||
|
- background-size: cover;
|
||
|
- cursor: move;
|
||
|
-
|
||
|
- &:hover {
|
||
|
- > .remove {
|
||
|
- display: block;
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > .remove {
|
||
|
- display: none;
|
||
|
- position: absolute;
|
||
|
- right: -6px;
|
||
|
- top: -6px;
|
||
|
- margin: 0;
|
||
|
- padding: 0;
|
||
|
- background: transparent;
|
||
|
- outline: none;
|
||
|
- border: none;
|
||
|
- border-radius: 0;
|
||
|
- box-shadow: none;
|
||
|
- cursor: pointer;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- .buttons {
|
||
|
- display: flex;
|
||
|
-
|
||
|
- ._button {
|
||
|
- margin: 0;
|
||
|
- padding: 16px;
|
||
|
- font-size: 1em;
|
||
|
- font-weight: normal;
|
||
|
- text-decoration: none;
|
||
|
- transition: color 0.1s ease;
|
||
|
-
|
||
|
- &:hover {
|
||
|
- color: var(--accent);
|
||
|
- }
|
||
|
-
|
||
|
- &:active {
|
||
|
- color: var(--accentDarken);
|
||
|
- transition: color 0s ease;
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > .send {
|
||
|
- margin-left: auto;
|
||
|
- color: var(--accent);
|
||
|
-
|
||
|
- &:hover {
|
||
|
- color: var(--accentLighten);
|
||
|
- }
|
||
|
-
|
||
|
- &:active {
|
||
|
- color: var(--accentDarken);
|
||
|
- transition: color 0s ease;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- input[type="file"] {
|
||
|
- display: none;
|
||
|
- }
|
||
|
-}
|
||
|
-</style>
|
||
|
diff --git a/packages/client/src/pages/messaging/messaging-room.message.vue b/packages/client/src/pages/messaging/messaging-room.message.vue
|
||
|
deleted file mode 100644
|
||
|
index f3fb921e3..000000000
|
||
|
--- a/packages/client/src/pages/messaging/messaging-room.message.vue
|
||
|
+++ /dev/null
|
||
|
@@ -1,377 +0,0 @@
|
||
|
-<template>
|
||
|
- <div v-size="{ max: [400, 500] }" class="thvuemwp" :class="{ isMe }">
|
||
|
- <MkAvatar
|
||
|
- v-if="!isMe"
|
||
|
- class="avatar"
|
||
|
- :user="message.user"
|
||
|
- :show-indicator="true"
|
||
|
- />
|
||
|
- <div class="content">
|
||
|
- <div class="balloon" :class="{ noText: message.text == null }">
|
||
|
- <button
|
||
|
- v-if="isMe"
|
||
|
- class="delete-button"
|
||
|
- :title="i18n.ts.delete"
|
||
|
- @click="del"
|
||
|
- >
|
||
|
- <i
|
||
|
- style="color: var(--accentLighten)"
|
||
|
- class="ph-x-circle ph-fill ph-lg"
|
||
|
- ></i>
|
||
|
- </button>
|
||
|
- <div v-if="!message.isDeleted" class="content">
|
||
|
- <Mfm
|
||
|
- v-if="message.text"
|
||
|
- ref="text"
|
||
|
- class="text"
|
||
|
- :text="message.text"
|
||
|
- :i="$i"
|
||
|
- />
|
||
|
- </div>
|
||
|
- <div v-else class="content">
|
||
|
- <p class="is-deleted">{{ i18n.ts.deleted }}</p>
|
||
|
- </div>
|
||
|
- </div>
|
||
|
- <div v-if="message.file" class="file" width="400px">
|
||
|
- <XMediaList
|
||
|
- v-if="
|
||
|
- message.file.type.split('/')[0] == 'image' ||
|
||
|
- message.file.type.split('/')[0] == 'video'
|
||
|
- "
|
||
|
- :in-dm="true"
|
||
|
- width="400px"
|
||
|
- :media-list="[message.file]"
|
||
|
- style="border-radius: 5px"
|
||
|
- />
|
||
|
- <a
|
||
|
- v-else
|
||
|
- :href="message.file.url"
|
||
|
- rel="noopener"
|
||
|
- target="_blank"
|
||
|
- :title="message.file.name"
|
||
|
- >
|
||
|
- <p>{{ message.file.name }}</p>
|
||
|
- </a>
|
||
|
- </div>
|
||
|
- <div></div>
|
||
|
- <MkUrlPreview
|
||
|
- v-for="url in urls"
|
||
|
- :key="url"
|
||
|
- :url="url"
|
||
|
- style="margin: 8px 0"
|
||
|
- />
|
||
|
- <footer>
|
||
|
- <template v-if="isGroup">
|
||
|
- <span v-if="message.reads.length > 0" class="read"
|
||
|
- >{{ i18n.ts.messageRead }}
|
||
|
- {{ message.reads.length }}</span
|
||
|
- >
|
||
|
- </template>
|
||
|
- <template v-else>
|
||
|
- <span v-if="isMe && message.isRead" class="read">{{
|
||
|
- i18n.ts.messageRead
|
||
|
- }}</span>
|
||
|
- </template>
|
||
|
- <MkTime :time="message.createdAt" />
|
||
|
- <template v-if="message.is_edited"
|
||
|
- ><i class="ph-pencil ph-bold ph-lg"></i
|
||
|
- ></template>
|
||
|
- </footer>
|
||
|
- </div>
|
||
|
- </div>
|
||
|
-</template>
|
||
|
-
|
||
|
-<script lang="ts" setup>
|
||
|
-import {} from "vue";
|
||
|
-import * as mfm from "mfm-js";
|
||
|
-import type * as Misskey from "iceshrimp-js";
|
||
|
-import XMediaList from "@/components/MkMediaList.vue";
|
||
|
-import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
|
||
|
-import MkUrlPreview from "@/components/MkUrlPreview.vue";
|
||
|
-import * as os from "@/os";
|
||
|
-import { $i } from "@/account";
|
||
|
-import { i18n } from "@/i18n";
|
||
|
-
|
||
|
-const props = defineProps<{
|
||
|
- message: Misskey.entities.MessagingMessage;
|
||
|
- isGroup?: boolean;
|
||
|
-}>();
|
||
|
-
|
||
|
-const isMe = $computed(() => props.message.userId === $i?.id);
|
||
|
-const urls = $computed(() =>
|
||
|
- props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : [],
|
||
|
-);
|
||
|
-
|
||
|
-function del(): void {
|
||
|
- os.api("messaging/messages/delete", {
|
||
|
- messageId: props.message.id,
|
||
|
- });
|
||
|
-}
|
||
|
-</script>
|
||
|
-
|
||
|
-<style lang="scss" scoped>
|
||
|
-.thvuemwp {
|
||
|
- $me-balloon-color: var(--accent);
|
||
|
- --plyr-color-main: var(--accent);
|
||
|
-
|
||
|
- position: relative;
|
||
|
- background-color: transparent;
|
||
|
- display: flex;
|
||
|
-
|
||
|
- > .avatar {
|
||
|
- position: sticky;
|
||
|
- top: calc(var(--stickyTop, 0px) + 20px);
|
||
|
- display: block;
|
||
|
- width: 45px;
|
||
|
- height: 45px;
|
||
|
- transition: all 0.1s ease;
|
||
|
- }
|
||
|
-
|
||
|
- > .content {
|
||
|
- min-width: 0;
|
||
|
-
|
||
|
- > .balloon {
|
||
|
- position: relative;
|
||
|
- display: inline-flex;
|
||
|
- align-items: center;
|
||
|
- padding: 0;
|
||
|
- min-height: 38px;
|
||
|
- border-radius: 16px;
|
||
|
- max-width: 100%;
|
||
|
-
|
||
|
- & + * {
|
||
|
- clear: both;
|
||
|
- }
|
||
|
-
|
||
|
- &:hover {
|
||
|
- > .delete-button {
|
||
|
- display: block;
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > .delete-button {
|
||
|
- display: none;
|
||
|
- position: absolute;
|
||
|
- z-index: 1;
|
||
|
- top: -4px;
|
||
|
- right: -4px;
|
||
|
- margin: 0;
|
||
|
- padding: 0;
|
||
|
- cursor: pointer;
|
||
|
- outline: none;
|
||
|
- border: none;
|
||
|
- border-radius: 0;
|
||
|
- box-shadow: none;
|
||
|
- background: transparent;
|
||
|
-
|
||
|
- > img {
|
||
|
- vertical-align: bottom;
|
||
|
- width: 16px;
|
||
|
- height: 16px;
|
||
|
- cursor: pointer;
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > .content {
|
||
|
- max-width: 100%;
|
||
|
-
|
||
|
- > .is-deleted {
|
||
|
- display: block;
|
||
|
- margin: 0;
|
||
|
- padding: 0;
|
||
|
- overflow: hidden;
|
||
|
- overflow-wrap: break-word;
|
||
|
- font-size: 1em;
|
||
|
- color: rgba(#000, 0.5);
|
||
|
- }
|
||
|
-
|
||
|
- > .text {
|
||
|
- display: block;
|
||
|
- margin: 0;
|
||
|
- padding: 12px 18px;
|
||
|
- overflow: hidden;
|
||
|
- overflow-wrap: break-word;
|
||
|
- word-break: break-word;
|
||
|
- font-size: 1em;
|
||
|
- color: rgba(#000, 0.8);
|
||
|
-
|
||
|
- & + .file {
|
||
|
- > a {
|
||
|
- border-radius: 0 0 16px 16px;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > .file {
|
||
|
- > a {
|
||
|
- display: block;
|
||
|
- max-width: 100%;
|
||
|
- border-radius: 16px;
|
||
|
- overflow: hidden;
|
||
|
- text-decoration: none;
|
||
|
-
|
||
|
- &:hover {
|
||
|
- text-decoration: none;
|
||
|
-
|
||
|
- > p {
|
||
|
- background: #ccc;
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > * {
|
||
|
- display: block;
|
||
|
- margin: 0;
|
||
|
- width: 100%;
|
||
|
- max-height: 512px;
|
||
|
- object-fit: contain;
|
||
|
- box-sizing: border-box;
|
||
|
- }
|
||
|
-
|
||
|
- > p {
|
||
|
- padding: 30px;
|
||
|
- text-align: center;
|
||
|
- color: #6e6a86;
|
||
|
- background: #ddd;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > footer {
|
||
|
- display: block;
|
||
|
- margin: 2px 0 0 0;
|
||
|
- font-size: 0.65em;
|
||
|
-
|
||
|
- > .read {
|
||
|
- margin: 0 8px;
|
||
|
- }
|
||
|
-
|
||
|
- > i {
|
||
|
- margin-left: 4px;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- &:not(.isMe) {
|
||
|
- padding-left: var(--margin);
|
||
|
-
|
||
|
- > .content {
|
||
|
- padding-left: 16px;
|
||
|
- padding-right: 32px;
|
||
|
-
|
||
|
- > .balloon {
|
||
|
- $color: var(--messagingIsNotMe);
|
||
|
- background: $color;
|
||
|
-
|
||
|
- &.noText {
|
||
|
- background: transparent;
|
||
|
- }
|
||
|
-
|
||
|
- &:not(.noText):before {
|
||
|
- left: -14px;
|
||
|
- border-top: solid 8px transparent;
|
||
|
- border-right: solid 8px $color;
|
||
|
- border-bottom: solid 8px transparent;
|
||
|
- border-left: solid 8px transparent;
|
||
|
- }
|
||
|
-
|
||
|
- > .content {
|
||
|
- > .text {
|
||
|
- color: var(--fg);
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > footer {
|
||
|
- text-align: left;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- &.isMe {
|
||
|
- flex-direction: row-reverse;
|
||
|
- padding-right: var(--margin);
|
||
|
- right: var(--margin); // 削除時にposition: absoluteになったときに使う
|
||
|
-
|
||
|
- > .content {
|
||
|
- padding-right: 16px;
|
||
|
- padding-left: 32px;
|
||
|
- text-align: right;
|
||
|
-
|
||
|
- > .balloon {
|
||
|
- background: $me-balloon-color;
|
||
|
- text-align: left;
|
||
|
-
|
||
|
- ::selection {
|
||
|
- color: var(--accent);
|
||
|
- background-color: #fff;
|
||
|
- }
|
||
|
-
|
||
|
- &.noText {
|
||
|
- background: transparent;
|
||
|
- }
|
||
|
-
|
||
|
- &:not(.noText):before {
|
||
|
- right: -14px;
|
||
|
- left: auto;
|
||
|
- border-top: solid 8px transparent;
|
||
|
- border-right: solid 8px transparent;
|
||
|
- border-bottom: solid 8px transparent;
|
||
|
- border-left: solid 8px $me-balloon-color;
|
||
|
- }
|
||
|
-
|
||
|
- > .content {
|
||
|
- > p.is-deleted {
|
||
|
- color: rgba(#fff, 0.5);
|
||
|
- }
|
||
|
-
|
||
|
- > .text {
|
||
|
- &,
|
||
|
- ::v-deep(*) {
|
||
|
- color: var(--fgOnAccent) !important;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > footer {
|
||
|
- text-align: right;
|
||
|
-
|
||
|
- > .read {
|
||
|
- user-select: none;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- &.max-width_400px {
|
||
|
- > .avatar {
|
||
|
- width: 48px;
|
||
|
- height: 48px;
|
||
|
- }
|
||
|
-
|
||
|
- > .content {
|
||
|
- > .balloon {
|
||
|
- > .content {
|
||
|
- > .text {
|
||
|
- font-size: 0.9em;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- &.max-width_500px {
|
||
|
- > .content {
|
||
|
- > .balloon {
|
||
|
- > .content {
|
||
|
- > .text {
|
||
|
- padding: 8px 16px;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-}
|
||
|
-</style>
|
||
|
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
|
||
|
deleted file mode 100644
|
||
|
index a360863f2..000000000
|
||
|
--- a/packages/client/src/pages/messaging/messaging-room.vue
|
||
|
+++ /dev/null
|
||
|
@@ -1,464 +0,0 @@
|
||
|
-<template>
|
||
|
- <div
|
||
|
- ref="rootEl"
|
||
|
- class="_section"
|
||
|
- @dragover.prevent.stop="onDragover"
|
||
|
- @drop.prevent.stop="onDrop"
|
||
|
- >
|
||
|
- <div class="_content mk-messaging-room">
|
||
|
- <MkSpacer :content-max="800">
|
||
|
- <div class="body">
|
||
|
- <MkPagination
|
||
|
- v-if="pagination"
|
||
|
- ref="pagingComponent"
|
||
|
- :key="userAcct || groupId"
|
||
|
- :pagination="pagination"
|
||
|
- >
|
||
|
- <template #empty>
|
||
|
- <div class="_fullinfo">
|
||
|
- <img
|
||
|
- :src="instance.images.info"
|
||
|
- class="_ghost"
|
||
|
- alt="Info"
|
||
|
- />
|
||
|
- <div>{{ i18n.ts.noMessagesYet }}</div>
|
||
|
- </div>
|
||
|
- </template>
|
||
|
- <template
|
||
|
- #default="{ items: messages, fetching: pFetching }"
|
||
|
- >
|
||
|
- <XList
|
||
|
- aria-live="polite"
|
||
|
- v-if="messages.length > 0"
|
||
|
- v-slot="{ item: message }"
|
||
|
- :class="{
|
||
|
- messages: true,
|
||
|
- 'deny-move-transition': pFetching,
|
||
|
- }"
|
||
|
- :items="messages"
|
||
|
- direction="up"
|
||
|
- reversed
|
||
|
- >
|
||
|
- <XMessage
|
||
|
- :key="message.id"
|
||
|
- :message="message"
|
||
|
- :is-group="group != null"
|
||
|
- />
|
||
|
- </XList>
|
||
|
- </template>
|
||
|
- </MkPagination>
|
||
|
- </div>
|
||
|
- <footer>
|
||
|
- <div
|
||
|
- v-if="typers.length > 0"
|
||
|
- class="typers"
|
||
|
- aria-live="polite"
|
||
|
- >
|
||
|
- <I18n
|
||
|
- :src="i18n.ts.typingUsers"
|
||
|
- text-tag="span"
|
||
|
- class="users"
|
||
|
- >
|
||
|
- <template #users>
|
||
|
- <b
|
||
|
- v-for="typer in typers"
|
||
|
- :key="typer.id"
|
||
|
- class="user"
|
||
|
- >{{ typer.username }}</b
|
||
|
- >
|
||
|
- </template>
|
||
|
- </I18n>
|
||
|
- <MkEllipsis />
|
||
|
- </div>
|
||
|
- <transition :name="animation ? 'fade' : ''">
|
||
|
- <div v-show="showIndicator" class="new-message">
|
||
|
- <button
|
||
|
- class="_buttonPrimary"
|
||
|
- @click="onIndicatorClick"
|
||
|
- >
|
||
|
- <i
|
||
|
- class="fas ph-fw ph-lg ph-arrow-circle-down-bold ph-lg"
|
||
|
- ></i
|
||
|
- >{{ i18n.ts.newMessageExists }}
|
||
|
- </button>
|
||
|
- </div>
|
||
|
- </transition>
|
||
|
- <XForm
|
||
|
- v-if="!fetching"
|
||
|
- ref="formEl"
|
||
|
- :user="user"
|
||
|
- :group="group"
|
||
|
- class="form"
|
||
|
- />
|
||
|
- </footer>
|
||
|
- </MkSpacer>
|
||
|
- </div>
|
||
|
- </div>
|
||
|
-</template>
|
||
|
-
|
||
|
-<script lang="ts" setup>
|
||
|
-import { computed, watch, onMounted, nextTick, onBeforeUnmount } from "vue";
|
||
|
-import * as Misskey from "iceshrimp-js";
|
||
|
-import * as Acct from "iceshrimp-js/built/acct";
|
||
|
-import XMessage from "./messaging-room.message.vue";
|
||
|
-import XForm from "./messaging-room.form.vue";
|
||
|
-import XList from "@/components/MkDateSeparatedList.vue";
|
||
|
-import MkPagination, { Paging } from "@/components/MkPagination.vue";
|
||
|
-import {
|
||
|
- isBottomVisible,
|
||
|
- onScrollBottom,
|
||
|
- scrollToBottom,
|
||
|
-} from "@/scripts/scroll";
|
||
|
-import * as os from "@/os";
|
||
|
-import { stream } from "@/stream";
|
||
|
-import * as sound from "@/scripts/sound";
|
||
|
-import { i18n } from "@/i18n";
|
||
|
-import { $i } from "@/account";
|
||
|
-import { defaultStore } from "@/store";
|
||
|
-import { definePageMetadata } from "@/scripts/page-metadata";
|
||
|
-import {instance} from "@/instance";
|
||
|
-
|
||
|
-const props = defineProps<{
|
||
|
- userAcct?: string;
|
||
|
- groupId?: string;
|
||
|
-}>();
|
||
|
-
|
||
|
-let rootEl = $ref<HTMLDivElement>();
|
||
|
-let formEl = $ref<InstanceType<typeof XForm>>();
|
||
|
-let pagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
||
|
-
|
||
|
-let fetching = $ref(true);
|
||
|
-let user: Misskey.entities.UserDetailed | null = $ref(null);
|
||
|
-let group: Misskey.entities.UserGroup | null = $ref(null);
|
||
|
-let typers: Misskey.entities.User[] = $ref([]);
|
||
|
-let connection: Misskey.ChannelConnection<
|
||
|
- Misskey.Channels["messaging"]
|
||
|
-> | null = $ref(null);
|
||
|
-let showIndicator = $ref(false);
|
||
|
-const { animation } = defaultStore.reactiveState;
|
||
|
-
|
||
|
-let pagination: Paging | null = $ref(null);
|
||
|
-
|
||
|
-watch([() => props.userAcct, () => props.groupId], () => {
|
||
|
- if (connection) connection.dispose();
|
||
|
- fetch();
|
||
|
-});
|
||
|
-
|
||
|
-async function fetch() {
|
||
|
- fetching = true;
|
||
|
-
|
||
|
- if (props.userAcct) {
|
||
|
- const acct = Acct.parse(props.userAcct);
|
||
|
- user = await os.api("users/show", {
|
||
|
- username: acct.username,
|
||
|
- host: acct.host || undefined,
|
||
|
- });
|
||
|
- group = null;
|
||
|
-
|
||
|
- pagination = {
|
||
|
- endpoint: "messaging/messages",
|
||
|
- limit: 20,
|
||
|
- params: {
|
||
|
- userId: user.id,
|
||
|
- },
|
||
|
- reversed: true,
|
||
|
- pageEl: $$(rootEl).value,
|
||
|
- };
|
||
|
- connection = stream.useChannel("messaging", {
|
||
|
- otherparty: user.id,
|
||
|
- });
|
||
|
- } else {
|
||
|
- user = null;
|
||
|
- group = await os.api("users/groups/show", { groupId: props.groupId });
|
||
|
-
|
||
|
- pagination = {
|
||
|
- endpoint: "messaging/messages",
|
||
|
- limit: 20,
|
||
|
- params: {
|
||
|
- groupId: group?.id,
|
||
|
- },
|
||
|
- reversed: true,
|
||
|
- pageEl: $$(rootEl).value,
|
||
|
- };
|
||
|
- connection = stream.useChannel("messaging", {
|
||
|
- group: group?.id,
|
||
|
- });
|
||
|
- }
|
||
|
-
|
||
|
- connection.on("message", onMessage);
|
||
|
- connection.on("read", onRead);
|
||
|
- connection.on("deleted", onDeleted);
|
||
|
- connection.on("typers", (_typers) => {
|
||
|
- typers = _typers.filter((u) => u.id !== $i?.id);
|
||
|
- });
|
||
|
-
|
||
|
- document.addEventListener("visibilitychange", onVisibilitychange);
|
||
|
-
|
||
|
- nextTick(() => {
|
||
|
- // thisScrollToBottom();
|
||
|
- window.setTimeout(() => {
|
||
|
- fetching = false;
|
||
|
- }, 300);
|
||
|
- });
|
||
|
-}
|
||
|
-
|
||
|
-function onDragover(ev: DragEvent) {
|
||
|
- if (!ev.dataTransfer) return;
|
||
|
-
|
||
|
- const isFile = ev.dataTransfer.items[0].kind === "file";
|
||
|
- const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||
|
-
|
||
|
- if (isFile || isDriveFile) {
|
||
|
- ev.dataTransfer.dropEffect =
|
||
|
- ev.dataTransfer.effectAllowed === "all" ? "copy" : "move";
|
||
|
- } else {
|
||
|
- ev.dataTransfer.dropEffect = "none";
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onDrop(ev: DragEvent): void {
|
||
|
- if (!ev.dataTransfer) return;
|
||
|
-
|
||
|
- // ファイルだったら
|
||
|
- if (ev.dataTransfer.files.length === 1) {
|
||
|
- formEl.upload(ev.dataTransfer.files[0]);
|
||
|
- return;
|
||
|
- } else if (ev.dataTransfer.files.length > 1) {
|
||
|
- os.alert({
|
||
|
- type: "error",
|
||
|
- text: i18n.ts.onlyOneFileCanBeAttached,
|
||
|
- });
|
||
|
- return;
|
||
|
- }
|
||
|
-
|
||
|
- //#region ドライブのファイル
|
||
|
- const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||
|
- if (driveFile != null && driveFile !== "") {
|
||
|
- const file = JSON.parse(driveFile);
|
||
|
- formEl.file = file;
|
||
|
- }
|
||
|
- //#endregion
|
||
|
-}
|
||
|
-
|
||
|
-function onMessage(message) {
|
||
|
- sound.play("chat");
|
||
|
-
|
||
|
- const _isBottom = isBottomVisible(rootEl, 64);
|
||
|
-
|
||
|
- pagingComponent.prepend(message);
|
||
|
- if (message.userId !== $i?.id && !document.hidden) {
|
||
|
- connection?.send("read", {
|
||
|
- id: message.id,
|
||
|
- });
|
||
|
- }
|
||
|
-
|
||
|
- if (_isBottom) {
|
||
|
- // Scroll to bottom
|
||
|
- nextTick(() => {
|
||
|
- thisScrollToBottom();
|
||
|
- });
|
||
|
- } else if (message.userId !== $i?.id) {
|
||
|
- // Notify
|
||
|
- notifyNewMessage();
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onRead(x) {
|
||
|
- if (user) {
|
||
|
- if (!Array.isArray(x)) x = [x];
|
||
|
- for (const id of x) {
|
||
|
- if (pagingComponent.items.some((y) => y.id === id)) {
|
||
|
- const exist = pagingComponent.items
|
||
|
- .map((y) => y.id)
|
||
|
- .indexOf(id);
|
||
|
- pagingComponent.items[exist] = {
|
||
|
- ...pagingComponent.items[exist],
|
||
|
- isRead: true,
|
||
|
- };
|
||
|
- }
|
||
|
- }
|
||
|
- } else if (group) {
|
||
|
- for (const id of x.ids) {
|
||
|
- if (pagingComponent.items.some((y) => y.id === id)) {
|
||
|
- const exist = pagingComponent.items
|
||
|
- .map((y) => y.id)
|
||
|
- .indexOf(id);
|
||
|
- pagingComponent.items[exist] = {
|
||
|
- ...pagingComponent.items[exist],
|
||
|
- reads: [...pagingComponent.items[exist].reads, x.userId],
|
||
|
- };
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onDeleted(id) {
|
||
|
- const msg = pagingComponent.items.find((m) => m.id === id);
|
||
|
- if (msg) {
|
||
|
- pagingComponent.items = pagingComponent.items.filter(
|
||
|
- (m) => m.id !== msg.id,
|
||
|
- );
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function thisScrollToBottom() {
|
||
|
- if (window.location.href.includes("my/messaging/")) {
|
||
|
- scrollToBottom($$(rootEl).value, { behavior: "smooth" });
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-function onIndicatorClick() {
|
||
|
- showIndicator = false;
|
||
|
- thisScrollToBottom();
|
||
|
-}
|
||
|
-
|
||
|
-let scrollRemove: (() => void) | null = $ref(null);
|
||
|
-
|
||
|
-function notifyNewMessage() {
|
||
|
- showIndicator = true;
|
||
|
-
|
||
|
- scrollRemove = onScrollBottom(rootEl, () => {
|
||
|
- showIndicator = false;
|
||
|
- scrollRemove = null;
|
||
|
- });
|
||
|
-}
|
||
|
-
|
||
|
-function onVisibilitychange() {
|
||
|
- if (document.hidden) return;
|
||
|
- for (const message of pagingComponent.items) {
|
||
|
- if (message.userId !== $i?.id && !message.isRead) {
|
||
|
- connection?.send("read", {
|
||
|
- id: message.id,
|
||
|
- });
|
||
|
- }
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-onMounted(() => {
|
||
|
- fetch();
|
||
|
- definePageMetadata(
|
||
|
- computed(() => ({
|
||
|
- title: group != null ? group.name : user?.name ?? 'Chat',
|
||
|
- icon: "ph-chats-teardrop-bold ph-lg",
|
||
|
- })),
|
||
|
- );
|
||
|
-});
|
||
|
-
|
||
|
-onBeforeUnmount(() => {
|
||
|
- connection?.dispose();
|
||
|
- document.removeEventListener("visibilitychange", onVisibilitychange);
|
||
|
- if (scrollRemove) scrollRemove();
|
||
|
-});
|
||
|
-</script>
|
||
|
-
|
||
|
-<style lang="scss" scoped>
|
||
|
-XMessage:last-of-type {
|
||
|
- margin-bottom: 4rem;
|
||
|
-}
|
||
|
-
|
||
|
-.mk-messaging-room {
|
||
|
- position: relative;
|
||
|
- overflow: auto;
|
||
|
-
|
||
|
- > .body {
|
||
|
- .more {
|
||
|
- display: block;
|
||
|
- margin: 16px auto;
|
||
|
- padding: 0 12px;
|
||
|
- line-height: 24px;
|
||
|
- color: #fff;
|
||
|
- background: rgba(#000, 0.3);
|
||
|
- border-radius: 12px;
|
||
|
-
|
||
|
- &:hover {
|
||
|
- background: rgba(#000, 0.4);
|
||
|
- }
|
||
|
-
|
||
|
- &:active {
|
||
|
- background: rgba(#000, 0.5);
|
||
|
- }
|
||
|
-
|
||
|
- &.fetching {
|
||
|
- cursor: wait;
|
||
|
- }
|
||
|
-
|
||
|
- > i {
|
||
|
- margin-right: 4px;
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- .messages {
|
||
|
- padding: 8px 0;
|
||
|
-
|
||
|
- > ::v-deep(*) {
|
||
|
- margin-bottom: 16px;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > footer {
|
||
|
- width: 100%;
|
||
|
- position: sticky;
|
||
|
- z-index: 2;
|
||
|
- bottom: 0;
|
||
|
- padding-top: 8px;
|
||
|
- bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
|
||
|
-
|
||
|
- > .new-message {
|
||
|
- width: 100%;
|
||
|
- padding-bottom: 8px;
|
||
|
- text-align: center;
|
||
|
-
|
||
|
- > button {
|
||
|
- display: inline-block;
|
||
|
- margin: 0;
|
||
|
- padding: 0 12px;
|
||
|
- line-height: 32px;
|
||
|
- font-size: 12px;
|
||
|
- border-radius: 16px;
|
||
|
-
|
||
|
- > i {
|
||
|
- display: inline-block;
|
||
|
- margin-right: 8px;
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > .typers {
|
||
|
- position: absolute;
|
||
|
- bottom: 100%;
|
||
|
- padding: 0 8px 0 8px;
|
||
|
- font-size: 0.9em;
|
||
|
- color: var(--fgTransparentWeak);
|
||
|
-
|
||
|
- > .users {
|
||
|
- > .user + .user:before {
|
||
|
- content: ", ";
|
||
|
- font-weight: normal;
|
||
|
- }
|
||
|
-
|
||
|
- > .user:last-of-type:after {
|
||
|
- content: " ";
|
||
|
- }
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
- > .form {
|
||
|
- max-height: 12em;
|
||
|
- overflow-y: scroll;
|
||
|
- border-top: solid 0.5px var(--divider);
|
||
|
- }
|
||
|
- }
|
||
|
-}
|
||
|
-
|
||
|
-.fade-enter-active,
|
||
|
-.fade-leave-active {
|
||
|
- transition: opacity 0.1s;
|
||
|
-}
|
||
|
-
|
||
|
-.fade-enter-from,
|
||
|
-.fade-leave-to {
|
||
|
- transition: opacity 0.5s;
|
||
|
- opacity: 0;
|
||
|
-}
|
||
|
-</style>
|
||
|
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
|
||
|
index dfbd9dc96..b0e6473fd 100644
|
||
|
--- a/packages/client/src/ui/universal.vue
|
||
|
+++ b/packages/client/src/ui/universal.vue
|
||
|
@@ -82,27 +82,6 @@
|
||
|
></span>
|
||
|
</div>
|
||
|
</button>
|
||
|
- <button
|
||
|
- :aria-label="i18n.t('messaging')"
|
||
|
- class="button messaging _button"
|
||
|
- @click="
|
||
|
- mainRouter.push('/my/messaging');
|
||
|
- updateButtonState();
|
||
|
- "
|
||
|
- >
|
||
|
- <div
|
||
|
- class="button-wrapper"
|
||
|
- :class="buttonAnimIndex === 2 ? 'on' : ''"
|
||
|
- >
|
||
|
- <i class="ph-chats-teardrop ph-bold ph-lg"></i
|
||
|
- ><span
|
||
|
- v-if="$i?.hasUnreadMessagingMessage"
|
||
|
- class="indicator"
|
||
|
- :class="{ animateIndicator: $store.state.animation }"
|
||
|
- ><i class="ph-circle ph-fill"></i
|
||
|
- ></span>
|
||
|
- </div>
|
||
|
- </button>
|
||
|
<button
|
||
|
:aria-label="i18n.t('_deck._columns.widgets')"
|
||
|
class="button widget _button"
|
||
|
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
|
||
|
index ff224bba3..243ab68a8 100644
|
||
|
--- a/packages/client/src/router.ts
|
||
|
+++ b/packages/client/src/router.ts
|
||
|
@@ -564,22 +564,6 @@ export const routes = [
|
||
|
component: page(() => import("./pages/favorites.vue")),
|
||
|
loginRequired: true,
|
||
|
},
|
||
|
- {
|
||
|
- name: "messaging",
|
||
|
- path: "/my/messaging",
|
||
|
- component: page(() => import("./pages/messaging/index.vue")),
|
||
|
- loginRequired: true,
|
||
|
- },
|
||
|
- {
|
||
|
- path: "/my/messaging/:userAcct",
|
||
|
- component: page(() => import("./pages/messaging/messaging-room.vue")),
|
||
|
- loginRequired: true,
|
||
|
- },
|
||
|
- {
|
||
|
- path: "/my/messaging/group/:groupId",
|
||
|
- component: page(() => import("./pages/messaging/messaging-room.vue")),
|
||
|
- loginRequired: true,
|
||
|
- },
|
||
|
{
|
||
|
path: "/my/drive/folder/:folder",
|
||
|
component: page(() => import("./pages/drive.vue")),
|
||
|
diff --git a/packages/client/src/navbar.ts b/packages/client/src/navbar.ts
|
||
|
index da6077cef..d83d596a1 100644
|
||
|
--- a/packages/client/src/navbar.ts
|
||
|
+++ b/packages/client/src/navbar.ts
|
||
|
@@ -18,13 +18,6 @@ export const navbarItemDef = reactive({
|
||
|
indicated: computed(() => $i?.hasUnreadNotification),
|
||
|
to: "/my/notifications",
|
||
|
},
|
||
|
- messaging: {
|
||
|
- title: "messaging",
|
||
|
- icon: "ph-chats-teardrop ph-bold ph-lg",
|
||
|
- show: computed(() => $i != null),
|
||
|
- indicated: computed(() => $i?.hasUnreadMessagingMessage),
|
||
|
- to: "/my/messaging",
|
||
|
- },
|
||
|
drive: {
|
||
|
title: "drive",
|
||
|
icon: "ph-cloud ph-bold ph-lg",
|