Improve chat UI (wip)
This commit is contained in:
parent
c03e2febb0
commit
f565c5f730
|
@ -4,85 +4,114 @@ import MkLoading from '@client/pages/_loading_.vue';
|
||||||
import MkError from '@client/pages/_error_.vue';
|
import MkError from '@client/pages/_error_.vue';
|
||||||
import MkTimeline from '@client/pages/timeline.vue';
|
import MkTimeline from '@client/pages/timeline.vue';
|
||||||
import { $i } from './account';
|
import { $i } from './account';
|
||||||
|
import { ui } from '@client/config';
|
||||||
|
|
||||||
const page = (path: string) => defineAsyncComponent({
|
const page = (path: string, ui?: string) => defineAsyncComponent({
|
||||||
loader: () => import(`./pages/${path}.vue`),
|
loader: ui ? () => import(`./ui/${ui}/pages/${path}.vue`) : () => import(`./pages/${path}.vue`),
|
||||||
loadingComponent: MkLoading,
|
loadingComponent: MkLoading,
|
||||||
errorComponent: MkError,
|
errorComponent: MkError,
|
||||||
});
|
});
|
||||||
|
|
||||||
let indexScrollPos = 0;
|
let indexScrollPos = 0;
|
||||||
|
|
||||||
|
const defaultRoutes = [
|
||||||
|
// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
|
||||||
|
{ path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') },
|
||||||
|
{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
|
||||||
|
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
|
||||||
|
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
|
||||||
|
{ path: '/@:acct/room', props: true, component: page('room/room') },
|
||||||
|
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
|
||||||
|
{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
|
||||||
|
{ path: '/announcements', component: page('announcements') },
|
||||||
|
{ path: '/about', component: page('about') },
|
||||||
|
{ path: '/about-misskey', component: page('about-misskey') },
|
||||||
|
{ path: '/featured', component: page('featured') },
|
||||||
|
{ path: '/docs', component: page('docs') },
|
||||||
|
{ path: '/theme-editor', component: page('theme-editor') },
|
||||||
|
{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
|
||||||
|
{ path: '/docs/:doc(.*)', component: page('doc'), props: route => ({ doc: route.params.doc }) },
|
||||||
|
{ path: '/explore', component: page('explore') },
|
||||||
|
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
|
||||||
|
{ path: '/federation', component: page('federation') },
|
||||||
|
{ path: '/emojis', component: page('emojis') },
|
||||||
|
{ path: '/search', component: page('search') },
|
||||||
|
{ path: '/pages', name: 'pages', component: page('pages') },
|
||||||
|
{ path: '/pages/new', component: page('page-editor/page-editor') },
|
||||||
|
{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
|
||||||
|
{ path: '/gallery', component: page('gallery/index') },
|
||||||
|
{ path: '/gallery/new', component: page('gallery/edit') },
|
||||||
|
{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
|
||||||
|
{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
|
||||||
|
{ path: '/channels', component: page('channels') },
|
||||||
|
{ path: '/channels/new', component: page('channel-editor') },
|
||||||
|
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
|
||||||
|
{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
|
||||||
|
{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
|
||||||
|
{ path: '/my/notifications', component: page('notifications') },
|
||||||
|
{ path: '/my/favorites', component: page('favorites') },
|
||||||
|
{ path: '/my/messages', component: page('messages') },
|
||||||
|
{ path: '/my/mentions', component: page('mentions') },
|
||||||
|
{ path: '/my/messaging', name: 'messaging', component: page('messaging/index') },
|
||||||
|
{ path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) },
|
||||||
|
{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
|
||||||
|
{ path: '/my/drive', name: 'drive', component: page('drive') },
|
||||||
|
{ path: '/my/drive/folder/:folder', component: page('drive') },
|
||||||
|
{ path: '/my/follow-requests', component: page('follow-requests') },
|
||||||
|
{ path: '/my/lists', component: page('my-lists/index') },
|
||||||
|
{ path: '/my/lists/:list', component: page('my-lists/list') },
|
||||||
|
{ path: '/my/groups', component: page('my-groups/index') },
|
||||||
|
{ path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) },
|
||||||
|
{ path: '/my/antennas', component: page('my-antennas/index') },
|
||||||
|
{ path: '/my/antennas/create', component: page('my-antennas/create') },
|
||||||
|
{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
|
||||||
|
{ path: '/my/clips', component: page('my-clips/index') },
|
||||||
|
{ path: '/scratchpad', component: page('scratchpad') },
|
||||||
|
{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
|
||||||
|
{ path: '/instance', component: page('instance/index') },
|
||||||
|
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
|
||||||
|
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
|
||||||
|
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
|
||||||
|
{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) },
|
||||||
|
{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
|
||||||
|
{ path: '/games/reversi', component: page('reversi/index') },
|
||||||
|
{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
|
||||||
|
{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
|
||||||
|
{ path: '/api-console', component: page('api-console') },
|
||||||
|
{ path: '/preview', component: page('preview') },
|
||||||
|
{ path: '/test', component: page('test') },
|
||||||
|
{ path: '/auth/:token', component: page('auth') },
|
||||||
|
{ path: '/miauth/:session', component: page('miauth') },
|
||||||
|
{ path: '/authorize-follow', component: page('follow') },
|
||||||
|
{ path: '/share', component: page('share') },
|
||||||
|
{ path: '/:catchAll(.*)', component: page('not-found') }
|
||||||
|
];
|
||||||
|
|
||||||
|
const chatRoutes = [
|
||||||
|
{ path: '/timeline', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
|
||||||
|
{ path: '/timeline/home', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
|
||||||
|
{ path: '/timeline/local', component: page('timeline', 'chat'), props: route => ({ src: 'local' }) },
|
||||||
|
{ path: '/timeline/social', component: page('timeline', 'chat'), props: route => ({ src: 'social' }) },
|
||||||
|
{ path: '/timeline/global', component: page('timeline', 'chat'), props: route => ({ src: 'global' }) },
|
||||||
|
{ path: '/channels/:channelId', component: page('channel', 'chat'), props: route => ({ channelId: route.params.channelId }) },
|
||||||
|
];
|
||||||
|
|
||||||
|
function margeRoutes(routes: any[]) {
|
||||||
|
const result = defaultRoutes;
|
||||||
|
for (const route of routes) {
|
||||||
|
const found = result.findIndex(x => x.path === route.path);
|
||||||
|
if (found > -1) {
|
||||||
|
result[found] = route;
|
||||||
|
} else {
|
||||||
|
result.unshift(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: margeRoutes(ui === 'chat' ? chatRoutes : []),
|
||||||
// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
|
|
||||||
{ path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') },
|
|
||||||
{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
|
|
||||||
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
|
|
||||||
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
|
|
||||||
{ path: '/@:acct/room', props: true, component: page('room/room') },
|
|
||||||
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
|
|
||||||
{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
|
|
||||||
{ path: '/announcements', component: page('announcements') },
|
|
||||||
{ path: '/about', component: page('about') },
|
|
||||||
{ path: '/about-misskey', component: page('about-misskey') },
|
|
||||||
{ path: '/featured', component: page('featured') },
|
|
||||||
{ path: '/docs', component: page('docs') },
|
|
||||||
{ path: '/theme-editor', component: page('theme-editor') },
|
|
||||||
{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
|
|
||||||
{ path: '/docs/:doc(.*)', component: page('doc'), props: route => ({ doc: route.params.doc }) },
|
|
||||||
{ path: '/explore', component: page('explore') },
|
|
||||||
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
|
|
||||||
{ path: '/search', component: page('search') },
|
|
||||||
{ path: '/pages', name: 'pages', component: page('pages') },
|
|
||||||
{ path: '/pages/new', component: page('page-editor/page-editor') },
|
|
||||||
{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
|
|
||||||
{ path: '/gallery', component: page('gallery/index') },
|
|
||||||
{ path: '/gallery/new', component: page('gallery/edit') },
|
|
||||||
{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
|
|
||||||
{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
|
|
||||||
{ path: '/channels', component: page('channels') },
|
|
||||||
{ path: '/channels/new', component: page('channel-editor') },
|
|
||||||
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
|
|
||||||
{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
|
|
||||||
{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
|
|
||||||
{ path: '/my/notifications', component: page('notifications') },
|
|
||||||
{ path: '/my/favorites', component: page('favorites') },
|
|
||||||
{ path: '/my/messages', component: page('messages') },
|
|
||||||
{ path: '/my/mentions', component: page('mentions') },
|
|
||||||
{ path: '/my/messaging', name: 'messaging', component: page('messaging/index') },
|
|
||||||
{ path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) },
|
|
||||||
{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
|
|
||||||
{ path: '/my/drive', name: 'drive', component: page('drive') },
|
|
||||||
{ path: '/my/drive/folder/:folder', component: page('drive') },
|
|
||||||
{ path: '/my/follow-requests', component: page('follow-requests') },
|
|
||||||
{ path: '/my/lists', component: page('my-lists/index') },
|
|
||||||
{ path: '/my/lists/:list', component: page('my-lists/list') },
|
|
||||||
{ path: '/my/groups', component: page('my-groups/index') },
|
|
||||||
{ path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) },
|
|
||||||
{ path: '/my/antennas', component: page('my-antennas/index') },
|
|
||||||
{ path: '/my/clips', component: page('my-clips/index') },
|
|
||||||
{ path: '/scratchpad', component: page('scratchpad') },
|
|
||||||
{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
|
|
||||||
{ path: '/instance', component: page('instance/index') },
|
|
||||||
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
|
|
||||||
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
|
|
||||||
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
|
|
||||||
{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) },
|
|
||||||
{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
|
|
||||||
{ path: '/games/reversi', component: page('reversi/index') },
|
|
||||||
{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
|
|
||||||
{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
|
|
||||||
{ path: '/api-console', component: page('api-console') },
|
|
||||||
{ path: '/preview', component: page('preview') },
|
|
||||||
{ path: '/test', component: page('test') },
|
|
||||||
{ path: '/auth/:token', component: page('auth') },
|
|
||||||
{ path: '/miauth/:session', component: page('miauth') },
|
|
||||||
{ path: '/authorize-follow', component: page('follow') },
|
|
||||||
{ path: '/share', component: page('share') },
|
|
||||||
{ path: '/:catchAll(.*)', component: page('not-found') }
|
|
||||||
],
|
|
||||||
// なんかHacky
|
// なんかHacky
|
||||||
// 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする
|
// 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする
|
||||||
scrollBehavior(to) {
|
scrollBehavior(to) {
|
||||||
|
|
|
@ -73,54 +73,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="main" @contextmenu.stop="onContextmenu">
|
<main class="main" @contextmenu.stop="onContextmenu">
|
||||||
<header class="header" ref="header" @click="onHeaderClick">
|
<header class="header">
|
||||||
<div class="left">
|
<XHeader class="header" :info="pageInfo" :menu="menu" :center="false" :back-button="true" @back="back()" @click="onHeaderClick"/>
|
||||||
<template v-if="tl === 'home'">
|
|
||||||
<i class="fas fa-home icon"></i>
|
|
||||||
<div class="title">{{ $ts._timelines.home }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="tl === 'local'">
|
|
||||||
<i class="fas fa-comments icon"></i>
|
|
||||||
<div class="title">{{ $ts._timelines.local }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="tl === 'social'">
|
|
||||||
<i class="fas fa-share-alt icon"></i>
|
|
||||||
<div class="title">{{ $ts._timelines.social }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="tl === 'global'">
|
|
||||||
<i class="fas fa-globe icon"></i>
|
|
||||||
<div class="title">{{ $ts._timelines.global }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="tl.startsWith('channel:')">
|
|
||||||
<i class="fas fa-satellite-dish icon"></i>
|
|
||||||
<div class="title" v-if="currentChannel">{{ currentChannel.name }}<div class="description">{{ currentChannel.description }}</div></div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right">
|
|
||||||
<div class="instance">{{ instanceName }}</div>
|
|
||||||
<XHeaderClock class="clock"/>
|
|
||||||
<button class="_button button timetravel" @click="timetravel" v-tooltip="$ts.jumpToSpecifiedDate">
|
|
||||||
<i class="fas fa-calendar-alt"></i>
|
|
||||||
</button>
|
|
||||||
<button class="_button button search" v-if="tl.startsWith('channel:') && currentChannel" @click="inChannelSearch" v-tooltip="$ts.inChannelSearch">
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</button>
|
|
||||||
<button class="_button button search" v-else @click="search" v-tooltip="$ts.search">
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</button>
|
|
||||||
<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow" v-tooltip="currentChannel.isFollowing ? $ts.unfollow : $ts.follow">
|
|
||||||
<i v-if="currentChannel.isFollowing" class="fas fa-star"></i>
|
|
||||||
<i v-else class="far fa-star"></i>
|
|
||||||
</button>
|
|
||||||
<button class="_button button menu" v-if="tl.startsWith('channel:') && currentChannel" @click="openChannelMenu">
|
|
||||||
<i class="fas fa-ellipsis-h"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
<XTimeline class="body" ref="tl" v-if="tl.startsWith('channel:')" src="channel" :key="tl" :channel="tl.replace('channel:', '')"/>
|
<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
|
||||||
<XTimeline class="body" ref="tl" v-else :src="tl" :key="tl"/>
|
<keep-alive :include="['timeline']">
|
||||||
|
<component :is="Component" :ref="changePage" class="body"/>
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
|
<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
|
||||||
|
@ -139,7 +101,7 @@ import XSidebar from '@client/ui/_common_/sidebar.vue';
|
||||||
import XWidgets from './widgets.vue';
|
import XWidgets from './widgets.vue';
|
||||||
import XCommon from '../_common_/common.vue';
|
import XCommon from '../_common_/common.vue';
|
||||||
import XSide from './side.vue';
|
import XSide from './side.vue';
|
||||||
import XTimeline from './timeline.vue';
|
import XHeader from '../_common_/header.vue';
|
||||||
import XHeaderClock from './header-clock.vue';
|
import XHeaderClock from './header-clock.vue';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
import { router } from '@client/router';
|
import { router } from '@client/router';
|
||||||
|
@ -147,6 +109,7 @@ import { menuDef } from '@client/menu';
|
||||||
import { search } from '@client/scripts/search';
|
import { search } from '@client/scripts/search';
|
||||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -154,29 +117,12 @@ export default defineComponent({
|
||||||
XSidebar,
|
XSidebar,
|
||||||
XWidgets,
|
XWidgets,
|
||||||
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
||||||
XTimeline,
|
XHeader,
|
||||||
XHeaderClock,
|
XHeaderClock,
|
||||||
},
|
},
|
||||||
|
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
navHook: (path) => {
|
|
||||||
switch (path) {
|
|
||||||
case '/timeline/home': this.tl = 'home'; return;
|
|
||||||
case '/timeline/local': this.tl = 'local'; return;
|
|
||||||
case '/timeline/social': this.tl = 'social'; return;
|
|
||||||
case '/timeline/global': this.tl = 'global'; return;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (path.startsWith('/channels/')) {
|
|
||||||
this.tl = `channel:${ path.replace('/channels/', '') }`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//os.pageWindow(path);
|
|
||||||
this.$refs.side.navigate(path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sideViewHook: (path) => {
|
sideViewHook: (path) => {
|
||||||
this.$refs.side.navigate(path);
|
this.$refs.side.navigate(path);
|
||||||
}
|
}
|
||||||
|
@ -185,7 +131,7 @@ export default defineComponent({
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tl: store.state.tl,
|
pageInfo: null,
|
||||||
lists: null,
|
lists: null,
|
||||||
antennas: null,
|
antennas: null,
|
||||||
followedChannels: null,
|
followedChannels: null,
|
||||||
|
@ -197,18 +143,30 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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() {
|
created() {
|
||||||
if (window.innerWidth < 1024) {
|
if (window.innerWidth < 1024) {
|
||||||
localStorage.setItem('ui', 'default');
|
localStorage.setItem('ui', 'default');
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
router.beforeEach((to, from) => {
|
|
||||||
this.$refs.side.navigate(to.fullPath);
|
|
||||||
// search?q=foo のようなクエリを受け取れるようにするため、return falseはできない
|
|
||||||
//return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
os.api('users/lists/list').then(lists => {
|
os.api('users/lists/list').then(lists => {
|
||||||
this.lists = lists;
|
this.lists = lists;
|
||||||
});
|
});
|
||||||
|
@ -225,18 +183,22 @@ export default defineComponent({
|
||||||
os.api('channels/featured', { limit: 20 }).then(channels => {
|
os.api('channels/featured', { limit: 20 }).then(channels => {
|
||||||
this.featuredChannels = channels;
|
this.featuredChannels = channels;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$watch('tl', () => {
|
|
||||||
if (this.tl.startsWith('channel:')) {
|
|
||||||
os.api('channels/show', { channelId: this.tl.replace('channel:', '') }).then(channel => {
|
|
||||||
this.currentChannel = channel;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
store.set('tl', this.tl);
|
|
||||||
}, { immediate: true });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTransition() {
|
||||||
|
if (window._scroll) window._scroll();
|
||||||
|
},
|
||||||
|
|
||||||
showMenu() {
|
showMenu() {
|
||||||
this.$refs.menu.show();
|
this.$refs.menu.show();
|
||||||
},
|
},
|
||||||
|
@ -245,59 +207,18 @@ export default defineComponent({
|
||||||
os.post();
|
os.post();
|
||||||
},
|
},
|
||||||
|
|
||||||
async timetravel() {
|
|
||||||
const { canceled, result: date } = await os.dialog({
|
|
||||||
title: this.$ts.date,
|
|
||||||
input: {
|
|
||||||
type: 'date'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
this.$refs.tl.timetravel(new Date(date));
|
|
||||||
},
|
|
||||||
|
|
||||||
search() {
|
search() {
|
||||||
search();
|
search();
|
||||||
},
|
},
|
||||||
|
|
||||||
async inChannelSearch() {
|
back() {
|
||||||
const { canceled, result: query } = await os.dialog({
|
history.back();
|
||||||
title: this.$ts.inChannelSearch,
|
|
||||||
input: true
|
|
||||||
});
|
|
||||||
if (canceled || query == null || query === '') return;
|
|
||||||
router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.currentChannel.id}`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
top() {
|
top() {
|
||||||
window.scroll({ top: 0, behavior: 'smooth' });
|
window.scroll({ top: 0, behavior: 'smooth' });
|
||||||
},
|
},
|
||||||
|
|
||||||
async toggleChannelFollow() {
|
|
||||||
if (this.currentChannel.isFollowing) {
|
|
||||||
await os.apiWithDialog('channels/unfollow', {
|
|
||||||
channelId: this.currentChannel.id
|
|
||||||
});
|
|
||||||
this.currentChannel.isFollowing = false;
|
|
||||||
} else {
|
|
||||||
await os.apiWithDialog('channels/follow', {
|
|
||||||
channelId: this.currentChannel.id
|
|
||||||
});
|
|
||||||
this.currentChannel.isFollowing = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
openChannelMenu(ev) {
|
|
||||||
os.modalMenu([{
|
|
||||||
text: this.$ts.copyUrl,
|
|
||||||
icon: 'fas fa-link',
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
|
|
||||||
}
|
|
||||||
}], ev.currentTarget || ev.target);
|
|
||||||
},
|
|
||||||
|
|
||||||
onTransition() {
|
onTransition() {
|
||||||
if (window._scroll) window._scroll();
|
if (window._scroll) window._scroll();
|
||||||
},
|
},
|
||||||
|
@ -516,87 +437,24 @@ export default defineComponent({
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
$padding: 8px;
|
|
||||||
display: flex;
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
padding: $padding;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: var(--panel);
|
background-color: var(--panel);
|
||||||
border-bottom: solid 0.5px var(--divider);
|
border-bottom: solid 0.5px var(--divider);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
> .left {
|
> .body {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
box-sizing: border-box;
|
||||||
flex: 1;
|
overflow: auto;
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
height: ($header-height - ($padding * 2));
|
|
||||||
width: ($header-height - ($padding * 2));
|
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-right: 4px;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
min-width: 0;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
> .description {
|
|
||||||
opacity: 0.6;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 8px;
|
|
||||||
|
|
||||||
> .instance {
|
|
||||||
margin-right: 16px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .clock {
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .button {
|
|
||||||
height: ($header-height - ($padding * 2));
|
|
||||||
width: ($header-height - ($padding * 2));
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.follow.followed {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .side {
|
> .side {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
border-left: solid 4px var(--divider);
|
border-left: solid 4px var(--divider);
|
||||||
|
background: var(--panel);
|
||||||
|
|
||||||
&.widgets.sideViewOpening {
|
&.widgets.sideViewOpening {
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1400px) {
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="channel" class="hhizbblb">
|
||||||
|
<div class="info" v-if="date">
|
||||||
|
<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
|
||||||
|
</div>
|
||||||
|
<div class="tl" ref="body">
|
||||||
|
<div class="new" v-if="queue > 0" :style="{ width: width + 'px', bottom: bottom + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
|
||||||
|
<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="true"/>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<div class="typers" v-if="typers.length > 0">
|
||||||
|
<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 '@client/os';
|
||||||
|
import * as sound from '@client/scripts/sound';
|
||||||
|
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
|
||||||
|
import follow from '@client/directives/follow-append';
|
||||||
|
import XPostForm from '../post-form.vue';
|
||||||
|
import MkInfo from '@client/components/ui/info.vue';
|
||||||
|
import * as symbols from '@client/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(os.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.dialog({
|
||||||
|
title: this.$ts.inChannelSearch,
|
||||||
|
input: true
|
||||||
|
});
|
||||||
|
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.modalMenu([{
|
||||||
|
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>
|
|
@ -0,0 +1,221 @@
|
||||||
|
<template>
|
||||||
|
<div class="dbiokgaf">
|
||||||
|
<div class="info" v-if="date">
|
||||||
|
<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
|
||||||
|
</div>
|
||||||
|
<div class="top">
|
||||||
|
<XPostForm/>
|
||||||
|
</div>
|
||||||
|
<div class="tl" ref="body">
|
||||||
|
<div class="new" v-if="queue > 0" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
|
||||||
|
<XNotes class="tl" ref="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 '@client/os';
|
||||||
|
import * as sound from '@client/scripts/sound';
|
||||||
|
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
|
||||||
|
import follow from '@client/directives/follow-append';
|
||||||
|
import XPostForm from '../post-form.vue';
|
||||||
|
import MkInfo from '@client/components/ui/info.vue';
|
||||||
|
import * as symbols from '@client/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(os.stream.useChannel('homeTimeline'));
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
|
||||||
|
this.connection2 = markRaw(os.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(os.stream.useChannel('localTimeline'));
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
} else if (this.src == 'social') {
|
||||||
|
endpoint = 'notes/hybrid-timeline';
|
||||||
|
this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
} else if (this.src == 'global') {
|
||||||
|
endpoint = 'notes/global-timeline';
|
||||||
|
this.connection = markRaw(os.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>
|
|
@ -1,11 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mrajymqm _narrow_" v-if="component">
|
<div class="mrajymqm _narrow_" v-if="component">
|
||||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||||
<button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
|
<XHeader class="title" :info="pageInfo" :center="false" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()"/>
|
||||||
<XHeader class="title" :info="pageInfo" :with-back="false" :center="false"/>
|
|
||||||
<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
|
|
||||||
</header>
|
</header>
|
||||||
<component :is="component" v-bind="props" :ref="changePage" class="_flat_"/>
|
<component :is="component" v-bind="props" :ref="changePage" class="body _flat_"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -130,7 +128,6 @@ export default defineComponent({
|
||||||
top: 0;
|
top: 0;
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: $header-height;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
//background-color: var(--panel);
|
//background-color: var(--panel);
|
||||||
-webkit-backdrop-filter: blur(32px);
|
-webkit-backdrop-filter: blur(32px);
|
||||||
|
@ -153,6 +150,10 @@ export default defineComponent({
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -1,292 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="dbiokgaf info" v-if="date">
|
|
||||||
<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
|
|
||||||
</div>
|
|
||||||
<div class="dbiokgaf top" v-if="['home', 'local', 'social', 'global'].includes(src)">
|
|
||||||
<XPostForm/>
|
|
||||||
</div>
|
|
||||||
<div class="dbiokgaf tl" ref="body">
|
|
||||||
<div class="new" v-if="queue > 0" :style="{ width: width + 'px', [pagination.reversed ? 'bottom' : 'top']: pagination.reversed ? bottom + 'px' : top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
|
|
||||||
<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="pagination.reversed"/>
|
|
||||||
</div>
|
|
||||||
<div class="dbiokgaf bottom" v-if="src === 'channel'">
|
|
||||||
<div class="typers" v-if="typers.length > 0">
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, markRaw } from 'vue';
|
|
||||||
import XNotes from './notes.vue';
|
|
||||||
import * as os from '@client/os';
|
|
||||||
import * as sound from '@client/scripts/sound';
|
|
||||||
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
|
|
||||||
import follow from '@client/directives/follow-append';
|
|
||||||
import XPostForm from './post-form.vue';
|
|
||||||
import MkInfo from '@client/components/ui/info.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
XNotes,
|
|
||||||
XPostForm,
|
|
||||||
MkInfo,
|
|
||||||
},
|
|
||||||
|
|
||||||
directives: {
|
|
||||||
follow
|
|
||||||
},
|
|
||||||
|
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
inChannel: this.src === 'channel'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
antenna: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
channel: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['note', 'queue', 'before', 'after'],
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
const prepend = note => {
|
|
||||||
(this.$refs.tl as any).prepend(note);
|
|
||||||
|
|
||||||
this.$emit('note');
|
|
||||||
|
|
||||||
sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUserAdded = () => {
|
|
||||||
(this.$refs.tl as any).reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUserRemoved = () => {
|
|
||||||
(this.$refs.tl as any).reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeFollowing = () => {
|
|
||||||
if (!this.$refs.tl.backed) {
|
|
||||||
this.$refs.tl.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let endpoint;
|
|
||||||
let reversed = false;
|
|
||||||
|
|
||||||
if (this.src == 'antenna') {
|
|
||||||
endpoint = 'antennas/notes';
|
|
||||||
this.query = {
|
|
||||||
antennaId: this.antenna
|
|
||||||
};
|
|
||||||
this.connection = markRaw(os.stream.useChannel('antenna', {
|
|
||||||
antennaId: this.antenna
|
|
||||||
}));
|
|
||||||
this.connection.on('note', prepend);
|
|
||||||
} else if (this.src == 'home') {
|
|
||||||
endpoint = 'notes/timeline';
|
|
||||||
this.connection = markRaw(os.stream.useChannel('homeTimeline'));
|
|
||||||
this.connection.on('note', prepend);
|
|
||||||
|
|
||||||
this.connection2 = markRaw(os.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(os.stream.useChannel('localTimeline'));
|
|
||||||
this.connection.on('note', prepend);
|
|
||||||
} else if (this.src == 'social') {
|
|
||||||
endpoint = 'notes/hybrid-timeline';
|
|
||||||
this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
|
|
||||||
this.connection.on('note', prepend);
|
|
||||||
} else if (this.src == 'global') {
|
|
||||||
endpoint = 'notes/global-timeline';
|
|
||||||
this.connection = markRaw(os.stream.useChannel('globalTimeline'));
|
|
||||||
this.connection.on('note', prepend);
|
|
||||||
} else if (this.src == 'mentions') {
|
|
||||||
endpoint = 'notes/mentions';
|
|
||||||
this.connection = markRaw(os.stream.useChannel('main'));
|
|
||||||
this.connection.on('mention', prepend);
|
|
||||||
} else if (this.src == 'directs') {
|
|
||||||
endpoint = 'notes/mentions';
|
|
||||||
this.query = {
|
|
||||||
visibility: 'specified'
|
|
||||||
};
|
|
||||||
const onNote = note => {
|
|
||||||
if (note.visibility == 'specified') {
|
|
||||||
prepend(note);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.connection = markRaw(os.stream.useChannel('main'));
|
|
||||||
this.connection.on('mention', onNote);
|
|
||||||
} else if (this.src == 'list') {
|
|
||||||
endpoint = 'notes/user-list-timeline';
|
|
||||||
this.query = {
|
|
||||||
listId: this.list
|
|
||||||
};
|
|
||||||
this.connection = markRaw(os.stream.useChannel('userList', {
|
|
||||||
listId: this.list
|
|
||||||
}));
|
|
||||||
this.connection.on('note', prepend);
|
|
||||||
this.connection.on('userAdded', onUserAdded);
|
|
||||||
this.connection.on('userRemoved', onUserRemoved);
|
|
||||||
} else if (this.src == 'channel') {
|
|
||||||
endpoint = 'channels/timeline';
|
|
||||||
reversed = true;
|
|
||||||
this.query = {
|
|
||||||
channelId: this.channel
|
|
||||||
};
|
|
||||||
this.connection = markRaw(os.stream.useChannel('channel', {
|
|
||||||
channelId: this.channel
|
|
||||||
}));
|
|
||||||
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: endpoint,
|
|
||||||
reversed,
|
|
||||||
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.info{
|
|
||||||
padding: 16px 16px 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dbiokgaf.top {
|
|
||||||
padding: 16px 16px 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dbiokgaf.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: " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dbiokgaf.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>
|
|
Loading…
Reference in New Issue