summaryrefslogtreecommitdiff
path: root/packages/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-01-09 18:50:03 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-01-09 18:50:03 +0900
commita10be38d0ee0e01e278422d58e2f2df7e20d3c40 (patch)
treefed75fd26e36bcb68f92c50ed716bf64bd750949 /packages/client
parentfix (diff)
downloadsharkey-a10be38d0ee0e01e278422d58e2f2df7e20d3c40.tar.gz
sharkey-a10be38d0ee0e01e278422d58e2f2df7e20d3c40.tar.bz2
sharkey-a10be38d0ee0e01e278422d58e2f2df7e20d3c40.zip
bye chat ui
Diffstat (limited to 'packages/client')
-rw-r--r--packages/client/src/init.ts1
-rw-r--r--packages/client/src/menu.ts7
-rw-r--r--packages/client/src/ui/chat/date-separated-list.vue157
-rw-r--r--packages/client/src/ui/chat/header-clock.vue62
-rw-r--r--packages/client/src/ui/chat/index.vue463
-rw-r--r--packages/client/src/ui/chat/note-header.vue99
-rw-r--r--packages/client/src/ui/chat/note-preview.vue112
-rw-r--r--packages/client/src/ui/chat/note.sub.vue137
-rw-r--r--packages/client/src/ui/chat/note.vue1143
-rw-r--r--packages/client/src/ui/chat/notes.vue94
-rw-r--r--packages/client/src/ui/chat/pages/channel.vue259
-rw-r--r--packages/client/src/ui/chat/pages/timeline.vue222
-rw-r--r--packages/client/src/ui/chat/post-form.vue770
-rw-r--r--packages/client/src/ui/chat/side.vue157
-rw-r--r--packages/client/src/ui/chat/store.ts17
-rw-r--r--packages/client/src/ui/chat/sub-note-content.vue62
-rw-r--r--packages/client/src/ui/chat/widgets.vue62
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>