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",