diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-01-09 18:50:03 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-01-09 18:50:03 +0900 |
| commit | a10be38d0ee0e01e278422d58e2f2df7e20d3c40 (patch) | |
| tree | fed75fd26e36bcb68f92c50ed716bf64bd750949 /packages | |
| parent | fix (diff) | |
| download | sharkey-a10be38d0ee0e01e278422d58e2f2df7e20d3c40.tar.gz sharkey-a10be38d0ee0e01e278422d58e2f2df7e20d3c40.tar.bz2 sharkey-a10be38d0ee0e01e278422d58e2f2df7e20d3c40.zip | |
bye chat ui
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/client/src/init.ts | 1 | ||||
| -rw-r--r-- | packages/client/src/menu.ts | 7 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/date-separated-list.vue | 157 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/header-clock.vue | 62 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/index.vue | 463 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/note-header.vue | 99 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/note-preview.vue | 112 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/note.sub.vue | 137 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/note.vue | 1143 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/notes.vue | 94 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/pages/channel.vue | 259 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/pages/timeline.vue | 222 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/post-form.vue | 770 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/side.vue | 157 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/store.ts | 17 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/sub-note-content.vue | 62 | ||||
| -rw-r--r-- | packages/client/src/ui/chat/widgets.vue | 62 |
17 files changed, 0 insertions, 3824 deletions
diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 2263b4ca3c..af70aec70a 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -172,7 +172,6 @@ const app = createApp(await ( !$i ? import('@/ui/visitor.vue') : ui === 'deck' ? import('@/ui/deck.vue') : ui === 'desktop' ? import('@/ui/desktop.vue') : - ui === 'chat' ? import('@/ui/chat/index.vue') : ui === 'classic' ? import('@/ui/classic.vue') : import('@/ui/universal.vue') ).then(x => x.default)); diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts index ea6f801fec..98a892d569 100644 --- a/packages/client/src/menu.ts +++ b/packages/client/src/menu.ts @@ -198,13 +198,6 @@ export const menuDef = reactive({ localStorage.setItem('ui', 'classic'); unisonReload(); } - }, { - text: 'Chat (β)', - active: ui === 'chat', - action: () => { - localStorage.setItem('ui', 'chat'); - unisonReload(); - } }, /*{ text: i18n.locale.desktop + ' (β)', active: ui === 'desktop', diff --git a/packages/client/src/ui/chat/date-separated-list.vue b/packages/client/src/ui/chat/date-separated-list.vue deleted file mode 100644 index 1a36aca6dd..0000000000 --- a/packages/client/src/ui/chat/date-separated-list.vue +++ /dev/null @@ -1,157 +0,0 @@ -<script lang="ts"> -import { defineComponent, h, PropType, TransitionGroup } from 'vue'; -import MkAd from '@/components/global/ad.vue'; - -export default defineComponent({ - props: { - items: { - type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>, - required: true, - }, - reversed: { - type: Boolean, - required: false, - default: false - }, - ad: { - type: Boolean, - required: false, - default: false - }, - }, - - render() { - const getDateText = (time: string) => { - const date = new Date(time).getDate(); - const month = new Date(time).getMonth() + 1; - return this.$t('monthAndDay', { - month: month.toString(), - day: date.toString() - }); - } - - return h(this.reversed ? 'div' : TransitionGroup, { - class: 'hmjzthxl', - name: this.reversed ? 'list-reversed' : 'list', - tag: 'div', - }, this.items.map((item, i) => { - const el = this.$slots.default({ - item: item - })[0]; - if (el.key == null && item.id) el.key = item.id; - - if ( - i != this.items.length - 1 && - new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() - ) { - const separator = h('div', { - class: 'separator', - key: item.id + ':separator', - }, h('p', { - class: 'date' - }, [ - h('span', [ - h('i', { - class: 'fas fa-angle-up icon', - }), - getDateText(item.createdAt) - ]), - h('span', [ - getDateText(this.items[i + 1].createdAt), - h('i', { - class: 'fas fa-angle-down icon', - }) - ]) - ])); - - return [el, separator]; - } else { - if (this.ad && item._shouldInsertAd_) { - return [h(MkAd, { - class: 'a', // advertiseの意(ブロッカー対策) - key: item.id + ':ad', - prefer: ['horizontal', 'horizontal-big'], - }), el]; - } else { - return el; - } - } - })); - }, -}); -</script> - -<style lang="scss"> -.hmjzthxl { - > .list-move { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } - > .list-enter-active { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } - > .list-enter-from { - opacity: 0; - transform: translateY(-64px); - } - - > .list-reversed-enter-active, > .list-reversed-leave-active { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } - > .list-reversed-enter-from { - opacity: 0; - transform: translateY(64px); - } -} -</style> - -<style lang="scss"> -.hmjzthxl { - > .separator { - text-align: center; - position: relative; - - &:before { - content: ""; - display: block; - position: absolute; - top: 50%; - left: 0; - right: 0; - margin: auto; - width: calc(100% - 32px); - height: 1px; - background: var(--divider); - } - - > .date { - display: inline-block; - position: relative; - margin: 0; - padding: 0 16px; - line-height: 32px; - text-align: center; - font-size: 12px; - color: var(--dateLabelFg); - background: var(--panel); - - > span { - &:first-child { - margin-right: 8px; - - > .icon { - margin-right: 8px; - } - } - - &:last-child { - margin-left: 8px; - - > .icon { - margin-left: 8px; - } - } - } - } - } -} -</style> diff --git a/packages/client/src/ui/chat/header-clock.vue b/packages/client/src/ui/chat/header-clock.vue deleted file mode 100644 index 3488289c21..0000000000 --- a/packages/client/src/ui/chat/header-clock.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="acemodlh _monospace"> - <div> - <span v-text="y"></span>/<span v-text="m"></span>/<span v-text="d"></span> - </div> - <div> - <span v-text="hh"></span> - <span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> - <span v-text="mm"></span> - <span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> - <span v-text="ss"></span> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; - -export default defineComponent({ - data() { - return { - clock: null, - y: null, - m: null, - d: null, - hh: null, - mm: null, - ss: null, - showColon: true, - }; - }, - created() { - this.tick(); - this.clock = setInterval(this.tick, 1000); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - tick() { - const now = new Date(); - this.y = now.getFullYear().toString(); - this.m = (now.getMonth() + 1).toString().padStart(2, '0'); - this.d = now.getDate().toString().padStart(2, '0'); - this.hh = now.getHours().toString().padStart(2, '0'); - this.mm = now.getMinutes().toString().padStart(2, '0'); - this.ss = now.getSeconds().toString().padStart(2, '0'); - this.showColon = now.getSeconds() % 2 === 0; - } - } -}); -</script> - -<style lang="scss" scoped> -.acemodlh { - opacity: 0.7; - font-size: 0.85em; - line-height: 1em; - text-align: center; -} -</style> diff --git a/packages/client/src/ui/chat/index.vue b/packages/client/src/ui/chat/index.vue deleted file mode 100644 index f66ab4dcee..0000000000 --- a/packages/client/src/ui/chat/index.vue +++ /dev/null @@ -1,463 +0,0 @@ -<template> -<div class="mk-app" @contextmenu.self.prevent="onContextmenu"> - <XSidebar ref="menu" class="menu" :default-hidden="true"/> - - <div class="nav"> - <header class="header"> - <div class="left"> - <button class="_button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><!--<MkAcct class="text" :user="$i"/>--> - </button> - </div> - <div class="right"> - <MkA v-tooltip="$ts.messaging" class="item" to="/my/messaging"><i class="fas fa-comments icon"></i><span v-if="$i.hasUnreadMessagingMessage" class="indicator"><i class="fas fa-circle"></i></span></MkA> - <MkA v-tooltip="$ts.directNotes" class="item" to="/my/messages"><i class="fas fa-envelope icon"></i><span v-if="$i.hasUnreadSpecifiedNotes" class="indicator"><i class="fas fa-circle"></i></span></MkA> - <MkA v-tooltip="$ts.mentions" class="item" to="/my/mentions"><i class="fas fa-at icon"></i><span v-if="$i.hasUnreadMentions" class="indicator"><i class="fas fa-circle"></i></span></MkA> - <MkA v-tooltip="$ts.notifications" class="item" to="/my/notifications"><i class="fas fa-bell icon"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></MkA> - </div> - </header> - <div class="body"> - <div class="container"> - <div class="header">{{ $ts.timeline }}</div> - <div class="body"> - <MkA to="/timeline/home" class="item" :class="{ active: tl === 'home' }"><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA> - <MkA to="/timeline/local" class="item" :class="{ active: tl === 'local' }"><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA> - <MkA to="/timeline/social" class="item" :class="{ active: tl === 'social' }"><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA> - <MkA to="/timeline/global" class="item" :class="{ active: tl === 'global' }"><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA> - </div> - </div> - <div v-if="followedChannels" class="container"> - <div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div> - <div class="body"> - <MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA> - </div> - </div> - <div v-if="featuredChannels" class="container"> - <div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div> - <div class="body"> - <MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA> - </div> - </div> - <div v-if="lists" class="container"> - <div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div> - <div class="body"> - <MkA v-for="list in lists" :key="list.id" :to="`/my/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA> - </div> - </div> - <div v-if="antennas" class="container"> - <div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div> - <div class="body"> - <MkA v-for="antenna in antennas" :key="antenna.id" :to="`/my/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA> - </div> - </div> - <div class="container"> - <div class="body"> - <MkA to="/my/favorites" class="item"><i class="fas fa-star icon"></i>{{ $ts.favorites }}</MkA> - </div> - </div> - <MkAd class="a" :prefer="['square']"/> - </div> - <footer class="footer"> - <div class="left"> - <button class="_button menu" @click="showMenu"> - <i class="fas fa-bars icon"></i> - </button> - </div> - <div class="right"> - <button v-tooltip="$ts.search" class="_button item search" @click="search"> - <i class="fas fa-search icon"></i> - </button> - <MkA v-tooltip="$ts.settings" class="item" to="/settings"><i class="fas fa-cog icon"></i></MkA> - </div> - </footer> - </div> - - <main class="main" @contextmenu.stop="onContextmenu"> - <header class="header"> - <MkHeader class="header" :info="pageInfo" :menu="menu" :center="false" @click="onHeaderClick"/> - </header> - <router-view v-slot="{ Component }"> - <transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition"> - <keep-alive :include="['timeline']"> - <component :is="Component" :ref="changePage" class="body"/> - </keep-alive> - </transition> - </router-view> - </main> - - <XSide ref="side" class="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/> - <div class="side widgets" :class="{ sideViewOpening }"> - <XWidgets/> - </div> - - <XCommon/> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import { instanceName, url } from '@/config'; -import XSidebar from '@/ui/_common_/sidebar.vue'; -import XWidgets from './widgets.vue'; -import XCommon from '../_common_/common.vue'; -import XSide from './side.vue'; -import XHeaderClock from './header-clock.vue'; -import * as os from '@/os'; -import { router } from '@/router'; -import { menuDef } from '@/menu'; -import { search } from '@/scripts/search'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { store } from './store'; -import * as symbols from '@/symbols'; -import { openAccountMenu } from '@/account'; - -export default defineComponent({ - components: { - XCommon, - XSidebar, - XWidgets, - XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる - XHeaderClock, - }, - - provide() { - return { - sideViewHook: (path) => { - this.$refs.side.navigate(path); - } - }; - }, - - data() { - return { - pageInfo: null, - lists: null, - antennas: null, - followedChannels: null, - featuredChannels: null, - currentChannel: null, - menuDef: menuDef, - sideViewOpening: false, - instanceName, - }; - }, - - computed: { - menu() { - return [{ - icon: 'fas fa-columns', - text: this.$ts.openInSideView, - action: () => { - this.$refs.side.navigate(this.$route.path); - } - }, { - icon: 'fas fa-window-maximize', - text: this.$ts.openInWindow, - action: () => { - os.pageWindow(this.$route.path); - } - }]; - } - }, - - created() { - if (window.innerWidth < 1024) { - localStorage.setItem('ui', 'default'); - location.reload(); - } - - os.api('users/lists/list').then(lists => { - this.lists = lists; - }); - - os.api('antennas/list').then(antennas => { - this.antennas = antennas; - }); - - os.api('channels/followed', { limit: 20 }).then(channels => { - this.followedChannels = channels; - }); - - // TODO: pagination - os.api('channels/featured', { limit: 20 }).then(channels => { - this.featuredChannels = channels; - }); - }, - - methods: { - changePage(page) { - console.log(page); - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - this.pageInfo = page[symbols.PAGE_INFO]; - document.title = `${this.pageInfo.title} | ${instanceName}`; - } - }, - - showMenu() { - this.$refs.menu.show(); - }, - - post() { - os.post(); - }, - - search() { - search(); - }, - - back() { - history.back(); - }, - - top() { - window.scroll({ top: 0, behavior: 'smooth' }); - }, - - onTransition() { - if (window._scroll) window._scroll(); - }, - - onHeaderClick() { - window.scroll({ top: 0, behavior: 'smooth' }); - }, - - onContextmenu(e) { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(e.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; - if (window.getSelection().toString() !== '') return; - const path = this.$route.path; - os.contextMenu([{ - type: 'label', - text: path, - }, { - icon: 'fas fa-columns', - text: this.$ts.openInSideView, - action: () => { - this.$refs.side.navigate(path); - } - }, { - icon: 'fas fa-window-maximize', - text: this.$ts.openInWindow, - action: () => { - os.pageWindow(path); - } - }], e); - }, - - openAccountMenu, - } -}); -</script> - -<style lang="scss" scoped> -.mk-app { - $header-height: 54px; // TODO: どこかに集約したい - $ui-font-size: 1em; // TODO: どこかに集約したい - - // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ - height: calc(var(--vh, 1vh) * 100); - display: flex; - - > .nav { - display: flex; - flex-direction: column; - width: 250px; - height: 100vh; - border-right: solid 4px var(--divider); - - > .header, > .footer { - $padding: 8px; - display: flex; - align-items: center; - z-index: 1000; - height: $header-height; - padding: $padding; - box-sizing: border-box; - user-select: none; - - &.header { - border-bottom: solid 0.5px var(--divider); - } - - &.footer { - border-top: solid 0.5px var(--divider); - } - - > .left, > .right { - > .item, > .menu { - display: inline-flex; - vertical-align: middle; - height: ($header-height - ($padding * 2)); - width: ($header-height - ($padding * 2)); - box-sizing: border-box; - //opacity: 0.6; - position: relative; - border-radius: 5px; - - &:hover { - background: rgba(0, 0, 0, 0.05); - } - - > .icon { - margin: auto; - } - - > .indicator { - position: absolute; - top: 8px; - right: 8px; - color: var(--indicator); - font-size: 8px; - line-height: 8px; - animation: blink 1s infinite; - } - } - } - - > .left { - flex: 1; - min-width: 0; - - > .account { - display: flex; - align-items: center; - padding: 0 8px; - - > .avatar { - width: 26px; - height: 26px; - margin-right: 8px; - } - - > .text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 0.9em; - } - } - } - - > .right { - margin-left: auto; - } - } - - > .body { - flex: 1; - min-width: 0; - overflow: auto; - - > .container { - margin-top: 8px; - margin-bottom: 8px; - - & + .container { - margin-top: 16px; - } - - > .header { - display: flex; - font-size: 0.9em; - padding: 8px 16px; - position: sticky; - top: 0; - background: var(--X17); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - z-index: 1; - color: var(--fgTransparentWeak); - - > .add { - margin-left: auto; - color: var(--fgTransparentWeak); - - &:hover { - color: var(--fg); - } - } - } - - > .body { - padding: 0 8px; - - > .item { - display: block; - padding: 6px 8px; - border-radius: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:hover { - text-decoration: none; - background: rgba(0, 0, 0, 0.05); - } - - &.active, &.active:hover { - background: var(--accent); - color: #fff !important; - } - - &.read { - color: var(--fgTransparent); - } - - > .icon { - margin-right: 8px; - opacity: 0.6; - } - } - } - } - - > .a { - margin: 12px; - } - } - } - - > .main { - display: flex; - flex: 1; - flex-direction: column; - min-width: 0; - height: 100vh; - position: relative; - background: var(--panel); - - > .header { - z-index: 1000; - height: $header-height; - background-color: var(--panel); - border-bottom: solid 0.5px var(--divider); - user-select: none; - } - - > .body { - width: 100%; - box-sizing: border-box; - overflow: auto; - } - } - - > .side { - width: 350px; - border-left: solid 4px var(--divider); - background: var(--panel); - - &.widgets.sideViewOpening { - @media (max-width: 1400px) { - display: none; - } - } - } -} -</style> diff --git a/packages/client/src/ui/chat/note-header.vue b/packages/client/src/ui/chat/note-header.vue deleted file mode 100644 index 5f87fdd14e..0000000000 --- a/packages/client/src/ui/chat/note-header.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> -<header class="dehvdgxo"> - <MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - <span v-if="note.user.isBot" class="is-bot">bot</span> - <span class="username"><MkAcct :user="note.user"/></span> - <div class="info"> - <MkA class="created-at" :to="notePage(note)"> - <MkTime :time="note.createdAt"/> - </MkA> - <span v-if="note.visibility !== 'public'" class="visibility"> - <i v-if="note.visibility === 'home'" class="fas fa-home"></i> - <i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i> - <i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i> - </span> - <span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span> - </div> -</header> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { notePage } from '@/filters/note'; -import { userPage } from '@/filters/user'; -import * as os from '@/os'; - -export default defineComponent({ - props: { - note: { - type: Object, - required: true - }, - }, - - data() { - return { - }; - }, - - methods: { - notePage, - userPage - } -}); -</script> - -<style lang="scss" scoped> -.dehvdgxo { - display: flex; - align-items: baseline; - white-space: nowrap; - font-size: 0.9em; - - > .name { - display: block; - margin: 0 .5em 0 0; - padding: 0; - overflow: hidden; - font-size: 1em; - font-weight: bold; - text-decoration: none; - text-overflow: ellipsis; - - &:hover { - text-decoration: underline; - } - } - - > .is-bot { - flex-shrink: 0; - align-self: center; - margin: 0 .5em 0 0; - padding: 1px 6px; - font-size: 80%; - border: solid 0.5px var(--divider); - border-radius: 3px; - } - - > .username { - margin: 0 .5em 0 0; - overflow: hidden; - text-overflow: ellipsis; - } - - > .info { - font-size: 0.9em; - opacity: 0.7; - - > .visibility { - margin-left: 8px; - } - - > .localOnly { - margin-left: 8px; - } - } -} -</style> diff --git a/packages/client/src/ui/chat/note-preview.vue b/packages/client/src/ui/chat/note-preview.vue deleted file mode 100644 index c28591815e..0000000000 --- a/packages/client/src/ui/chat/note-preview.vue +++ /dev/null @@ -1,112 +0,0 @@ -<template> -<div class="hduudsxk"> - <MkAvatar class="avatar" :user="note.user"/> - <div class="main"> - <XNoteHeader class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <span v-if="note.cw != ''" class="text">{{ note.cw }}</span> - <XCwButton v-model="showContent" :note="note"/> - </p> - <div v-show="note.cw == null || showContent" class="content"> - <XSubNote-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XNoteHeader from './note-header.vue'; -import XSubNoteContent from './sub-note-content.vue'; -import XCwButton from '@/components/cw-button.vue'; -import * as os from '@/os'; - -export default defineComponent({ - components: { - XNoteHeader, - XSubNoteContent, - XCwButton, - }, - - props: { - note: { - type: Object, - required: true - } - }, - - data() { - return { - showContent: false - }; - } -}); -</script> - -<style lang="scss" scoped> -.hduudsxk { - display: flex; - margin: 0; - padding: 0; - overflow: hidden; - font-size: 0.95em; - - > .avatar { - - @media (min-width: 350px) { - margin: 0 10px 0 0; - width: 44px; - height: 44px; - } - - @media (min-width: 500px) { - margin: 0 12px 0 0; - width: 48px; - height: 48px; - } - } - - > .avatar { - flex-shrink: 0; - display: block; - margin: 0 10px 0 0; - width: 40px; - height: 40px; - border-radius: 8px; - } - - > .main { - flex: 1; - min-width: 0; - - > .header { - margin-bottom: 2px; - } - - > .body { - - > .cw { - cursor: default; - display: block; - margin: 0; - padding: 0; - overflow-wrap: break-word; - - > .text { - margin-right: 8px; - } - } - - > .content { - > .text { - cursor: default; - margin: 0; - padding: 0; - } - } - } - } -} -</style> diff --git a/packages/client/src/ui/chat/note.sub.vue b/packages/client/src/ui/chat/note.sub.vue deleted file mode 100644 index b61b7521a8..0000000000 --- a/packages/client/src/ui/chat/note.sub.vue +++ /dev/null @@ -1,137 +0,0 @@ -<template> -<div class="wrpstxzv" :class="{ children }"> - <div class="main"> - <MkAvatar class="avatar" :user="note.user"/> - <div class="body"> - <XNoteHeader class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> - <XCwButton v-model="showContent" :note="note"/> - </p> - <div v-show="note.cw == null || showContent" class="content"> - <XSubNote-content class="text" :note="note"/> - </div> - </div> - </div> - </div> - <XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XNoteHeader from './note-header.vue'; -import XSubNoteContent from './sub-note-content.vue'; -import XCwButton from '@/components/cw-button.vue'; -import * as os from '@/os'; - -export default defineComponent({ - name: 'XSub', - - components: { - XNoteHeader, - XSubNoteContent, - XCwButton, - }, - - props: { - note: { - type: Object, - required: true - }, - detail: { - type: Boolean, - required: false, - default: false - }, - children: { - type: Boolean, - required: false, - default: false - }, - // TODO - truncate: { - type: Boolean, - default: true - } - }, - - data() { - return { - showContent: false, - replies: [], - }; - }, - - created() { - if (this.detail) { - os.api('notes/children', { - noteId: this.note.id, - limit: 5 - }).then(replies => { - this.replies = replies; - }); - } - }, -}); -</script> - -<style lang="scss" scoped> -.wrpstxzv { - padding: 16px 16px; - font-size: 0.8em; - - &.children { - padding: 10px 0 0 16px; - font-size: 1em; - } - - > .main { - display: flex; - - > .avatar { - flex-shrink: 0; - display: block; - margin: 0 8px 0 0; - width: 36px; - height: 36px; - } - - > .body { - flex: 1; - min-width: 0; - - > .header { - margin-bottom: 2px; - } - - > .body { - > .cw { - cursor: default; - display: block; - margin: 0; - padding: 0; - overflow-wrap: break-word; - - > .text { - margin-right: 8px; - } - } - - > .content { - > .text { - margin: 0; - padding: 0; - } - } - } - } - } - - > .reply { - border-left: solid 0.5px var(--divider); - margin-top: 10px; - } -} -</style> diff --git a/packages/client/src/ui/chat/note.vue b/packages/client/src/ui/chat/note.vue deleted file mode 100644 index fa5faa4ec3..0000000000 --- a/packages/client/src/ui/chat/note.vue +++ /dev/null @@ -1,1143 +0,0 @@ -<template> -<div - v-if="!muted" - v-show="!isDeleted" - v-hotkey="keymap" - class="vfzoeqcg" - :tabindex="!isDeleted ? '-1' : null" - :class="{ renote: isRenote, highlighted: appearNote._prId_ || appearNote._featuredId_, operating }" -> - <XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> - <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div> - <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div> - <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div> - <div v-if="isRenote" class="renote"> - <MkAvatar class="avatar" :user="note.user"/> - <i class="fas fa-retweet"></i> - <I18n :src="$ts.renotedBy" tag="span"> - <template #user> - <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - </template> - </I18n> - <div class="info"> - <button ref="renoteTime" class="_button time" @click="showRenoteMenu()"> - <i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i> - <MkTime :time="note.createdAt"/> - </button> - <span v-if="note.visibility !== 'public'" class="visibility"> - <i v-if="note.visibility === 'home'" class="fas fa-home"></i> - <i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i> - <i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i> - </span> - <span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span> - </div> - </div> - <article class="article" @contextmenu.stop="onContextmenu"> - <MkAvatar class="avatar" :user="appearNote.user"/> - <div class="main"> - <XNoteHeader class="header" :note="appearNote" :mini="true"/> - <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> - <XCwButton v-model="showContent" :note="appearNote"/> - </p> - <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span> - <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA> - <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> - <a v-if="appearNote.renote != null" class="rp">RN:</a> - </div> - <div v-if="appearNote.files.length > 0" class="files"> - <XMediaList :media-list="appearNote.files"/> - </div> - <XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> - <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> - <button v-if="collapsed" class="fade _button" @click="collapsed = false"> - <span>{{ $ts.showMore }}</span> - </button> - </div> - <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA> - </div> - <XReactionsViewer ref="reactionsViewer" :note="appearNote"/> - <footer class="footer _panel"> - <button v-tooltip="$ts.reply" class="button _button" @click="reply()"> - <template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template> - <template v-else><i class="fas fa-reply"></i></template> - <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="canRenote" ref="renoteButton" v-tooltip="$ts.renote" class="button _button" @click="renote()"> - <i class="fas fa-retweet"></i><p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="button _button"> - <i class="fas fa-ban"></i> - </button> - <button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip="$ts.reaction" class="button _button" @click="react()"> - <i class="fas fa-plus"></i> - </button> - <button v-if="appearNote.myReaction != null" ref="reactButton" v-tooltip="$ts.reaction" class="button _button reacted" @click="undoReact(appearNote)"> - <i class="fas fa-minus"></i> - </button> - <button ref="menuButton" class="button _button" @click="menu()"> - <i class="fas fa-ellipsis-h"></i> - </button> - </footer> - </div> - </article> -</div> -<div v-else class="muted" @click="muted = false"> - <I18n :src="$ts.userSaysSomething" tag="small"> - <template #name> - <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> - <MkUserName :user="appearNote.user"/> - </MkA> - </template> - </I18n> -</div> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; -import * as mfm from 'mfm-js'; -import { sum } from '@/scripts/array'; -import XSub from './note.sub.vue'; -import XNoteHeader from './note-header.vue'; -import XNoteSimple from './note-preview.vue'; -import XReactionsViewer from '@/components/reactions-viewer.vue'; -import XMediaList from '@/components/media-list.vue'; -import XCwButton from '@/components/cw-button.vue'; -import XPoll from '@/components/poll.vue'; -import { pleaseLogin } from '@/scripts/please-login'; -import { focusPrev, focusNext } from '@/scripts/focus'; -import { url } from '@/config'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { checkWordMute } from '@/scripts/check-word-mute'; -import { userPage } from '@/filters/user'; -import * as os from '@/os'; -import { stream } from '@/stream'; -import { noteActions, noteViewInterruptors } from '@/store'; -import { reactionPicker } from '@/scripts/reaction-picker'; -import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; - -export default defineComponent({ - components: { - XSub, - XNoteHeader, - XNoteSimple, - XReactionsViewer, - XMediaList, - XCwButton, - XPoll, - MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), - MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), - }, - - inject: { - inChannel: { - default: null - }, - }, - - props: { - note: { - type: Object, - required: true - }, - pinned: { - type: Boolean, - required: false, - default: false - }, - }, - - emits: ['update:note'], - - data() { - return { - connection: null, - replies: [], - showContent: false, - collapsed: false, - isDeleted: false, - muted: false, - operating: false, - }; - }, - - computed: { - rs() { - return this.$store.state.reactions; - }, - keymap(): any { - return { - 'r': () => this.reply(true), - 'e|a|plus': () => this.react(true), - 'q': () => this.renote(true), - 'f|b': this.favorite, - 'delete|ctrl+d': this.del, - 'ctrl+q': this.renoteDirectly, - 'up|k|shift+tab': this.focusBefore, - 'down|j|tab': this.focusAfter, - 'esc': this.blur, - 'm|o': () => this.menu(true), - 's': this.toggleShowContent, - '1': () => this.reactDirectly(this.rs[0]), - '2': () => this.reactDirectly(this.rs[1]), - '3': () => this.reactDirectly(this.rs[2]), - '4': () => this.reactDirectly(this.rs[3]), - '5': () => this.reactDirectly(this.rs[4]), - '6': () => this.reactDirectly(this.rs[5]), - '7': () => this.reactDirectly(this.rs[6]), - '8': () => this.reactDirectly(this.rs[7]), - '9': () => this.reactDirectly(this.rs[8]), - '0': () => this.reactDirectly(this.rs[9]), - }; - }, - - isRenote(): boolean { - return (this.note.renote && - this.note.text == null && - this.note.fileIds.length == 0 && - this.note.poll == null); - }, - - appearNote(): any { - return this.isRenote ? this.note.renote : this.note; - }, - - isMyNote(): boolean { - return this.$i && (this.$i.id === this.appearNote.userId); - }, - - isMyRenote(): boolean { - return this.$i && (this.$i.id === this.note.userId); - }, - - canRenote(): boolean { - return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote; - }, - - reactionsCount(): number { - return this.appearNote.reactions - ? sum(Object.values(this.appearNote.reactions)) - : 0; - }, - - urls(): string[] { - if (this.appearNote.text) { - return extractUrlFromMfm(mfm.parse(this.appearNote.text)); - } else { - return null; - } - }, - - showTicker() { - if (this.$store.state.instanceTicker === 'always') return true; - if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true; - return false; - } - }, - - async created() { - if (this.$i) { - this.connection = stream; - } - - this.collapsed = this.appearNote.cw == null && this.appearNote.text && ( - (this.appearNote.text.split('\n').length > 9) || - (this.appearNote.text.length > 500) - ); - this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords); - - // plugin - if (noteViewInterruptors.length > 0) { - let result = this.note; - for (const interruptor of noteViewInterruptors) { - result = await interruptor.handler(JSON.parse(JSON.stringify(result))); - } - this.$emit('update:note', Object.freeze(result)); - } - }, - - mounted() { - this.capture(true); - - if (this.$i) { - this.connection.on('_connected_', this.onStreamConnected); - } - }, - - beforeUnmount() { - this.decapture(true); - - if (this.$i) { - this.connection.off('_connected_', this.onStreamConnected); - } - }, - - methods: { - updateAppearNote(v) { - this.$emit('update:note', Object.freeze(this.isRenote ? { - ...this.note, - renote: { - ...this.note.renote, - ...v - } - } : { - ...this.note, - ...v - })); - }, - - readPromo() { - os.api('promo/read', { - noteId: this.appearNote.id - }); - this.isDeleted = true; - }, - - capture(withHandler = false) { - if (this.$i) { - // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id }); - if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); - } - }, - - decapture(withHandler = false) { - if (this.$i) { - this.connection.send('un', { - id: this.appearNote.id - }); - if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); - } - }, - - onStreamConnected() { - this.capture(); - }, - - onStreamNoteUpdated(data) { - const { type, id, body } = data; - - if (id !== this.appearNote.id) return; - - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - if (body.emoji) { - const emojis = this.appearNote.emojis || []; - if (!emojis.includes(body.emoji)) { - n.emojis = [...emojis, body.emoji]; - } - } - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (this.appearNote.reactions || {})[reaction] || 0; - - // Increment the count - n.reactions = { - ...this.appearNote.reactions, - [reaction]: currentCount + 1 - }; - - if (body.userId === this.$i.id) { - n.myReaction = reaction; - } - - this.updateAppearNote(n); - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (this.appearNote.reactions || {})[reaction] || 0; - - // Decrement the count - n.reactions = { - ...this.appearNote.reactions, - [reaction]: Math.max(0, currentCount - 1) - }; - - if (body.userId === this.$i.id) { - n.myReaction = null; - } - - this.updateAppearNote(n); - break; - } - - case 'pollVoted': { - const choice = body.choice; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - const choices = [...this.appearNote.poll.choices]; - choices[choice] = { - ...choices[choice], - votes: choices[choice].votes + 1, - ...(body.userId === this.$i.id ? { - isVoted: true - } : {}) - }; - - n.poll = { - ...this.appearNote.poll, - choices: choices - }; - - this.updateAppearNote(n); - break; - } - - case 'deleted': { - this.isDeleted = true; - break; - } - } - }, - - reply(viaKeyboard = false) { - pleaseLogin(); - this.operating = true; - os.post({ - reply: this.appearNote, - animation: !viaKeyboard, - }, () => { - this.operating = false; - this.focus(); - }); - }, - - renote(viaKeyboard = false) { - pleaseLogin(); - this.operating = true; - this.blur(); - os.popupMenu([{ - text: this.$ts.renote, - icon: 'fas fa-retweet', - action: () => { - os.api('notes/create', { - renoteId: this.appearNote.id - }); - } - }, { - text: this.$ts.quote, - icon: 'fas fa-quote-right', - action: () => { - os.post({ - renote: this.appearNote, - }); - } - }], this.$refs.renoteButton, { - viaKeyboard - }).then(() => { - this.operating = false; - }); - }, - - renoteDirectly() { - os.apiWithDialog('notes/create', { - renoteId: this.appearNote.id - }, undefined, (res: any) => { - os.alert({ - type: 'success', - text: this.$ts.renoted, - }); - }, (e: Error) => { - if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') { - os.alert({ - type: 'error', - text: this.$ts.cantRenote, - }); - } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') { - os.alert({ - type: 'error', - text: this.$ts.cantReRenote, - }); - } - }); - }, - - async react(viaKeyboard = false) { - pleaseLogin(); - this.operating = true; - this.blur(); - reactionPicker.show(this.$refs.reactButton, reaction => { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, () => { - this.operating = false; - this.focus(); - }); - }, - - reactDirectly(reaction) { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, - - undoReact(note) { - const oldReaction = note.myReaction; - if (!oldReaction) return; - os.api('notes/reactions/delete', { - noteId: note.id - }); - }, - - favorite() { - pleaseLogin(); - os.apiWithDialog('notes/favorites/create', { - noteId: this.appearNote.id - }, undefined, (res: any) => { - os.alert({ - type: 'success', - text: this.$ts.favorited, - }); - }, (e: Error) => { - if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') { - os.alert({ - type: 'error', - text: this.$ts.alreadyFavorited, - }); - } else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') { - os.alert({ - type: 'error', - text: this.$ts.cantFavorite, - }); - } - }); - }, - - del() { - os.confirm({ - type: 'warning', - text: this.$ts.noteDeleteConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: this.appearNote.id - }); - }); - }, - - delEdit() { - os.confirm({ - type: 'warning', - text: this.$ts.deleteAndEditConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: this.appearNote.id - }); - - os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel }); - }); - }, - - toggleFavorite(favorite: boolean) { - os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: this.appearNote.id - }); - }, - - toggleWatch(watch: boolean) { - os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', { - noteId: this.appearNote.id - }); - }, - - getMenu() { - let menu; - if (this.$i) { - const statePromise = os.api('notes/state', { - noteId: this.appearNote.id - }); - - menu = [{ - icon: 'fas fa-copy', - text: this.$ts.copyContent, - action: this.copyContent - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: this.copyLink - }, (this.appearNote.url || this.appearNote.uri) ? { - icon: 'fas fa-external-link-square-alt', - text: this.$ts.showOnRemote, - action: () => { - window.open(this.appearNote.url || this.appearNote.uri, '_blank'); - } - } : undefined, - { - icon: 'fas fa-share-alt', - text: this.$ts.share, - action: this.share - }, - null, - statePromise.then(state => state.isFavorited ? { - icon: 'fas fa-star', - text: this.$ts.unfavorite, - action: () => this.toggleFavorite(false) - } : { - icon: 'fas fa-star', - text: this.$ts.favorite, - action: () => this.toggleFavorite(true) - }), - { - icon: 'fas fa-paperclip', - text: this.$ts.clip, - action: () => this.clip() - }, - (this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? { - icon: 'fas fa-eye-slash', - text: this.$ts.unwatch, - action: () => this.toggleWatch(false) - } : { - icon: 'fas fa-eye', - text: this.$ts.watch, - action: () => this.toggleWatch(true) - }) : undefined, - this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? { - icon: 'fas fa-thumbtack', - text: this.$ts.unpin, - action: () => this.togglePin(false) - } : { - icon: 'fas fa-thumbtack', - text: this.$ts.pin, - action: () => this.togglePin(true) - } : undefined, - /* - ...(this.$i.isModerator || this.$i.isAdmin ? [ - null, - { - icon: 'fas fa-bullhorn', - text: this.$ts.promote, - action: this.promote - }] - : [] - ),*/ - ...(this.appearNote.userId != this.$i.id ? [ - null, - { - icon: 'fas fa-exclamation-circle', - text: this.$ts.reportAbuse, - action: () => { - const u = `${url}/notes/${this.appearNote.id}`; - os.popup(import('@/components/abuse-report-window.vue'), { - user: this.appearNote.user, - initialComment: `Note: ${u}\n-----\n` - }, {}, 'closed'); - } - }] - : [] - ), - ...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [ - null, - this.appearNote.userId == this.$i.id ? { - icon: 'fas fa-edit', - text: this.$ts.deleteAndEdit, - action: this.delEdit - } : undefined, - { - icon: 'fas fa-trash-alt', - text: this.$ts.delete, - danger: true, - action: this.del - }] - : [] - )] - .filter(x => x !== undefined); - } else { - menu = [{ - icon: 'fas fa-copy', - text: this.$ts.copyContent, - action: this.copyContent - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: this.copyLink - }, (this.appearNote.url || this.appearNote.uri) ? { - icon: 'fas fa-external-link-square-alt', - text: this.$ts.showOnRemote, - action: () => { - window.open(this.appearNote.url || this.appearNote.uri, '_blank'); - } - } : undefined] - .filter(x => x !== undefined); - } - - if (noteActions.length > 0) { - menu = menu.concat([null, ...noteActions.map(action => ({ - icon: 'fas fa-plug', - text: action.title, - action: () => { - action.handler(this.appearNote); - } - }))]); - } - - return menu; - }, - - onContextmenu(e) { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(e.target)) return; - if (window.getSelection().toString() !== '') return; - - if (this.$store.state.useReactionPickerForContextMenu) { - e.preventDefault(); - this.react(); - } else { - os.contextMenu(this.getMenu(), e).then(this.focus); - } - }, - - menu(viaKeyboard = false) { - this.operating = true; - os.popupMenu(this.getMenu(), this.$refs.menuButton, { - viaKeyboard - }).then(() => { - this.operating = false; - this.focus(); - }); - }, - - showRenoteMenu(viaKeyboard = false) { - if (!this.isMyRenote) return; - os.popupMenu([{ - text: this.$ts.unrenote, - icon: 'fas fa-trash-alt', - danger: true, - action: () => { - os.api('notes/delete', { - noteId: this.note.id - }); - this.isDeleted = true; - } - }], this.$refs.renoteTime, { - viaKeyboard: viaKeyboard - }); - }, - - toggleShowContent() { - this.showContent = !this.showContent; - }, - - copyContent() { - copyToClipboard(this.appearNote.text); - os.success(); - }, - - copyLink() { - copyToClipboard(`${url}/notes/${this.appearNote.id}`); - os.success(); - }, - - togglePin(pin: boolean) { - os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { - noteId: this.appearNote.id - }, undefined, null, e => { - if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { - os.alert({ - type: 'error', - text: this.$ts.pinLimitExceeded - }); - } - }); - }, - - async clip() { - const clips = await os.api('clips/list'); - os.popupMenu([{ - icon: 'fas fa-plus', - text: this.$ts.createNew, - action: async () => { - const { canceled, result } = await os.form(this.$ts.createNewClip, { - name: { - type: 'string', - label: this.$ts.name - }, - description: { - type: 'string', - required: false, - multiline: true, - label: this.$ts.description - }, - isPublic: { - type: 'boolean', - label: this.$ts.public, - default: false - } - }); - if (canceled) return; - - const clip = await os.apiWithDialog('clips/create', result); - - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); - } - }, null, ...clips.map(clip => ({ - text: clip.name, - action: () => { - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); - } - }))], this.$refs.menuButton, { - }).then(this.focus); - }, - - async promote() { - const { canceled, result: days } = await os.inputNumber({ - title: this.$ts.numberOfDays, - }); - - if (canceled) return; - - os.apiWithDialog('admin/promo/create', { - noteId: this.appearNote.id, - expiresAt: Date.now() + (86400000 * days) - }); - }, - - share() { - navigator.share({ - title: this.$t('noteOf', { user: this.appearNote.user.name }), - text: this.appearNote.text, - url: `${url}/notes/${this.appearNote.id}` - }); - }, - - focus() { - this.$el.focus(); - }, - - blur() { - this.$el.blur(); - }, - - focusBefore() { - focusPrev(this.$el); - }, - - focusAfter() { - focusNext(this.$el); - }, - - userPage - } -}); -</script> - -<style lang="scss" scoped> -.vfzoeqcg { - position: relative; - contain: content; - - // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 - // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう - // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、 - // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる - // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?) - //content-visibility: auto; - //contain-intrinsic-size: 0 128px; - - &:focus-visible { - outline: none; - } - - &:hover { - background: rgba(0, 0, 0, 0.05); - } - - &:hover, &.operating { - > .article > .main > .footer { - display: block; - } - } - - &.renote { - background: rgba(128, 255, 0, 0.05); - } - - &.highlighted { - background: rgba(255, 128, 0, 0.05); - } - - > .info { - display: flex; - align-items: center; - padding: 12px 16px 4px 16px; - line-height: 24px; - font-size: 85%; - white-space: pre; - color: #d28a3f; - - > i { - margin-right: 4px; - } - - > .hide { - margin-left: 16px; - color: inherit; - opacity: 0.7; - } - } - - > .info + .article { - padding-top: 8px; - } - - > .reply-to { - opacity: 0.7; - padding-bottom: 0; - } - - > .renote { - display: flex; - align-items: center; - padding: 12px 16px 4px 16px; - line-height: 28px; - white-space: pre; - color: var(--renote); - font-size: 0.9em; - - > .avatar { - flex-shrink: 0; - display: inline-block; - width: 28px; - height: 28px; - margin: 0 8px 0 0; - border-radius: 6px; - } - - > i { - margin-right: 4px; - } - - > span { - overflow: hidden; - flex-shrink: 1; - text-overflow: ellipsis; - white-space: nowrap; - - > .name { - font-weight: bold; - } - } - - > .info { - margin-left: 8px; - font-size: 0.9em; - opacity: 0.7; - - > .time { - flex-shrink: 0; - color: inherit; - - > .dropdownIcon { - margin-right: 4px; - } - } - - > .visibility { - margin-left: 8px; - } - - > .localOnly { - margin-left: 8px; - } - } - } - - > .renote + .article { - padding-top: 8px; - } - - > .article { - display: flex; - padding: 12px 16px; - - > .avatar { - flex-shrink: 0; - display: block; - position: sticky; - top: 0; - margin: 0 14px 0 0; - width: 46px; - height: 46px; - } - - > .main { - flex: 1; - min-width: 0; - - > .body { - > .cw { - cursor: default; - display: block; - margin: 0; - padding: 0; - overflow-wrap: break-word; - - > .text { - margin-right: 8px; - } - } - - > .content { - &.collapsed { - position: relative; - max-height: 9em; - overflow: hidden; - - > .fade { - display: block; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); - - > span { - display: inline-block; - background: var(--panel); - padding: 6px 10px; - font-size: 0.8em; - border-radius: 999px; - box-shadow: 0 2px 6px rgb(0 0 0 / 20%); - } - - &:hover { - > span { - background: var(--panelHighlight); - } - } - } - } - - > .text { - overflow-wrap: break-word; - - > .reply { - color: var(--accent); - margin-right: 0.5em; - } - - > .rp { - margin-left: 4px; - font-style: oblique; - color: var(--renote); - } - } - - > .files { - max-width: 500px; - } - - > .url-preview { - margin-top: 8px; - max-width: 500px; - } - - > .poll { - font-size: 80%; - max-width: 500px; - } - - > .renote { - padding: 8px 0; - - > * { - padding: 16px; - border: dashed 1px var(--renote); - border-radius: 8px; - } - } - } - - > .channel { - opacity: 0.7; - font-size: 80%; - } - } - - > .footer { - display: none; - position: absolute; - top: 8px; - right: 8px; - padding: 0 6px; - opacity: 0.7; - - &:hover { - opacity: 1; - } - - > .button { - margin: 0; - padding: 8px; - opacity: 0.7; - - &:hover { - color: var(--accent); - } - - > .count { - display: inline; - margin: 0 0 0 8px; - opacity: 0.7; - } - - &.reacted { - color: var(--accent); - } - } - } - } - } - - > .reply { - border-top: solid 0.5px var(--divider); - } -} - -.muted { - padding: 8px 16px; - opacity: 0.7; - - &:hover { - background: rgba(0, 0, 0, 0.05); - } -} -</style> diff --git a/packages/client/src/ui/chat/notes.vue b/packages/client/src/ui/chat/notes.vue deleted file mode 100644 index 51d4afcf54..0000000000 --- a/packages/client/src/ui/chat/notes.vue +++ /dev/null @@ -1,94 +0,0 @@ -<template> -<div class=""> - <div v-if="empty" class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noNotes }}</div> - </div> - - <MkLoading v-if="fetching"/> - - <MkError v-if="error" @retry="init()"/> - - <div v-show="more && reversed" style="margin-bottom: var(--margin);"> - <MkButton style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore"> - <template v-if="!moreFetching">{{ $ts.loadMore }}</template> - <template v-if="moreFetching"><MkLoading inline/></template> - </MkButton> - </div> - - <XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :ad="true"> - <XNote :key="note._featuredId_ || note._prId_ || note.id" :note="note" @update:note="updated(note, $event)"/> - </XList> - - <div v-show="more && !reversed" style="margin-top: var(--margin);"> - <MkButton v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore"> - <template v-if="!moreFetching">{{ $ts.loadMore }}</template> - <template v-if="moreFetching"><MkLoading inline/></template> - </MkButton> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import paging from '@/scripts/paging'; -import XNote from './note.vue'; -import XList from './date-separated-list.vue'; -import MkButton from '@/components/ui/button.vue'; - -export default defineComponent({ - components: { - XNote, XList, MkButton, - }, - - mixins: [ - paging({ - before: (self) => { - self.$emit('before'); - }, - - after: (self, e) => { - self.$emit('after', e); - } - }), - ], - - props: { - pagination: { - required: true - }, - - prop: { - type: String, - required: false - } - }, - - emits: ['before', 'after'], - - computed: { - notes(): any[] { - return this.prop ? this.items.map(item => item[this.prop]) : this.items; - }, - - reversed(): boolean { - return this.pagination.reversed; - } - }, - - methods: { - updated(oldValue, newValue) { - const i = this.notes.findIndex(n => n === oldValue); - if (this.prop) { - this.items[i][this.prop] = newValue; - } else { - this.items[i] = newValue; - } - }, - - focus() { - this.$refs.notes.focus(); - } - } -}); -</script> diff --git a/packages/client/src/ui/chat/pages/channel.vue b/packages/client/src/ui/chat/pages/channel.vue deleted file mode 100644 index 2755cc92b7..0000000000 --- a/packages/client/src/ui/chat/pages/channel.vue +++ /dev/null @@ -1,259 +0,0 @@ -<template> -<div v-if="channel" class="hhizbblb"> - <div v-if="date" class="info"> - <MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo> - </div> - <div ref="body" class="tl"> - <div v-if="queue > 0" class="new" :style="{ width: width + 'px', bottom: bottom + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div> - <XNotes ref="tl" v-follow="true" class="tl" :pagination="pagination" @queue="queueUpdated"/> - </div> - <div class="bottom"> - <div v-if="typers.length > 0" class="typers"> - <I18n :src="$ts.typingUsers" text-tag="span" class="users"> - <template #users> - <b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b> - </template> - </I18n> - <MkEllipsis/> - </div> - <XPostForm :channel="channel"/> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, markRaw } from 'vue'; -import * as Misskey from 'misskey-js'; -import XNotes from '../notes.vue'; -import * as os from '@/os'; -import { stream } from '@/stream'; -import * as sound from '@/scripts/sound'; -import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; -import follow from '@/directives/follow-append'; -import XPostForm from '../post-form.vue'; -import MkInfo from '@/components/ui/info.vue'; -import * as symbols from '@/symbols'; - -export default defineComponent({ - components: { - XNotes, - XPostForm, - MkInfo, - }, - - directives: { - follow - }, - - provide() { - return { - inChannel: true - }; - }, - - props: { - channelId: { - type: String, - required: true - }, - }, - - data() { - return { - channel: null as Misskey.entities.Channel | null, - connection: null, - pagination: null, - baseQuery: { - includeMyRenotes: this.$store.state.showMyRenotes, - includeRenotedMyNotes: this.$store.state.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.showLocalRenotes - }, - queue: 0, - width: 0, - top: 0, - bottom: 0, - typers: [], - date: null, - [symbols.PAGE_INFO]: computed(() => ({ - title: this.channel ? this.channel.name : '-', - subtitle: this.channel ? this.channel.description : '-', - icon: 'fas fa-satellite-dish', - actions: [{ - icon: this.channel?.isFollowing ? 'fas fa-star' : 'far fa-star', - text: this.channel?.isFollowing ? this.$ts.unfollow : this.$ts.follow, - highlighted: this.channel?.isFollowing, - handler: this.toggleChannelFollow - }, { - icon: 'fas fa-search', - text: this.$ts.inChannelSearch, - handler: this.inChannelSearch - }, { - icon: 'fas fa-calendar-alt', - text: this.$ts.jumpToSpecifiedDate, - handler: this.timetravel - }] - })), - }; - }, - - async created() { - this.channel = await os.api('channels/show', { channelId: this.channelId }); - - const prepend = note => { - (this.$refs.tl as any).prepend(note); - - this.$emit('note'); - - sound.play(note.userId === this.$i.id ? 'noteMy' : 'note'); - }; - - this.connection = markRaw(stream.useChannel('channel', { - channelId: this.channelId - })); - this.connection.on('note', prepend); - this.connection.on('typers', typers => { - this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers; - }); - - this.pagination = { - endpoint: 'channels/timeline', - reversed: true, - limit: 10, - params: init => ({ - channelId: this.channelId, - untilDate: this.date?.getTime(), - ...this.baseQuery - }) - }; - }, - - mounted() { - - }, - - beforeUnmount() { - this.connection.dispose(); - }, - - methods: { - focus() { - this.$refs.body.focus(); - }, - - goTop() { - const container = getScrollContainer(this.$refs.body); - container.scrollTop = 0; - }, - - queueUpdated(q) { - if (this.$refs.body.offsetWidth !== 0) { - const rect = this.$refs.body.getBoundingClientRect(); - this.width = this.$refs.body.offsetWidth; - this.top = rect.top; - this.bottom = this.$refs.body.offsetHeight; - } - this.queue = q; - }, - - async inChannelSearch() { - const { canceled, result: query } = await os.inputText({ - title: this.$ts.inChannelSearch, - }); - if (canceled || query == null || query === '') return; - router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.channelId}`); - }, - - async toggleChannelFollow() { - if (this.channel.isFollowing) { - await os.apiWithDialog('channels/unfollow', { - channelId: this.channel.id - }); - this.channel.isFollowing = false; - } else { - await os.apiWithDialog('channels/follow', { - channelId: this.channel.id - }); - this.channel.isFollowing = true; - } - }, - - openChannelMenu(ev) { - os.popupMenu([{ - text: this.$ts.copyUrl, - icon: 'fas fa-link', - action: () => { - copyToClipboard(`${url}/channels/${this.currentChannel.id}`); - } - }], ev.currentTarget || ev.target); - }, - - timetravel(date?: Date) { - this.date = date; - this.$refs.tl.reload(); - } - } -}); -</script> - -<style lang="scss" scoped> -.hhizbblb { - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - - > .info { - padding: 16px 16px 0 16px; - } - - > .top { - padding: 16px 16px 0 16px; - } - - > .bottom { - padding: 0 16px 16px 16px; - position: relative; - - > .typers { - position: absolute; - bottom: 100%; - padding: 0 8px 0 8px; - font-size: 0.9em; - background: var(--panel); - border-radius: 0 8px 0 0; - color: var(--fgTransparentWeak); - - > .users { - > .user + .user:before { - content: ", "; - font-weight: normal; - } - - > .user:last-of-type:after { - content: " "; - } - } - } - } - - > .tl { - position: relative; - padding: 16px 0; - flex: 1; - min-width: 0; - overflow: auto; - - > .new { - position: fixed; - z-index: 1000; - - > button { - display: block; - margin: 16px auto; - padding: 8px 16px; - border-radius: 32px; - } - } - } -} -</style> diff --git a/packages/client/src/ui/chat/pages/timeline.vue b/packages/client/src/ui/chat/pages/timeline.vue deleted file mode 100644 index f67d333398..0000000000 --- a/packages/client/src/ui/chat/pages/timeline.vue +++ /dev/null @@ -1,222 +0,0 @@ -<template> -<div class="dbiokgaf"> - <div v-if="date" class="info"> - <MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo> - </div> - <div class="top"> - <XPostForm/> - </div> - <div ref="body" class="tl"> - <div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div> - <XNotes ref="tl" class="tl" :pagination="pagination" @queue="queueUpdated"/> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, markRaw } from 'vue'; -import XNotes from '../notes.vue'; -import * as os from '@/os'; -import { stream } from '@/stream'; -import * as sound from '@/scripts/sound'; -import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; -import follow from '@/directives/follow-append'; -import XPostForm from '../post-form.vue'; -import MkInfo from '@/components/ui/info.vue'; -import * as symbols from '@/symbols'; - -export default defineComponent({ - components: { - XNotes, - XPostForm, - MkInfo, - }, - - directives: { - follow - }, - - props: { - src: { - type: String, - required: true - }, - }, - - data() { - return { - connection: null, - connection2: null, - pagination: null, - baseQuery: { - includeMyRenotes: this.$store.state.showMyRenotes, - includeRenotedMyNotes: this.$store.state.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.showLocalRenotes - }, - query: {}, - queue: 0, - width: 0, - top: 0, - bottom: 0, - typers: [], - date: null, - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.timeline, - icon: 'fas fa-home', - actions: [{ - icon: 'fas fa-calendar-alt', - text: this.$ts.jumpToSpecifiedDate, - handler: this.timetravel - }] - })), - }; - }, - - created() { - const prepend = note => { - (this.$refs.tl as any).prepend(note); - - this.$emit('note'); - - sound.play(note.userId === this.$i.id ? 'noteMy' : 'note'); - }; - - const onChangeFollowing = () => { - if (!this.$refs.tl.backed) { - this.$refs.tl.reload(); - } - }; - - let endpoint; - - if (this.src == 'home') { - endpoint = 'notes/timeline'; - this.connection = markRaw(stream.useChannel('homeTimeline')); - this.connection.on('note', prepend); - - this.connection2 = markRaw(stream.useChannel('main')); - this.connection2.on('follow', onChangeFollowing); - this.connection2.on('unfollow', onChangeFollowing); - } else if (this.src == 'local') { - endpoint = 'notes/local-timeline'; - this.connection = markRaw(stream.useChannel('localTimeline')); - this.connection.on('note', prepend); - } else if (this.src == 'social') { - endpoint = 'notes/hybrid-timeline'; - this.connection = markRaw(stream.useChannel('hybridTimeline')); - this.connection.on('note', prepend); - } else if (this.src == 'global') { - endpoint = 'notes/global-timeline'; - this.connection = markRaw(stream.useChannel('globalTimeline')); - this.connection.on('note', prepend); - } - - this.pagination = { - endpoint: endpoint, - limit: 10, - params: init => ({ - untilDate: this.date?.getTime(), - ...this.baseQuery, ...this.query - }) - }; - }, - - mounted() { - - }, - - beforeUnmount() { - this.connection.dispose(); - if (this.connection2) this.connection2.dispose(); - }, - - methods: { - focus() { - this.$refs.body.focus(); - }, - - goTop() { - const container = getScrollContainer(this.$refs.body); - container.scrollTop = 0; - }, - - queueUpdated(q) { - if (this.$refs.body.offsetWidth !== 0) { - const rect = this.$refs.body.getBoundingClientRect(); - this.width = this.$refs.body.offsetWidth; - this.top = rect.top; - this.bottom = this.$refs.body.offsetHeight; - } - this.queue = q; - }, - - timetravel(date?: Date) { - this.date = date; - this.$refs.tl.reload(); - } - } -}); -</script> - -<style lang="scss" scoped> -.dbiokgaf { - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - - > .info { - padding: 16px 16px 0 16px; - } - - > .top { - padding: 16px 16px 0 16px; - } - - > .bottom { - padding: 0 16px 16px 16px; - position: relative; - - > .typers { - position: absolute; - bottom: 100%; - padding: 0 8px 0 8px; - font-size: 0.9em; - background: var(--panel); - border-radius: 0 8px 0 0; - color: var(--fgTransparentWeak); - - > .users { - > .user + .user:before { - content: ", "; - font-weight: normal; - } - - > .user:last-of-type:after { - content: " "; - } - } - } - } - - > .tl { - position: relative; - padding: 16px 0; - flex: 1; - min-width: 0; - overflow: auto; - - > .new { - position: fixed; - z-index: 1000; - - > button { - display: block; - margin: 16px auto; - padding: 8px 16px; - border-radius: 32px; - } - } - } -} -</style> diff --git a/packages/client/src/ui/chat/post-form.vue b/packages/client/src/ui/chat/post-form.vue deleted file mode 100644 index 0f04096653..0000000000 --- a/packages/client/src/ui/chat/post-form.vue +++ /dev/null @@ -1,770 +0,0 @@ -<template> -<div class="pxiwixjf" - @dragover.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" -> - <div class="form"> - <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div> - <div v-if="visibility === 'specified'" class="to-specified"> - <span style="margin-right: 8px;">{{ $ts.recipient }}</span> - <div class="visibleUsers"> - <span v-for="u in visibleUsers" :key="u.id"> - <MkAcct :user="u"/> - <button class="_button" @click="removeVisibleUser(u)"><i class="fas fa-times"></i></button> - </span> - <button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button> - </div> - </div> - <input v-show="useCw" ref="cw" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown"> - <textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> - <XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> - <XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/> - <footer> - <div class="left"> - <button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> - <button v-tooltip="$ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button> - <button v-tooltip="$ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button> - <button v-tooltip="$ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button> - <button v-tooltip="$ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button> - <button v-if="postFormActions.length > 0" v-tooltip="$ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button> - </div> - <div class="right"> - <span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span> - <span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span> - <button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> - <span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span> - <span v-if="visibility === 'home'"><i class="fas fa-home"></i></span> - <span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span> - <span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span> - </button> - <button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button> - </div> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import insertTextAtCursor from 'insert-text-at-cursor'; -import { length } from 'stringz'; -import { toASCII } from 'punycode/'; -import * as mfm from 'mfm-js'; -import { host, url } from '@/config'; -import { erase, unique } from '@/scripts/array'; -import { extractMentions } from '@/scripts/extract-mentions'; -import * as Acct from 'misskey-js/built/acct'; -import { formatTimeString } from '@/scripts/format-time-string'; -import { Autocomplete } from '@/scripts/autocomplete'; -import * as os from '@/os'; -import { stream } from '@/stream'; -import { selectFiles } from '@/scripts/select-file'; -import { notePostInterruptors, postFormActions } from '@/store'; -import { throttle } from 'throttle-debounce'; - -export default defineComponent({ - components: { - XPostFormAttaches: defineAsyncComponent(() => import('@/components/post-form-attaches.vue')), - XPollEditor: defineAsyncComponent(() => import('@/components/poll-editor.vue')) - }, - - props: { - reply: { - type: Object, - required: false - }, - renote: { - type: Object, - required: false - }, - channel: { - type: String, - required: false - }, - mention: { - type: Object, - required: false - }, - specified: { - type: Object, - required: false - }, - initialText: { - type: String, - required: false - }, - initialNote: { - type: Object, - required: false - }, - share: { - type: Boolean, - required: false, - default: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - }, - - emits: ['posted', 'cancel', 'esc'], - - data() { - return { - posting: false, - text: '', - files: [], - poll: null, - useCw: false, - cw: null, - localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly, - visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility, - visibleUsers: [], - autocomplete: null, - draghover: false, - quoteId: null, - recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), - imeText: '', - typing: throttle(3000, () => { - if (this.channel) { - stream.send('typingOnChannel', { channel: this.channel }); - } - }), - postFormActions, - }; - }, - - computed: { - draftKey(): string { - let key = this.channel ? `channel:${this.channel}` : ''; - - if (this.renote) { - key += `renote:${this.renote.id}`; - } else if (this.reply) { - key += `reply:${this.reply.id}`; - } else { - key += 'note'; - } - - return key; - }, - - placeholder(): string { - if (this.renote) { - return this.$ts._postForm.quotePlaceholder; - } else if (this.reply) { - return this.$ts._postForm.replyPlaceholder; - } else if (this.channel) { - return this.$ts._postForm.channelPlaceholder; - } else { - const xs = [ - this.$ts._postForm._placeholders.a, - this.$ts._postForm._placeholders.b, - this.$ts._postForm._placeholders.c, - this.$ts._postForm._placeholders.d, - this.$ts._postForm._placeholders.e, - this.$ts._postForm._placeholders.f - ]; - return xs[Math.floor(Math.random() * xs.length)]; - } - }, - - submitText(): string { - return this.renote - ? this.$ts.quote - : this.reply - ? this.$ts.reply - : this.$ts.note; - }, - - textLength(): number { - return length((this.text + this.imeText).trim()); - }, - - canPost(): boolean { - return !this.posting && - (1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) && - (this.textLength <= this.max) && - (!this.poll || this.poll.choices.length >= 2); - }, - - max(): number { - return this.$instance ? this.$instance.maxNoteTextLength : 1000; - } - }, - - mounted() { - if (this.initialText) { - this.text = this.initialText; - } - - if (this.mention) { - this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`; - this.text += ' '; - } - - if (this.reply && (this.reply.user.username != this.$i.username || (this.reply.user.host != null && this.reply.user.host != host))) { - this.text = `@${this.reply.user.username}${this.reply.user.host != null ? '@' + toASCII(this.reply.user.host) : ''} `; - } - - if (this.reply && this.reply.text != null) { - const ast = mfm.parse(this.reply.text); - - for (const x of extractMentions(ast)) { - const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`; - - // 自分は除外 - if (this.$i.username == x.username && x.host == null) continue; - if (this.$i.username == x.username && x.host == host) continue; - - // 重複は除外 - if (this.text.indexOf(`${mention} `) != -1) continue; - - this.text += `${mention} `; - } - } - - if (this.channel) { - this.visibility = 'public'; - this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す - } - - // 公開以外へのリプライ時は元の公開範囲を引き継ぐ - if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) { - this.visibility = this.reply.visibility; - if (this.reply.visibility === 'specified') { - os.api('users/show', { - userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId) - }).then(users => { - this.visibleUsers.push(...users); - }); - - if (this.reply.userId !== this.$i.id) { - os.api('users/show', { userId: this.reply.userId }).then(user => { - this.visibleUsers.push(user); - }); - } - } - } - - if (this.specified) { - this.visibility = 'specified'; - this.visibleUsers.push(this.specified); - } - - // keep cw when reply - if (this.$store.state.keepCw && this.reply && this.reply.cw) { - this.useCw = true; - this.cw = this.reply.cw; - } - - if (this.autofocus) { - this.focus(); - - this.$nextTick(() => { - this.focus(); - }); - } - - // TODO: detach when unmount - new Autocomplete(this.$refs.text, this, { model: 'text' }); - new Autocomplete(this.$refs.cw, this, { model: 'cw' }); - - this.$nextTick(() => { - // 書きかけの投稿を復元 - if (!this.share && !this.mention && !this.specified) { - const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; - if (draft) { - this.text = draft.data.text; - this.useCw = draft.data.useCw; - this.cw = draft.data.cw; - this.visibility = draft.data.visibility; - this.localOnly = draft.data.localOnly; - this.files = (draft.data.files || []).filter(e => e); - if (draft.data.poll) { - this.poll = draft.data.poll; - } - } - } - - // 削除して編集 - if (this.initialNote) { - const init = this.initialNote; - this.text = init.text ? init.text : ''; - this.files = init.files; - this.cw = init.cw; - this.useCw = init.cw != null; - if (init.poll) { - this.poll = init.poll; - } - this.visibility = init.visibility; - this.localOnly = init.localOnly; - this.quoteId = init.renote ? init.renote.id : null; - } - - this.$nextTick(() => this.watch()); - }); - }, - - methods: { - watch() { - this.$watch('text', () => this.saveDraft()); - this.$watch('useCw', () => this.saveDraft()); - this.$watch('cw', () => this.saveDraft()); - this.$watch('poll', () => this.saveDraft()); - this.$watch('files', () => this.saveDraft(), { deep: true }); - this.$watch('visibility', () => this.saveDraft()); - this.$watch('localOnly', () => this.saveDraft()); - }, - - togglePoll() { - if (this.poll) { - this.poll = null; - } else { - this.poll = { - choices: ['', ''], - multiple: false, - expiresAt: null, - expiredAfter: null, - }; - } - }, - - addTag(tag: string) { - insertTextAtCursor(this.$refs.text, ` #${tag} `); - }, - - focus() { - (this.$refs.text as any).focus(); - }, - - chooseFileFrom(ev) { - selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => { - for (const file of files) { - this.files.push(file); - } - }); - }, - - detachFile(id) { - this.files = this.files.filter(x => x.id != id); - }, - - updateFiles(files) { - this.files = files; - }, - - updateFileSensitive(file, sensitive) { - this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive; - }, - - updateFileName(file, name) { - this.files[this.files.findIndex(x => x.id === file.id)].name = name; - }, - - upload(file: File, name?: string) { - os.upload(file, this.$store.state.uploadFolder, name).then(res => { - this.files.push(res); - }); - }, - - onPollUpdate(poll) { - this.poll = poll; - this.saveDraft(); - }, - - setVisibility() { - if (this.channel) { - // TODO: information dialog - return; - } - - os.popup(import('@/components/visibility-picker.vue'), { - currentVisibility: this.visibility, - currentLocalOnly: this.localOnly, - src: this.$refs.visibilityButton - }, { - changeVisibility: visibility => { - this.visibility = visibility; - if (this.$store.state.rememberNoteVisibility) { - this.$store.set('visibility', visibility); - } - }, - changeLocalOnly: localOnly => { - this.localOnly = localOnly; - if (this.$store.state.rememberNoteVisibility) { - this.$store.set('localOnly', localOnly); - } - } - }, 'closed'); - }, - - addVisibleUser() { - os.selectUser().then(user => { - this.visibleUsers.push(user); - }); - }, - - removeVisibleUser(user) { - this.visibleUsers = erase(user, this.visibleUsers); - }, - - clear() { - this.text = ''; - this.files = []; - this.poll = null; - this.quoteId = null; - }, - - onKeydown(e: KeyboardEvent) { - if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); - if (e.which === 27) this.$emit('esc'); - this.typing(); - }, - - onCompositionUpdate(e: CompositionEvent) { - this.imeText = e.data; - this.typing(); - }, - - onCompositionEnd(e: CompositionEvent) { - this.imeText = ''; - }, - - async onPaste(e: ClipboardEvent) { - for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) { - if (item.kind == 'file') { - const file = item.getAsFile(); - const lio = file.name.lastIndexOf('.'); - const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; - this.upload(file, formatted); - } - } - - const paste = e.clipboardData.getData('text'); - - if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) { - e.preventDefault(); - - os.confirm({ - type: 'info', - text: this.$ts.quoteQuestion, - }).then(({ canceled }) => { - if (canceled) { - insertTextAtCursor(this.$refs.text, paste); - return; - } - - this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; - }); - } - }, - - onDragover(e) { - if (!e.dataTransfer.items[0]) return; - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - if (isFile || isDriveFile) { - e.preventDefault(); - this.draghover = true; - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, - - onDragenter(e) { - this.draghover = true; - }, - - onDragleave(e) { - this.draghover = false; - }, - - onDrop(e): void { - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.preventDefault(); - for (const x of Array.from(e.dataTransfer.files)) this.upload(x); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.files.push(file); - e.preventDefault(); - } - //#endregion - }, - - saveDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - data[this.draftKey] = { - updatedAt: new Date(), - data: { - text: this.text, - useCw: this.useCw, - cw: this.cw, - visibility: this.visibility, - localOnly: this.localOnly, - files: this.files, - poll: this.poll - } - }; - - localStorage.setItem('drafts', JSON.stringify(data)); - }, - - deleteDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - delete data[this.draftKey]; - - localStorage.setItem('drafts', JSON.stringify(data)); - }, - - async post() { - let data = { - text: this.text == '' ? undefined : this.text, - fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, - replyId: this.reply ? this.reply.id : undefined, - renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined, - channelId: this.channel ? this.channel : undefined, - poll: this.poll, - cw: this.useCw ? this.cw || '' : undefined, - localOnly: this.localOnly, - visibility: this.visibility, - visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, - }; - - // plugin - if (notePostInterruptors.length > 0) { - for (const interruptor of notePostInterruptors) { - data = await interruptor.handler(JSON.parse(JSON.stringify(data))); - } - } - - this.posting = true; - os.api('notes/create', data).then(() => { - this.clear(); - this.$nextTick(() => { - this.deleteDraft(); - this.$emit('posted'); - if (this.text && this.text != '') { - const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); - const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; - localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); - } - this.posting = false; - }); - }).catch(err => { - this.posting = false; - os.alert({ - type: 'error', - text: err.message + '\n' + (err as any).id, - }); - }); - }, - - cancel() { - this.$emit('cancel'); - }, - - insertMention() { - os.selectUser().then(user => { - insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' '); - }); - }, - - async insertEmoji(ev) { - os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); - }, - - showActions(ev) { - os.popupMenu(postFormActions.map(action => ({ - text: action.title, - action: () => { - action.handler({ - text: this.text - }, (key, value) => { - if (key === 'text') { this.text = value; } - }); - } - })), ev.currentTarget || ev.target); - } - } -}); -</script> - -<style lang="scss" scoped> -.pxiwixjf { - position: relative; - border: solid 0.5px var(--divider); - border-radius: 8px; - - > .form { - > .preview { - padding: 16px; - } - - > .with-quote { - margin: 0 0 8px 0; - color: var(--accent); - - > button { - padding: 4px 8px; - color: var(--accentAlpha04); - - &:hover { - color: var(--accentAlpha06); - } - - &:active { - color: var(--accentDarken30); - } - } - } - - > .to-specified { - padding: 6px 24px; - margin-bottom: 8px; - overflow: auto; - white-space: nowrap; - - > .visibleUsers { - display: inline; - top: -1px; - font-size: 14px; - - > button { - padding: 4px; - border-radius: 8px; - } - - > span { - margin-right: 14px; - padding: 8px 0 8px 8px; - border-radius: 8px; - background: var(--X4); - - > button { - padding: 4px 8px; - } - } - } - } - - > .cw, - > .text { - display: block; - box-sizing: border-box; - padding: 16px; - margin: 0; - width: 100%; - font-size: 16px; - border: none; - border-radius: 0; - background: transparent; - color: var(--fg); - font-family: inherit; - - &:focus { - outline: none; - } - - &:disabled { - opacity: 0.5; - } - } - - > .cw { - z-index: 1; - padding-bottom: 8px; - border-bottom: solid 0.5px var(--divider); - } - - > .text { - max-width: 100%; - min-width: 100%; - min-height: 60px; - - &.withCw { - padding-top: 8px; - } - } - - > footer { - $height: 44px; - display: flex; - padding: 0 8px 8px 8px; - line-height: $height; - - > .left { - > button { - display: inline-block; - padding: 0; - margin: 0; - font-size: 16px; - width: $height; - height: $height; - border-radius: 6px; - - &:hover { - background: var(--X5); - } - - &.active { - color: var(--accent); - } - } - } - - > .right { - margin-left: auto; - - > .text-count { - opacity: 0.7; - } - - > .visibility { - width: $height; - margin: 0 8px; - - & + .localOnly { - margin-left: 0 !important; - } - } - - > .local-only { - margin: 0 0 0 12px; - opacity: 0.7; - } - - > .submit { - margin: 0; - padding: 0 12px; - line-height: 34px; - font-weight: bold; - border-radius: 4px; - - &:disabled { - opacity: 0.7; - } - - > i { - margin-left: 6px; - } - } - } - } - } -} -</style> diff --git a/packages/client/src/ui/chat/side.vue b/packages/client/src/ui/chat/side.vue deleted file mode 100644 index 548a46102b..0000000000 --- a/packages/client/src/ui/chat/side.vue +++ /dev/null @@ -1,157 +0,0 @@ -<template> -<div v-if="component" class="mrajymqm _narrow_"> - <header class="header" @contextmenu.prevent.stop="onContextmenu"> - <MkHeader class="title" :info="pageInfo" :center="false"/> - </header> - <component :is="component" v-bind="props" :ref="changePage" class="body"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { resolve } from '@/router'; -import { url } from '@/config'; -import * as symbols from '@/symbols'; - -export default defineComponent({ - components: { - }, - - provide() { - return { - navHook: (path) => { - this.navigate(path); - } - }; - }, - - data() { - return { - path: null, - component: null, - props: {}, - pageInfo: null, - history: [], - }; - }, - - computed: { - url(): string { - return url + this.path; - } - }, - - methods: { - changePage(page) { - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - this.pageInfo = page[symbols.PAGE_INFO]; - } - }, - - navigate(path, record = true) { - if (record && this.path) this.history.push(this.path); - this.path = path; - const { component, props } = resolve(path); - this.component = component; - this.props = props; - this.$emit('open'); - }, - - back() { - this.navigate(this.history.pop(), false); - }, - - close() { - this.path = null; - this.component = null; - this.props = {}; - this.$emit('close'); - }, - - onContextmenu(e) { - os.contextMenu([{ - type: 'label', - text: this.path, - }, { - icon: 'fas fa-expand-alt', - text: this.$ts.showInPage, - action: () => { - this.$router.push(this.path); - this.close(); - } - }, { - icon: 'fas fa-window-maximize', - text: this.$ts.openInWindow, - action: () => { - os.pageWindow(this.path); - this.close(); - } - }, null, { - icon: 'fas fa-external-link-alt', - text: this.$ts.openInNewTab, - action: () => { - window.open(this.url, '_blank'); - this.close(); - } - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: () => { - copyToClipboard(this.url); - } - }], e); - } - } -}); -</script> - -<style lang="scss" scoped> -.mrajymqm { - $header-height: 54px; // TODO: どこかに集約したい - - --root-margin: 16px; - --margin: var(--marginHalf); - - height: 100%; - overflow: auto; - box-sizing: border-box; - - > .header { - display: flex; - position: sticky; - z-index: 1000; - top: 0; - height: $header-height; - width: 100%; - font-weight: bold; - //background-color: var(--panel); - -webkit-backdrop-filter: var(--blur, blur(32px)); - backdrop-filter: var(--blur, blur(32px)); - background-color: var(--header); - border-bottom: solid 0.5px var(--divider); - box-sizing: border-box; - - > ._button { - height: $header-height; - width: $header-height; - - &:hover { - color: var(--fgHighlighted); - } - } - - > .title { - flex: 1; - position: relative; - } - } - - > .body { - - } -} -</style> - diff --git a/packages/client/src/ui/chat/store.ts b/packages/client/src/ui/chat/store.ts deleted file mode 100644 index 389d56afb6..0000000000 --- a/packages/client/src/ui/chat/store.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { markRaw } from 'vue'; -import { Storage } from '../../pizzax'; - -export const store = markRaw(new Storage('chatUi', { - widgets: { - where: 'account', - default: [] as { - name: string; - id: string; - data: Record<string, any>; - }[] - }, - tl: { - where: 'deviceAccount', - default: 'home' - }, -})); diff --git a/packages/client/src/ui/chat/sub-note-content.vue b/packages/client/src/ui/chat/sub-note-content.vue deleted file mode 100644 index a85096ebc9..0000000000 --- a/packages/client/src/ui/chat/sub-note-content.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="wrmlmaau"> - <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span> - <span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span> - <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> - <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> - </div> - <details v-if="note.files.length > 0"> - <summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary> - <XMediaList :media-list="note.files"/> - </details> - <details v-if="note.poll"> - <summary>{{ $ts.poll }}</summary> - <XPoll :note="note"/> - </details> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XPoll from '@/components/poll.vue'; -import XMediaList from '@/components/media-list.vue'; -import * as os from '@/os'; - -export default defineComponent({ - components: { - XPoll, - XMediaList, - }, - props: { - note: { - type: Object, - required: true - } - }, - data() { - return { - }; - } -}); -</script> - -<style lang="scss" scoped> -.wrmlmaau { - overflow-wrap: break-word; - - > .body { - > .reply { - margin-right: 6px; - color: var(--accent); - } - - > .rp { - margin-left: 4px; - font-style: oblique; - color: var(--renote); - } - } -} -</style> diff --git a/packages/client/src/ui/chat/widgets.vue b/packages/client/src/ui/chat/widgets.vue deleted file mode 100644 index 337d5a7b58..0000000000 --- a/packages/client/src/ui/chat/widgets.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="qydbhufi"> - <XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> - - <button v-if="edit" class="_textButton" style="font-size: 0.9em;" @click="edit = false">{{ $ts.editWidgetsExit }}</button> - <button v-else class="_textButton" style="font-size: 0.9em;" @click="edit = true">{{ $ts.editWidgets }}</button> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import XWidgets from '@/components/widgets.vue'; -import { store } from './store'; - -export default defineComponent({ - components: { - XWidgets, - }, - - data() { - return { - edit: false, - widgets: store.reactiveState.widgets - }; - }, - - methods: { - addWidget(widget) { - store.set('widgets', [widget, ...store.state.widgets]); - }, - - removeWidget(widget) { - store.set('widgets', store.state.widgets.filter(w => w.id != widget.id)); - }, - - updateWidget({ id, data }) { - // TODO: throttleしたい - store.set('widgets', store.state.widgets.map(w => w.id === id ? { - ...w, - data: data - } : w)); - }, - - updateWidgets(widgets) { - store.set('widgets', widgets); - } - } -}); -</script> - -<style lang="scss" scoped> -.qydbhufi { - height: 100%; - box-sizing: border-box; - overflow: auto; - padding: var(--margin); - - ::v-deep(._panel) { - box-shadow: none; - } -} -</style> |