<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>