diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-02-15 13:06:06 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-15 13:06:06 +0900 |
| commit | 8f2049bcd261c3fb10afdc8c15cf4edffe1baa71 (patch) | |
| tree | dc5aa1cefc24d3f6eb36bb1723d7433f8a19e5a2 /packages/frontend | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | misskey-8f2049bcd261c3fb10afdc8c15cf4edffe1baa71.tar.gz misskey-8f2049bcd261c3fb10afdc8c15cf4edffe1baa71.tar.bz2 misskey-8f2049bcd261c3fb10afdc8c15cf4edffe1baa71.zip | |
drop messaging (#9919)
* drop messaging (from backend)
* wip
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/src/components/MkPostForm.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/init.ts | 9 | ||||
| -rw-r--r-- | packages/frontend/src/navbar.ts | 7 | ||||
| -rw-r--r-- | packages/frontend/src/pages/messaging/index.vue | 305 | ||||
| -rw-r--r-- | packages/frontend/src/pages/messaging/messaging-room.form.vue | 366 | ||||
| -rw-r--r-- | packages/frontend/src/pages/messaging/messaging-room.message.vue | 338 | ||||
| -rw-r--r-- | packages/frontend/src/pages/messaging/messaging-room.vue | 415 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/notifications.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/router.ts | 13 |
9 files changed, 0 insertions, 1466 deletions
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 620b126822..77d5adc23e 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -160,12 +160,6 @@ let hasNotSpecifiedMentions = $ref(false); let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]')); let imeText = $ref(''); -const typing = throttle(3000, () => { - if (props.channel) { - stream.send('typingOnChannel', { channel: props.channel.id }); - } -}); - const draftKey = $computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; @@ -447,12 +441,10 @@ function clear() { function onKeydown(ev: KeyboardEvent) { if ((ev.which === 10 || ev.which === 13) && (ev.ctrlKey || ev.metaKey) && canPost) post(); if (ev.which === 27) emit('esc'); - typing(); } function onCompositionUpdate(ev: CompositionEvent) { imeText = ev.data; - typing(); } function onCompositionEnd(ev: CompositionEvent) { diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts index 64c252ce55..b013b376fb 100644 --- a/packages/frontend/src/init.ts +++ b/packages/frontend/src/init.ts @@ -505,15 +505,6 @@ if ($i) { updateAccount({ hasUnreadSpecifiedNotes: false }); }); - main.on('readAllMessagingMessages', () => { - updateAccount({ hasUnreadMessagingMessage: false }); - }); - - main.on('unreadMessagingMessage', () => { - updateAccount({ hasUnreadMessagingMessage: true }); - sound.play('chatBg'); - }); - main.on('readAllAntennas', () => { updateAccount({ hasUnreadAntenna: false }); }); diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 4f809d888e..b85299b07c 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -15,13 +15,6 @@ export const navbarItemDef = reactive({ indicated: computed(() => $i != null && $i.hasUnreadNotification), to: '/my/notifications', }, - messaging: { - title: i18n.ts.messaging, - icon: 'ti ti-messages', - show: computed(() => $i != null), - indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage), - to: '/my/messaging', - }, drive: { title: i18n.ts.drive, icon: 'ti ti-cloud', diff --git a/packages/frontend/src/pages/messaging/index.vue b/packages/frontend/src/pages/messaging/index.vue deleted file mode 100644 index 3d11cf13e9..0000000000 --- a/packages/frontend/src/pages/messaging/index.vue +++ /dev/null @@ -1,305 +0,0 @@ -<template> -<MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> - <div class="yweeujhr"> - <MkButton primary class="start" @click="start"><i class="ti ti-plus"></i> {{ $ts.startMessaging }}</MkButton> - - <div v-if="messages.length > 0" class="history"> - <MkA - v-for="(message, i) in messages" - :key="message.id" - v-anim="i" - class="message _panel" - :class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" - :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" - :data-index="i" - > - <div> - <MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" indicator link preview/> - <header v-if="message.groupId"> - <span class="name">{{ message.group.name }}</span> - <MkTime :time="message.createdAt" class="time"/> - </header> - <header v-else> - <span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span> - <span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span> - <MkTime :time="message.createdAt" class="time"/> - </header> - <div class="body"> - <p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p> - </div> - </div> - </MkA> - </div> - <div v-if="!fetching && messages.length == 0" class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noHistory }}</div> - </div> - <MkLoading v-if="fetching"/> - </div> - </MkSpacer> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue'; -import * as Acct from 'misskey-js/built/acct'; -import MkButton from '@/components/MkButton.vue'; -import { acct } from '@/filters/user'; -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'; - -const router = useRouter(); - -let fetching = $ref(true); -let moreFetching = $ref(false); -let messages = $ref([]); -let connection = $ref(null); - -const getAcct = Acct.toString; - -function isMe(message) { - return message.userId === $i.id; -} - -function onMessage(message) { - 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) { - 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 start(ev) { - os.popupMenu([{ - text: i18n.ts.messagingWithUser, - icon: 'ti ti-user', - action: () => { startUser(); }, - }, { - text: i18n.ts.messagingWithGroup, - icon: 'ti ti-users', - action: () => { startGroup(); }, - }], ev.currentTarget ?? ev.target); -} - -async function startUser() { - os.selectUser().then(user => { - router.push(`/my/messaging/${Acct.toString(user)}`); - }); -} - -async function startGroup() { - 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}`); -} - -onMounted(() => { - connection = markRaw(stream.useChannel('messagingIndex')); - - connection.on('message', onMessage); - connection.on('read', onRead); - - os.api('messaging/history', { group: false }).then(userMessages => { - os.api('messaging/history', { group: true }).then(groupMessages => { - const _messages = userMessages.concat(groupMessages); - _messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - messages = _messages; - fetching = false; - }); - }); -}); - -onUnmounted(() => { - if (connection) connection.dispose(); -}); - -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - -definePageMetadata({ - title: i18n.ts.messaging, - icon: 'ti ti-messages', -}); -</script> - -<style lang="scss" scoped> -.yweeujhr { - - > .start { - margin: 0 auto var(--margin) auto; - } - - > .history { - > .message { - display: block; - text-decoration: none; - margin-bottom: var(--margin); - - * { - pointer-events: none; - user-select: none; - } - - &:hover { - .avatar { - filter: saturate(200%); - } - } - - &:active { - } - - &.isRead, - &.isMe { - opacity: 0.8; - } - - &:not(.isMe):not(.isRead) { - > div { - background-image: url("/client-assets/unread.svg"); - background-repeat: no-repeat; - background-position: 0 center; - } - } - - &:after { - content: ""; - display: block; - clear: both; - } - - > div { - padding: 20px 30px; - - &:after { - content: ""; - display: block; - clear: both; - } - - > header { - display: flex; - align-items: center; - margin-bottom: 2px; - white-space: nowrap; - overflow: hidden; - - > .name { - margin: 0; - padding: 0; - overflow: hidden; - text-overflow: ellipsis; - font-size: 1em; - font-weight: bold; - transition: all 0.1s ease; - } - - > .username { - margin: 0 8px; - } - - > .time { - margin: 0 0 0 auto; - } - } - - > .avatar { - float: left; - width: 54px; - height: 54px; - margin: 0 16px 0 0; - border-radius: 8px; - transition: all 0.1s ease; - } - - > .body { - - > .text { - display: block; - margin: 0 0 0 0; - padding: 0; - overflow: hidden; - overflow-wrap: break-word; - font-size: 1.1em; - color: var(--faceText); - - .me { - opacity: 0.7; - } - } - - > .image { - display: block; - max-width: 100%; - max-height: 512px; - } - } - } - } - } -} - -@container (max-width: 400px) { - .yweeujhr { - > .history { - > .message { - &:not(.isMe):not(.isRead) { - > div { - background-image: none; - border-left: solid 4px #3aa2dc; - } - } - - > div { - padding: 16px; - font-size: 0.9em; - - > .avatar { - margin: 0 12px 0 0; - } - } - } - } - } -} -</style> diff --git a/packages/frontend/src/pages/messaging/messaging-room.form.vue b/packages/frontend/src/pages/messaging/messaging-room.form.vue deleted file mode 100644 index d6113668dd..0000000000 --- a/packages/frontend/src/pages/messaging/messaging-room.form.vue +++ /dev/null @@ -1,366 +0,0 @@ -<template> -<div - :class="$style['root']" - @dragover.stop="onDragover" - @drop.stop="onDrop" -> - <textarea - :class="$style['textarea']" - class="_acrylic" - ref="textEl" - v-model="text" - :placeholder="i18n.ts.inputMessageHere" - @keydown="onKeydown" - @compositionupdate="onCompositionUpdate" - @paste="onPaste" - ></textarea> - <footer :class="$style['footer']"> - <div v-if="file" :class="$style['file']" @click="file = null">{{ file.name }}</div> - <div :class="$style['buttons']"> - <button class="_button" :class="$style['button']" @click="chooseFile"><i class="ti ti-photo-plus"></i></button> - <button class="_button" :class="$style['button']" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> - <button class="_button" :class="[$style['button'], $style['send']]" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send"> - <template v-if="!sending"><i class="ti ti-send"></i></template><template v-if="sending"><MkLoading :em="true"/></template> - </button> - </div> - </footer> - <input :class="$style['file-input']" ref="fileEl" type="file" @change="onChangeFile"/> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, watch } from 'vue'; -import * as Misskey from 'misskey-js'; -import autosize from 'autosize'; -//import insertTextAtCursor from 'insert-text-at-cursor'; -import { throttle } from 'throttle-debounce'; -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 { Autocomplete } from '@/scripts/autocomplete'; -import { uploadFile } from '@/scripts/upload'; -import { miLocalStorage } from '@/local-storage'; - -const props = defineProps<{ - user?: Misskey.entities.UserDetailed | null; - group?: Misskey.entities.UserGroup | null; -}>(); - -let textEl = $shallowRef<HTMLTextAreaElement>(); -let fileEl = $shallowRef<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 !== '') || 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(); - switch (ev.dataTransfer.effectAllowed) { - case 'all': - case 'uninitialized': - case 'copy': - case 'copyLink': - case 'copyMove': - ev.dataTransfer.dropEffect = 'copy'; - break; - case 'linkMove': - case 'move': - ev.dataTransfer.dropEffect = 'move'; - break; - default: - ev.dataTransfer.dropEffect = 'none'; - break; - } - } -} - -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(); - if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey) && canSend) { - 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(miLocalStorage.getItem('message_drafts') || '{}'); - - drafts[draftKey] = { - updatedAt: new Date(), - // eslint-disable-next-line id-denylist - data: { - text: text, - file: file, - }, - }; - - miLocalStorage.setItem('message_drafts', JSON.stringify(drafts)); -} - -function deleteDraft() { - const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}'); - - delete drafts[draftKey]; - - miLocalStorage.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 - // TODO - //new Autocomplete(textEl, this, { model: 'text' }); - - // 書きかけの投稿を復元 - const draft = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}')[draftKey]; - if (draft) { - text = draft.data.text; - file = draft.data.file; - } -}); - -defineExpose({ - file, - upload, -}); -</script> - -<style lang="scss" module> -.root { - position: relative; -} - -.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; - 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; - } - } - } -} - -.file-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; - } -} - -.file-input { - display: none; -} -</style> diff --git a/packages/frontend/src/pages/messaging/messaging-room.message.vue b/packages/frontend/src/pages/messaging/messaging-room.message.vue deleted file mode 100644 index d10798b92e..0000000000 --- a/packages/frontend/src/pages/messaging/messaging-room.message.vue +++ /dev/null @@ -1,338 +0,0 @@ -<template> -<div class="thvuemwp" :class="{ isMe }"> - <MkAvatar class="avatar" :user="message.user" indicator link preview/> - <div class="content"> - <div class="balloon" :class="{ noText: message.text == null }"> - <button v-if="isMe" class="delete-button" :title="$ts.delete" @click="del"> - <img src="/client-assets/remove.png" alt="Delete"/> - </button> - <div v-if="!message.isDeleted" class="content"> - <Mfm v-if="message.text" ref="text" class="text" :text="message.text" :i="$i"/> - <div v-if="message.file" class="file"> - <a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name"> - <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> - <p v-else>{{ message.file.name }}</p> - </a> - </div> - </div> - <div v-else class="content"> - <p class="is-deleted">{{ $ts.deleted }}</p> - </div> - </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">{{ $ts.messageRead }} {{ message.reads.length }}</span> - </template> - <template v-else> - <span v-if="isMe && message.isRead" class="read">{{ $ts.messageRead }}</span> - </template> - <MkTime :time="message.createdAt"/> - <template v-if="message.is_edited"><i class="ti ti-pencil"></i></template> - </footer> - </div> -</div> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import * as mfm from 'mfm-js'; -import * as Misskey from 'misskey-js'; -import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; -import MkUrlPreview from '@/components/MkUrlPreview.vue'; -import * as os from '@/os'; -import { $i } from '@/account'; - -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); - - position: relative; - background-color: transparent; - display: flex; - - > .avatar { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - display: block; - width: 54px; - height: 54px; - 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%; - - &:before { - content: ""; - pointer-events: none; - display: block; - position: absolute; - top: 12px; - } - - & + * { - 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: #555; - 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(--messageBg); - 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; - } - } - } - } -} - -@container (max-width: 400px) { - .thvuemwp { - > .avatar { - width: 48px; - height: 48px; - } - - > .content { - > .balloon { - > .content { - > .text { - font-size: 0.9em; - } - } - } - } - } -} - -@container (max-width: 500px) { - .thvuemwp { - > .content { - > .balloon { - > .content { - > .text { - padding: 8px 16px; - } - } - } - } - } -} -</style> diff --git a/packages/frontend/src/pages/messaging/messaging-room.vue b/packages/frontend/src/pages/messaging/messaging-room.vue deleted file mode 100644 index 0867f003a3..0000000000 --- a/packages/frontend/src/pages/messaging/messaging-room.vue +++ /dev/null @@ -1,415 +0,0 @@ -<template> -<MkStickyContainer> -<template #header> - <MkPageHeader /> -</template> -<div - ref="rootEl" - :class="$style['root']" - @dragover.prevent.stop="onDragover" - @drop.prevent.stop="onDrop" -> - <div :class="$style['body']"> - <MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination"> - <template #empty> - <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ i18n.ts.noMessagesYet }}</div> - </div> - </template> - <template #default="{ items: messages, fetching: pFetching }"> - <MkDateSeparatedList - v-if="messages.length > 0" - v-slot="{ item: message }" - :class="{ [$style['messages']]: true, 'deny-move-transition': pFetching }" - :items="messages" - direction="up" - reversed - > - <XMessage :key="message.id" :message="message" :is-group="group != null"/> - </MkDateSeparatedList> - </template> - </MkPagination> - </div> - <footer :class="$style['footer']"> - <div v-if="typers.length > 0" :class="$style['typers']"> - <I18n :src="i18n.ts.typingUsers" text-tag="span"> - <template #users> - <b v-for="typer in typers" :key="typer.id" :class="$style['user']">{{ typer.username }}</b> - </template> - </I18n> - <MkEllipsis/> - </div> - <Transition :name="animation ? 'fade' : ''"> - <div v-show="showIndicator" :class="$style['new-message']"> - <button class="_buttonPrimary" @click="onIndicatorClick" :class="$style['new-message-button']"> - <i class="fas ti-fw fa-arrow-circle-down" :class="$style['new-message-icon']"></i>{{ i18n.ts.newMessageExists }} - </button> - </div> - </Transition> - <XForm v-if="!fetching" ref="formEl" :user="user" :group="group" :class="$style['form']"/> - </footer> -</div> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue'; -import * as Misskey from 'misskey-js'; -import * as Acct from 'misskey-js/built/acct'; -import XMessage from './messaging-room.message.vue'; -import XForm from './messaging-room.form.vue'; -import MkDateSeparatedList 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'; - -const props = defineProps<{ - userAcct?: string; - groupId?: string; -}>(); - -let rootEl = $shallowRef<HTMLDivElement>(); -let formEl = $shallowRef<InstanceType<typeof XForm>>(); -let pagingComponent = $shallowRef<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(() => { - pagingComponent.inited.then(() => { - 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) { - switch (ev.dataTransfer.effectAllowed) { - case 'all': - case 'uninitialized': - case 'copy': - case 'copyLink': - case 'copyMove': - ev.dataTransfer.dropEffect = 'copy'; - break; - case 'linkMove': - case 'move': - ev.dataTransfer.dropEffect = 'move'; - break; - default: - ev.dataTransfer.dropEffect = 'none'; - break; - } - } 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() { - 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(); -}); - -onBeforeUnmount(() => { - connection?.dispose(); - document.removeEventListener('visibilitychange', onVisibilitychange); - if (scrollRemove) scrollRemove(); -}); - -definePageMetadata(computed(() => !fetching ? user ? { - userName: user, - avatar: user, -} : { - title: group?.name, - icon: 'ti ti-users', -} : null)); -</script> - -<style lang="scss" module> -.root { - display: content; -} - -.body { - min-height: 80%; -} - -.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); - } - > i { - margin-right: 4px; - } -} - -.fetching { - cursor: wait; -} - -.messages { - padding: 16px 0 0; - - > * { - margin-bottom: 16px; - } -} - -.footer { - width: 100%; - position: sticky; - z-index: 2; - padding-top: 8px; - bottom: var(--minBottomSpacing); -} - -.new-message { - width: 100%; - padding-bottom: 8px; - text-align: center; -} - -.new-message-button { - display: inline-block; - margin: 0; - padding: 0 12px; - line-height: 32px; - font-size: 12px; - border-radius: 16px; -} - -.new-message-icon { - display: inline-block; - margin-right: 8px; -} - -.typers { - position: absolute; - bottom: 100%; - padding: 0 8px 0 8px; - font-size: 0.9em; - color: var(--fgTransparentWeak); -} - - -.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); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.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/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index db32ee862c..05c7fb72e5 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -5,7 +5,6 @@ <div class="_gaps_m"> <FormLink @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink> <FormLink @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink> - <FormLink @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink> </div> </FormSection> <FormSection> @@ -47,10 +46,6 @@ async function readAllUnreadNotes() { await os.api('i/read-all-unread-notes'); } -async function readAllMessagingMessages() { - await os.api('i/read-all-messaging-messages'); -} - async function readAllNotifications() { await os.api('notifications/mark-all-as-read'); } diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 9004262689..2aa2e0ab3d 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -421,19 +421,6 @@ export const routes = [{ component: page(() => import('./pages/achievements.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')), loginRequired: true, |