Compare commits
7 Commits
8d4614a48d
...
ac63accbed
Author | SHA1 | Date |
---|---|---|
Derek | ac63accbed | |
Derek | 832e49b29b | |
Derek | e3035ea18b | |
Derek | 03a7a4e56d | |
Derek | c0013b6c88 | |
Derek | de403aca9d | |
Derek | a6d6e778b0 |
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkButton rounded full small @click="toggle"><b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b><span v-if="!modelValue" :class="$style.label">{{ label }}</span></MkButton>
|
||||
<MkButton rounded :full="!props.small" small @click="toggle"><b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b><span v-if="!modelValue" :class="$style.label">{{ label }}</span></MkButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -21,6 +21,7 @@ const props = defineProps<{
|
|||
renote?: Misskey.entities.Note | null;
|
||||
files?: Misskey.entities.DriveFile[];
|
||||
poll?: Misskey.entities.Note['poll'] | PollEditorModelValue | null;
|
||||
small?: boolean | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
@ -36,6 +36,7 @@ import { deviceKind } from '@/scripts/device-kind.js';
|
|||
const props = withDefaults(defineProps<{
|
||||
src?: HTMLElement;
|
||||
anchor?: { x: string; y: string; };
|
||||
all?: boolean | null;
|
||||
}>(), {
|
||||
anchor: () => ({ x: 'right', y: 'center' }),
|
||||
});
|
||||
|
@ -52,7 +53,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
|
|||
|
||||
const menu = defaultStore.state.menu;
|
||||
|
||||
const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
||||
const items = Object.keys(navbarItemDef).filter(k => props.all || !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
||||
type: def.to ? 'link' : 'button',
|
||||
text: def.title,
|
||||
icon: def.icon,
|
||||
|
|
|
@ -24,16 +24,10 @@
|
|||
<MkA to="/#global" v-if="isGlobalTimelineAvailable" class="item" active-class="active" exact><i class="ti ti-whirl icon"></i>{{ i18n.ts._timelines.global }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followedChannels" class="container">
|
||||
<div class="header">{{ i18n.ts.channel }} ({{ i18n.ts.following }})<button class="_button add" @click="addChannel"><i class="ti ti-plus"></i></button></div>
|
||||
<div v-if="favoriteChannels" class="container">
|
||||
<div class="header">{{ i18n.ts.channel }} ({{ i18n.ts.favorites }})<MkA to="/channels" class="_button add"><i class="ti ti-dots"></i></MkA></div>
|
||||
<div class="body">
|
||||
<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ read: !channel.hasUnreadNote }" active-class="active" exact><i class="ti ti-device-tv icon"></i>{{ channel.name }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="featuredChannels" class="container">
|
||||
<div class="header">{{ i18n.ts.channel }}<button class="_button add" @click="addChannel"><i class="ti ti-plus"></i></button></div>
|
||||
<div class="body">
|
||||
<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" active-class="active" exact><i class="ti ti-device-tv icon"></i>{{ channel.name }}</MkA>
|
||||
<MkA v-for="channel in favoriteChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ read: !channel.hasUnreadNote }" active-class="active" exact><i class="ti ti-device-tv icon"></i>{{ channel.name }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="lists" class="container">
|
||||
|
@ -43,7 +37,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="antennas" class="container">
|
||||
<div class="header">{{ i18n.ts.antennas }}<button class="_button add" @click="addAntenna"><i class="ti ti-plus"></i></button></div>
|
||||
<div class="header">{{ i18n.ts.antennas }}<MkA to="/my/antennas/create" class="_button add"><i class="ti ti-plus"></i></MkA></div>
|
||||
<div class="body">
|
||||
<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/timeline/antenna/${ antenna.id }`" class="item" active-class="active" exact><i class="ti ti-antenna icon"></i>{{ antenna.name }}</MkA>
|
||||
</div>
|
||||
|
@ -55,10 +49,14 @@
|
|||
<button class="_button menu" @click="showMenu">
|
||||
<i class="ti ti-menu-2 icon"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div class="right">
|
||||
<button v-tooltip="i18n.ts.search" class="_button item search" @click="search">
|
||||
<i class="ti ti-search icon"></i>
|
||||
<MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip="i18n.ts.controlPanel" class="item" to="/admin">
|
||||
<i class="ti ti-dashboard icon"></i>
|
||||
</MkA>
|
||||
<button v-tooltip="i18n.ts.more" class="item _button" @click="more">
|
||||
<i class="ti ti-grid-dots icon"></i>
|
||||
</button>
|
||||
<MkA v-tooltip="i18n.ts.settings" class="item" to="/settings"><i class="ti ti-settings icon"></i></MkA>
|
||||
</div>
|
||||
|
@ -101,11 +99,9 @@ import { provide, ref, shallowRef, toRefs, computed, onMounted, defineAsyncCompo
|
|||
import { instanceName, url } from '@/config.js';
|
||||
import XWidgets from './chat/widgets.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import XHeaderClock from './chat/header-clock.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { search } from '@/scripts/search.js';
|
||||
import makeList from '@/scripts/new-list.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
|
@ -116,9 +112,11 @@ import { mainRouter } from '@/router/main.js';
|
|||
import { defaultRoutes, page } from '@/router/definition.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
|
||||
|
||||
const XDrawerMenu = defineAsyncComponent(() => import('@/ui/_common_/navbar-for-mobile.vue'));
|
||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||
const XLaunchPad = defineAsyncComponent(() => import('@/components/MkLaunchPad.vue'));
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
@ -129,6 +127,9 @@ const routes = [
|
|||
globalCacheKey: 'index',
|
||||
}, {
|
||||
...defaultRoutes.find((route) => route.path == '/channels/new'),
|
||||
}, {
|
||||
path: '/channels/:channelId/about',
|
||||
component: page(() => import('./chat/pages/channels.about.vue')),
|
||||
}, {
|
||||
path: '/channels/:channelId',
|
||||
props: { tlSrc: 'channel' },
|
||||
|
@ -192,8 +193,7 @@ const menu = computed(() => [{
|
|||
|
||||
const lists = ref([]);
|
||||
const antennas = ref([]);
|
||||
const followedChannels = ref([]);
|
||||
const featuredChannels = ref([]);
|
||||
const favoriteChannels = ref([]);
|
||||
|
||||
function getSidebarContent() {
|
||||
misskeyApi('users/lists/list').then(newLists => {
|
||||
|
@ -204,22 +204,11 @@ function getSidebarContent() {
|
|||
antennas.value = newAntennas;
|
||||
});
|
||||
|
||||
misskeyApi('channels/followed', { limit: 20 }).then(channels => {
|
||||
followedChannels.value = channels;
|
||||
});
|
||||
|
||||
// TODO: pagination
|
||||
misskeyApi('channels/featured', { limit: 20 }).then(channels => {
|
||||
featuredChannels.value = channels;
|
||||
misskeyApi('channels/my-favorites').then(channels => {
|
||||
favoriteChannels.value = channels;
|
||||
});
|
||||
}
|
||||
|
||||
function addChannel() {
|
||||
chatRouter.push('/channels/new');
|
||||
}
|
||||
function addAntenna() {
|
||||
chatRouter.push('/my/antennas/create');
|
||||
}
|
||||
async function addList() {
|
||||
const success = await makeList();
|
||||
if (success) {
|
||||
|
@ -235,6 +224,13 @@ function hideMenu() {
|
|||
drawerMenuShowing.value = false;
|
||||
}
|
||||
|
||||
function more(ev: MouseEvent) {
|
||||
os.popup(XLaunchPad, {
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
all: true,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
function post() {
|
||||
os.post();
|
||||
}
|
||||
|
|
|
@ -1,61 +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';
|
||||
|
||||
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>
|
|
@ -1,21 +1,28 @@
|
|||
<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"/>
|
||||
<header :class="$style.root">
|
||||
<div :class="$style.left">
|
||||
<MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<span v-if="note.visibility !== 'public'" class="visibility">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" class="localOnly"><i class="ti ti-rocket-off"></i></span>
|
||||
<span v-if="note.channel" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||
<span v-if="note.user.isBot" :class="$style.is-bot">bot</span>
|
||||
<span :class="$style.username"><MkAcct :user="note.user"/></span>
|
||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.right">
|
||||
<div :class="$style.info">
|
||||
<span v-if="note.visibility !== 'public'">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" :class="$style.localOnly"><i class="ti ti-rocket-off"></i></span>
|
||||
<span v-if="note.channel" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||
<MkA :class="$style.createdAt" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
@ -45,51 +52,69 @@ export default defineComponent({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dehvdgxo {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
.left {
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .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;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
> .username {
|
||||
margin: 0 .5em 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.isBot {
|
||||
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;
|
||||
}
|
||||
|
||||
.badgeRoles {
|
||||
margin: 0 .5em 0 0;
|
||||
}
|
||||
|
||||
.badgeRole {
|
||||
height: 1.3em;
|
||||
vertical-align: -20%;
|
||||
|
||||
& + .badgeRole {
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.7;
|
||||
.info {
|
||||
opacity: 0.7;
|
||||
|
||||
> span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
> *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,113 +1,56 @@
|
|||
<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"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
<XSubNoteContent class="text" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrpstxzv">
|
||||
<MkAvatar class="avatar" :user="note.user" link preview/>
|
||||
<Mfm :text="summary" :plain="true" :nowrap="true" :author="note.user" :nyaize="'respect'" class="text"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { ref, shallowRef, watchEffect } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XSubNoteContent from './sub-note-content.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
detail?: boolean;
|
||||
children?: boolean;
|
||||
// how many notes are in between this one and the note being viewed in detail
|
||||
depth?: number;
|
||||
}>(), {
|
||||
depth: 1,
|
||||
children: false,
|
||||
showContent?: boolean | null;
|
||||
}>();
|
||||
|
||||
const summary = ref(null);
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.showContent) {
|
||||
summary.value = getNoteSummary({
|
||||
...props.note,
|
||||
cw: null,
|
||||
});
|
||||
} else {
|
||||
summary.value = getNoteSummary(props.note);
|
||||
}
|
||||
});
|
||||
|
||||
let showContent = ref(false);
|
||||
let replies: misskey.entities.Note[] = shallowRef([]);
|
||||
|
||||
if (props.detail) {
|
||||
misskeyApi('notes/children', {
|
||||
noteId: props.note.id,
|
||||
limit: 5,
|
||||
}).then(res => {
|
||||
replies = res;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrpstxzv {
|
||||
padding: 16px 16px;
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
&.children {
|
||||
padding: 10px 0 0 16px;
|
||||
font-size: 1em;
|
||||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin: 0 8px 0 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
> .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;
|
||||
> .text {
|
||||
overflow: hidden;
|
||||
flex-shrink: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,14 +5,35 @@
|
|||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
class="vfzoeqcg"
|
||||
:class="{ renote: isRenote, operating: operating }"
|
||||
:class="{ collapsed: renoteCollapsed, operating: operating }"
|
||||
:tabindex="!isDeleted ? '-1' : undefined"
|
||||
>
|
||||
<XSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" class="reply-to"/>
|
||||
<div v-if="appearNote.reply && !renoteCollapsed" class="header reply-to">
|
||||
<MkA class="icon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-corner-up-right"></i></MkA>
|
||||
<XSub class="note" :note="appearNote.reply" :showContent="showContent"/>
|
||||
</div>
|
||||
<div v-if="pinned" class="info"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
<!--<div v-if="appearNote._prId_" class="tip"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
|
||||
<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
|
||||
<div v-if="isRenote" class="renote">
|
||||
<div v-if="isRenote && renoteCollapsed" class="collapsed-renote" @click="renoteCollapsed = false">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<i class="icon ti ti-repeat"></i>
|
||||
<XSub class="note" :note="appearNote"/>
|
||||
<div class="info">
|
||||
<span v-if="note.visibility !== 'public'" class="visibility" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||
<span v-if="note.channel" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||
<button ref="renoteTime" class="_button time" @click.stop="showRenoteMenu()">
|
||||
<i class="ti ti-dots dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isRenote" class="header renote-header">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
|
@ -23,10 +44,6 @@
|
|||
</template>
|
||||
</I18n>
|
||||
<div class="info">
|
||||
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
||||
<i class="ti ti-dots dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<span v-if="note.visibility !== 'public'" class="visibility" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
|
@ -34,14 +51,13 @@
|
|||
</span>
|
||||
<span v-if="note.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||
<span v-if="note.channel" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
||||
<i class="ti ti-dots dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="renoteCollapsed" class="collapsed-renote">
|
||||
<MkAvatar class="avatar" :user="appearNote.user" link preview/>
|
||||
<Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'respect'" class="text" @click="renoteCollapsed = false"/>
|
||||
</div>
|
||||
<article v-else class="article" @contextmenu.stop="onContextmenu">
|
||||
<!-- <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> -->
|
||||
<article v-if="!renoteCollapsed" class="article" @contextmenu.stop="onContextmenu">
|
||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||
|
@ -49,12 +65,11 @@
|
|||
<div class="body" style="container-type: inline-size;">
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" :small="true" />
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
|
@ -80,7 +95,7 @@
|
|||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer :note="appearNote"/>
|
||||
<footer class="footer _panel">
|
||||
<footer class="footer">
|
||||
<button v-tooltip="i18n.ts.reply" class="button _button" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
|
@ -246,6 +261,12 @@ const renoteCollapsed = ref(
|
|||
);
|
||||
const operating = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (appearNote.value.channel && rootEl.value && !inChannel.value) {
|
||||
rootEl.value.style.setProperty('--channelColor', appearNote.value.channel.color);
|
||||
}
|
||||
});
|
||||
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
|
@ -561,15 +582,32 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
&:hover, &.operating {
|
||||
> .article > .main > .footer {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
&::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.renote {
|
||||
background: rgba(128, 255, 0, 0.05);
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: -1;
|
||||
background: var(--channelColor);
|
||||
opacity: 0.15;
|
||||
filter: saturate(75%);
|
||||
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
background: rgba(255, 128, 0, 0.05);
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 5px; height: 100%;
|
||||
z-index: -1;
|
||||
background: var(--channelColor);
|
||||
}
|
||||
|
||||
> .info {
|
||||
|
@ -596,19 +634,58 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
padding-top: 8px;
|
||||
}
|
||||
|
||||
> .reply-to {
|
||||
opacity: 0.7;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
> .renote, > .collapsed-renote {
|
||||
> .collapsed-renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 8px;
|
||||
overflow: hidden;
|
||||
gap: 0.5em;
|
||||
padding: 12px 16px 12px 16px;
|
||||
|
||||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
color: var(--renote);
|
||||
}
|
||||
|
||||
> .note {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> .header {
|
||||
padding: 12px 16px 0px 16px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
> .reply-to {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .icon {
|
||||
color: var(--mention);
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
> .note {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
> .renote-header {
|
||||
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;
|
||||
|
@ -620,7 +697,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
> span {
|
||||
|
@ -633,11 +710,16 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .renote-header, > .collapsed-renote {
|
||||
color: var(--renote);
|
||||
|
||||
> .info {
|
||||
margin-left: 8px;
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
|
||||
> .time {
|
||||
flex-shrink: 0;
|
||||
|
@ -648,16 +730,12 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: 8px;
|
||||
> *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .renote + .article {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
> .article {
|
||||
display: flex;
|
||||
padding: 12px 16px;
|
||||
|
@ -724,11 +802,6 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
> .text {
|
||||
overflow-wrap: break-word;
|
||||
|
||||
> .reply {
|
||||
color: var(--accent);
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
> .rp {
|
||||
margin-left: 4px;
|
||||
font-style: oblique;
|
||||
|
@ -785,11 +858,14 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
|
||||
> .footer {
|
||||
display: none;
|
||||
overflow: clip;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 0 6px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
padding: 2px 8px;
|
||||
opacity: 0.7;
|
||||
background: var(--panel);
|
||||
border-top-left-radius: var(--radius);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
|
|
@ -0,0 +1,286 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :class="$style.main">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="channel && tab === 'overview'" key="overview" class="_gaps">
|
||||
<div class="_panel" :class="$style.bannerContainer">
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
|
||||
<div :class="$style.bannerStatus">
|
||||
<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
</div>
|
||||
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||
<div :class="$style.bannerFade"></div>
|
||||
</div>
|
||||
<div v-if="channel.description" :class="$style.description">
|
||||
<Mfm :text="channel.description" :isNote="false"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection>
|
||||
<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
|
||||
<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
|
||||
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'featured'" key="featured">
|
||||
<MkNotes :pagination="featuredPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'" key="search">
|
||||
<div class="_gaps">
|
||||
<div>
|
||||
<MkInput v-model="searchQuery" @enter="search()">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton inline rounded primary gradate @click="goToTl()"><i class="ti ti-pencil"></i> {{ i18n.ts.postToTheChannel }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { url } from '@/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
channelId: string;
|
||||
}>();
|
||||
|
||||
const tab = ref('overview');
|
||||
|
||||
const channel = ref<Misskey.entities.Channel | null>(null);
|
||||
const favorited = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const searchPagination = ref();
|
||||
const searchKey = ref('');
|
||||
const featuredPagination = computed(() => ({
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
channelId: props.channelId,
|
||||
},
|
||||
}));
|
||||
|
||||
watch(() => props.channelId, async () => {
|
||||
channel.value = await misskeyApi('channels/show', {
|
||||
channelId: props.channelId,
|
||||
});
|
||||
favorited.value = channel.value.isFavorited ?? false;
|
||||
}, { immediate: true });
|
||||
|
||||
function edit() {
|
||||
router.push(`/channels/${channel.value?.id}/edit`);
|
||||
}
|
||||
|
||||
function goToTl() {
|
||||
router.push(`/channels/${channel.value?.id}`)
|
||||
}
|
||||
|
||||
async function search() {
|
||||
if (!channel.value) return;
|
||||
|
||||
const query = searchQuery.value.toString().trim();
|
||||
|
||||
if (query == null) return;
|
||||
|
||||
searchPagination.value = {
|
||||
endpoint: 'notes/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: query,
|
||||
channelId: channel.value.id,
|
||||
},
|
||||
};
|
||||
|
||||
searchKey.value = query;
|
||||
}
|
||||
|
||||
const headerActions = computed(() => {
|
||||
if (channel.value && channel.value.userId) {
|
||||
const headerItems: PageHeaderItem[] = [];
|
||||
|
||||
headerItems.push({
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyUrl,
|
||||
handler: async (): Promise<void> => {
|
||||
if (!channel.value) {
|
||||
console.warn('failed to copy channel URL. channel.value is null.');
|
||||
return;
|
||||
}
|
||||
copyToClipboard(`${url}/channels/${channel.value.id}`);
|
||||
os.success();
|
||||
},
|
||||
});
|
||||
|
||||
if (isSupportShare()) {
|
||||
headerItems.push({
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
handler: async (): Promise<void> => {
|
||||
if (!channel.value) {
|
||||
console.warn('failed to share channel. channel.value is null.');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.share({
|
||||
title: channel.value.name,
|
||||
text: channel.value.description ?? undefined,
|
||||
url: `${url}/channels/${channel.value.id}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (($i && $i.id === channel.value.userId) || iAmModerator) {
|
||||
headerItems.push({
|
||||
icon: 'ti ti-settings',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
});
|
||||
}
|
||||
|
||||
return headerItems.length > 0 ? headerItems : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ti ti-info-circle',
|
||||
}, {
|
||||
key: 'featured',
|
||||
title: i18n.ts.featured,
|
||||
icon: 'ti ti-bolt',
|
||||
}, {
|
||||
key: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: channel.value ? channel.value.name : i18n.ts.channel,
|
||||
icon: 'ti ti-device-tv',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.main {
|
||||
min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
|
||||
}
|
||||
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
background: var(--acrylicBg);
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
.bannerContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.subscribe {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.favorite {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bannerFade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
}
|
||||
|
||||
.bannerStatus {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
padding: 8px 12px;
|
||||
font-size: 80%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sensitiveIndicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: var(--warn);
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 4px 7px;
|
||||
}
|
||||
</style>
|
|
@ -1,26 +1,26 @@
|
|||
<template>
|
||||
<div class="dbiokgaf">
|
||||
<MkPageHeader @click="goTop()"/>
|
||||
<div v-if="timetravelTarget" class="info">
|
||||
<MkInfo>{{ i18n.ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ i18n.ts.clear }}</button></MkInfo>
|
||||
<div :class="$style.root">
|
||||
<MkPageHeader @click="scrollToNew()" :actions="headerActions"/>
|
||||
<div v-if="timetravelTarget" :class="$style.info">
|
||||
<MkInfo>{{ i18n.ts.showingPastTimeline }} <button class="_textButton" @click="timetravel()">{{ i18n.ts.clear }}</button></MkInfo>
|
||||
</div>
|
||||
<div class="body" :class="{ reverse: inChannel }">
|
||||
<div class="postform">
|
||||
<div :class="[$style.body, { [$style.reverse]: inChannel }]">
|
||||
<div :class="$style.postform">
|
||||
<div v-if="typers.length > 0" class="typers">
|
||||
<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
|
||||
<I18n :src="i18n.ts.typingUsers" text-tag="span">
|
||||
<template #users>
|
||||
<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
|
||||
<b v-for="user in typers" :key="user.id" :class="$style.user">{{ user.username }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkEllipsis/>
|
||||
</div>
|
||||
<XPostForm :channel="channel" :autofocus="!inChannel" />
|
||||
</div>
|
||||
<div ref="scroller" class="tl">
|
||||
<div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: inChannel ? null : top + 'px', bottom: inChannel ? bottom + 'px' : null }">
|
||||
<div ref="scroller" :class="$style.tl">
|
||||
<div v-if="queue > 0" :class="$style.new" :style="{ width: width + 'px', top: inChannel ? null : top + 'px', bottom: inChannel ? bottom + 'px' : null }">
|
||||
<button class="_buttonPrimary" @click="scrollToNew()">{{ i18n.ts.newNoteRecived }}</button>
|
||||
</div>
|
||||
<XNotes ref="tlElement" class="tl" :pagination="pagination" :reversed="inChannel" @queue="queueUpdated"/>
|
||||
<XNotes ref="tlElement" :pagination="pagination" :reversed="inChannel" @queue="queueUpdated"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,13 +35,19 @@ import XNotes from '../notes.vue';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import * as os from '@/os.js';
|
||||
import { scrollToBottom, scrollToTop } from '@/scripts/scroll.js';
|
||||
import XPostForm from '../post-form.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||
import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
type Props = {
|
||||
tlSrc?: string;
|
||||
|
@ -54,6 +60,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
const emit = defineEmits(['note']);
|
||||
|
||||
const tlElement = ref();
|
||||
const tab = ref('timeline');
|
||||
const inChannel = computed(() => props.tlSrc == 'channel');
|
||||
provide('inChannel', inChannel);
|
||||
|
||||
|
@ -63,6 +70,15 @@ watch(() => props.channelId, async () => {
|
|||
channel.value = await misskeyApi('channels/show', {
|
||||
channelId: props.channelId,
|
||||
});
|
||||
|
||||
if ((channel.value.isFavorited || channel.value.isFollowing) && channel.value.lastNotedAt) {
|
||||
const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0;
|
||||
const lastNotedAt = Date.parse(channel.value.lastNotedAt);
|
||||
|
||||
if (lastNotedAt > lastReadedAt) {
|
||||
miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, lastNotedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
|
@ -188,91 +204,145 @@ function scrollToNew() {
|
|||
}
|
||||
};
|
||||
|
||||
const headerActions = computed(() => {
|
||||
if (channel.value && channel.value.userId) {
|
||||
const headerItems: PageHeaderItem[] = [];
|
||||
|
||||
headerItems.push({
|
||||
icon: 'ti ti-star',
|
||||
text: i18n.ts.favorite,
|
||||
highlighted: channel.value.isFavorited,
|
||||
handler: async () => {
|
||||
if (channel.value.isFavorited) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unfavoriteConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
await os.apiWithDialog('channels/unfavorite', {
|
||||
channelId: channel.value.id,
|
||||
});
|
||||
channel.value.isFavorited = false;
|
||||
} else {
|
||||
await os.apiWithDialog('channels/favorite', {
|
||||
channelId: channel.value.id,
|
||||
});
|
||||
channel.value.isFavorited = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
headerItems.push({
|
||||
icon: channel.value.isFollowing ? 'ti ti-minus' : 'ti ti-plus',
|
||||
text: channel.value.isFollowing ? i18n.ts.unfollow : i18n.ts.follow,
|
||||
highlighted: channel.value.isFollowing,
|
||||
handler: async () => {
|
||||
if (channel.value.isFollowing) {
|
||||
await os.apiWithDialog('channels/unfollow', {
|
||||
channelId: channel.value.id,
|
||||
});
|
||||
channel.value.isFollowing = false;
|
||||
} else {
|
||||
await os.apiWithDialog('channels/follow', {
|
||||
channelId: channel.value.id,
|
||||
});
|
||||
channel.value.isFollowing = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
headerItems.push({
|
||||
icon: 'ti ti-info-circle',
|
||||
text: i18n.ts.about,
|
||||
handler: () => router.push(`/channels/${channel.value?.id}/about`)
|
||||
});
|
||||
|
||||
return headerItems.length > 0 ? headerItems : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
definePageMetadata(() => metadata.value);
|
||||
provideReactiveMetadata(metadata);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dbiokgaf {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: clip;
|
||||
contain: strict;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
> .info {
|
||||
.info {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.postform {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
|
||||
> .postform {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
&.reverse {
|
||||
flex-direction: column-reverse;
|
||||
> .postform {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .tl {
|
||||
position: relative;
|
||||
padding: 16px 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
|
||||
> .new {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
|
||||
> button {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
&.reverse {
|
||||
flex-direction: column-reverse;
|
||||
.postform {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding: 0 16px 16px 16px;
|
||||
position: relative;
|
||||
.tl {
|
||||
position: relative;
|
||||
padding: 16px 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
> .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);
|
||||
.new {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
|
||||
> .users {
|
||||
> .user + .user:before {
|
||||
content: ", ";
|
||||
font-weight: normal;
|
||||
}
|
||||
> button {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .user:last-of-type:after {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
.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);
|
||||
|
||||
.user + .user:before {
|
||||
content: ", ";
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
.user:last-of-type:after {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue