summaryrefslogtreecommitdiff
path: root/src/client/ui
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-08-07 19:19:31 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-08-07 19:19:31 +0900
commitf565c5f730b8d90233e050ff2abf803870e5c4c8 (patch)
treee47d0399bb761bb7f76115fb65ae04e3848a2378 /src/client/ui
parent:art: (diff)
downloadsharkey-f565c5f730b8d90233e050ff2abf803870e5c4c8.tar.gz
sharkey-f565c5f730b8d90233e050ff2abf803870e5c4c8.tar.bz2
sharkey-f565c5f730b8d90233e050ff2abf803870e5c4c8.zip
Improve chat UI (wip)
Diffstat (limited to 'src/client/ui')
-rw-r--r--src/client/ui/chat/index.vue246
-rw-r--r--src/client/ui/chat/pages/channel.vue259
-rw-r--r--src/client/ui/chat/pages/timeline.vue221
-rw-r--r--src/client/ui/chat/side.vue11
-rw-r--r--src/client/ui/chat/timeline.vue292
5 files changed, 538 insertions, 491 deletions
diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue
index 6e433de126..db663c4530 100644
--- a/src/client/ui/chat/index.vue
+++ b/src/client/ui/chat/index.vue
@@ -73,54 +73,16 @@
</div>
<main class="main" @contextmenu.stop="onContextmenu">
- <header class="header" ref="header" @click="onHeaderClick">
- <div class="left">
- <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 class="header">
+ <XHeader class="header" :info="pageInfo" :menu="menu" :center="false" :back-button="true" @back="back()" @click="onHeaderClick"/>
</header>
-
- <XTimeline class="body" ref="tl" v-if="tl.startsWith('channel:')" src="channel" :key="tl" :channel="tl.replace('channel:', '')"/>
- <XTimeline class="body" ref="tl" v-else :src="tl" :key="tl"/>
+ <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 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 XCommon from '../_common_/common.vue';
import XSide from './side.vue';
-import XTimeline from './timeline.vue';
+import XHeader from '../_common_/header.vue';
import XHeaderClock from './header-clock.vue';
import * as os from '@client/os';
import { router } from '@client/router';
@@ -147,6 +109,7 @@ import { menuDef } from '@client/menu';
import { search } from '@client/scripts/search';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { store } from './store';
+import * as symbols from '@client/symbols';
export default defineComponent({
components: {
@@ -154,29 +117,12 @@ export default defineComponent({
XSidebar,
XWidgets,
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
- XTimeline,
+ XHeader,
XHeaderClock,
},
provide() {
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) => {
this.$refs.side.navigate(path);
}
@@ -185,7 +131,7 @@ export default defineComponent({
data() {
return {
- tl: store.state.tl,
+ pageInfo: null,
lists: null,
antennas: 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() {
if (window.innerWidth < 1024) {
localStorage.setItem('ui', 'default');
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 => {
this.lists = lists;
});
@@ -225,18 +183,22 @@ export default defineComponent({
os.api('channels/featured', { limit: 20 }).then(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: {
+ 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() {
this.$refs.menu.show();
},
@@ -245,59 +207,18 @@ export default defineComponent({
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();
},
- 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.currentChannel.id}`);
+ back() {
+ history.back();
},
top() {
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() {
if (window._scroll) window._scroll();
},
@@ -516,87 +437,24 @@ export default defineComponent({
background: var(--panel);
> .header {
- $padding: 8px;
- display: flex;
z-index: 1000;
height: $header-height;
- padding: $padding;
- box-sizing: border-box;
background-color: var(--panel);
border-bottom: solid 0.5px var(--divider);
user-select: none;
+ }
- > .left {
- display: flex;
- align-items: center;
- flex: 1;
- 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);
- }
- }
- }
+ > .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) {
diff --git a/src/client/ui/chat/pages/channel.vue b/src/client/ui/chat/pages/channel.vue
new file mode 100644
index 0000000000..76b334487e
--- /dev/null
+++ b/src/client/ui/chat/pages/channel.vue
@@ -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>
diff --git a/src/client/ui/chat/pages/timeline.vue b/src/client/ui/chat/pages/timeline.vue
new file mode 100644
index 0000000000..0f9cd7f11e
--- /dev/null
+++ b/src/client/ui/chat/pages/timeline.vue
@@ -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>
diff --git a/src/client/ui/chat/side.vue b/src/client/ui/chat/side.vue
index 7ad39c7a19..5ccfad1b75 100644
--- a/src/client/ui/chat/side.vue
+++ b/src/client/ui/chat/side.vue
@@ -1,11 +1,9 @@
<template>
<div class="mrajymqm _narrow_" v-if="component">
<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" :with-back="false" :center="false"/>
- <button class="_button" @click="close()"><i class="fas fa-times"></i></button>
+ <XHeader class="title" :info="pageInfo" :center="false" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()"/>
</header>
- <component :is="component" v-bind="props" :ref="changePage" class="_flat_"/>
+ <component :is="component" v-bind="props" :ref="changePage" class="body _flat_"/>
</div>
</template>
@@ -130,7 +128,6 @@ export default defineComponent({
top: 0;
height: $header-height;
width: 100%;
- line-height: $header-height;
font-weight: bold;
//background-color: var(--panel);
-webkit-backdrop-filter: blur(32px);
@@ -153,6 +150,10 @@ export default defineComponent({
position: relative;
}
}
+
+ > .body {
+
+ }
}
</style>
diff --git a/src/client/ui/chat/timeline.vue b/src/client/ui/chat/timeline.vue
deleted file mode 100644
index 0fbcbfb713..0000000000
--- a/src/client/ui/chat/timeline.vue
+++ /dev/null
@@ -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>