Compare commits
4 Commits
7d8205930a
...
ead8a4eb3b
Author | SHA1 | Date |
---|---|---|
Derek | ead8a4eb3b | |
Derek | 1a90d4f5f6 | |
Derek | ddb612853b | |
Derek | 581bbc6021 |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "12.119.0+birb3-2",
|
"version": "12.119.0+birb4",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 82 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 81 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 82 KiB |
|
@ -15,23 +15,34 @@ import { useRouter } from '@/router';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
to: string;
|
to: string;
|
||||||
|
exact?: boolean;
|
||||||
activeClass?: null | string;
|
activeClass?: null | string;
|
||||||
behavior?: null | 'window' | 'browser' | 'modalWindow';
|
behavior?: null | 'window' | 'browser' | 'modalWindow';
|
||||||
}>(), {
|
}>(), {
|
||||||
activeClass: null,
|
activeClass: null,
|
||||||
behavior: null,
|
behavior: null,
|
||||||
|
exact: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const currentPath = $ref(router.getCurrentPath());
|
||||||
|
router.addListener('change', ({ path }) => {
|
||||||
|
currentPath = path;
|
||||||
|
});
|
||||||
|
|
||||||
const active = $computed(() => {
|
const active = $computed(() => {
|
||||||
if (props.activeClass == null) return false;
|
if (props.activeClass == null) return false;
|
||||||
|
if (props.exact) {
|
||||||
|
return currentPath === props.to;
|
||||||
|
} else {
|
||||||
const resolved = router.resolve(props.to);
|
const resolved = router.resolve(props.to);
|
||||||
if (resolved == null) return false;
|
if (resolved == null) return false;
|
||||||
if (resolved.route.path === router.currentRoute.value.path) return true;
|
if (resolved.route.path === router.currentRoute.value.path) return true;
|
||||||
if (resolved.route.name == null) return false;
|
if (resolved.route.name == null) return false;
|
||||||
if (router.currentRoute.value.name == null) return false;
|
if (router.currentRoute.value.name == null) return false;
|
||||||
return resolved.route.name === router.currentRoute.value.name;
|
return resolved.route.name === router.currentRoute.value.name;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function onContextmenu(ev) {
|
function onContextmenu(ev) {
|
||||||
|
|
|
@ -14,12 +14,15 @@ export default {
|
||||||
const height = container.scrollHeight;
|
const height = container.scrollHeight;
|
||||||
isBottom = (pos + viewHeight > height - 32);
|
isBottom = (pos + viewHeight > height - 32);
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
container.scrollTop = container.scrollHeight;
|
console.log('mount', container.scrollHeight);
|
||||||
|
|
||||||
|
container.scroll({ top: container.scrollHeight, behavior: 'smooth' });
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
const ro = new ResizeObserver((entries, observer) => {
|
||||||
|
console.log('resize', container.scrollHeight);
|
||||||
if (isBottom) {
|
if (isBottom) {
|
||||||
const height = container.scrollHeight;
|
const height = container.scrollHeight;
|
||||||
container.scrollTop = height;
|
container.scroll({ top: height, behavior: 'instant' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import anim from './anim';
|
||||||
import clickAnime from './click-anime';
|
import clickAnime from './click-anime';
|
||||||
import panel from './panel';
|
import panel from './panel';
|
||||||
import adaptiveBorder from './adaptive-border';
|
import adaptiveBorder from './adaptive-border';
|
||||||
|
import follow from './follow-append';
|
||||||
|
|
||||||
export default function(app: App) {
|
export default function(app: App) {
|
||||||
app.directive('userPreview', userPreview);
|
app.directive('userPreview', userPreview);
|
||||||
|
@ -25,4 +26,5 @@ export default function(app: App) {
|
||||||
app.directive('click-anime', clickAnime);
|
app.directive('click-anime', clickAnime);
|
||||||
app.directive('panel', panel);
|
app.directive('panel', panel);
|
||||||
app.directive('adaptive-border', adaptiveBorder);
|
app.directive('adaptive-border', adaptiveBorder);
|
||||||
|
app.directive('follow', follow);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export type RouteDef = {
|
||||||
loginRequired?: boolean;
|
loginRequired?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
|
props?: Record<string, any>;
|
||||||
globalCacheKey?: string;
|
globalCacheKey?: string;
|
||||||
children?: RouteDef[];
|
children?: RouteDef[];
|
||||||
};
|
};
|
||||||
|
@ -65,7 +66,7 @@ export class Router extends EventEmitter<{
|
||||||
beforePath: string;
|
beforePath: string;
|
||||||
path: string;
|
path: string;
|
||||||
route: RouteDef | null;
|
route: RouteDef | null;
|
||||||
props: Map<string, string> | null;
|
props: Map<string, any> | null;
|
||||||
key: string;
|
key: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
same: () => void;
|
same: () => void;
|
||||||
|
@ -106,7 +107,7 @@ export class Router extends EventEmitter<{
|
||||||
forEachRouteLoop:
|
forEachRouteLoop:
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
let parts = [ ..._parts ];
|
let parts = [ ..._parts ];
|
||||||
const props = new Map<string, string>();
|
const props = new Map<string, any>();
|
||||||
|
|
||||||
pathMatchLoop:
|
pathMatchLoop:
|
||||||
for (const p of parsePath(route.path)) {
|
for (const p of parsePath(route.path)) {
|
||||||
|
@ -172,6 +173,12 @@ export class Router extends EventEmitter<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route.props) {
|
||||||
|
Object.entries(route.props).forEach(([key, value]) => {
|
||||||
|
props.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
route,
|
route,
|
||||||
props,
|
props,
|
||||||
|
|
|
@ -17,34 +17,34 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">{{ $ts.timeline }}</div>
|
<div class="header">{{ $ts.timeline }}</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA to="/#home" class="item" active-class="active"><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
|
<MkA to="/#home" class="item" active-class="active" exact><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
|
||||||
<MkA to="/#local" class="item" :class="{ active: tl === 'local' }"><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
|
<MkA to="/#local" v-if="isLocalTimelineAvailable" class="item" active-class="active" exact><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
|
||||||
<MkA to="/#social" class="item" :class="{ active: tl === 'social' }"><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
|
<MkA to="/#social" v-if="isLocalTimelineAvailable" class="item" active-class="active" exact><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
|
||||||
<MkA to="/#global" class="item" :class="{ active: tl === 'global' }"><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
|
<MkA to="/#global" v-if="isGlobalTimelineAvailable" class="item" active-class="active" exact><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="followedChannels" class="container">
|
<div v-if="followedChannels" class="container">
|
||||||
<div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
|
<div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
<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="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="featuredChannels" class="container">
|
<div v-if="featuredChannels" class="container">
|
||||||
<div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
|
<div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" active-class="active" exact><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="lists" class="container">
|
<div v-if="lists" class="container">
|
||||||
<div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div>
|
<div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA v-for="list in lists" :key="list.id" :to="`/timeline/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
|
<MkA v-for="list in lists" :key="list.id" :to="`/timeline/list/${ list.id }`" class="item" active-class="active" exact><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="antennas" class="container">
|
<div v-if="antennas" class="container">
|
||||||
<div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div>
|
<div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/timeline/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
|
<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/timeline/antenna/${ antenna.id }`" class="item" active-class="active" exact><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkAd class="a" :prefer="['square']"/>
|
<MkAd class="a" :prefer="['square']"/>
|
||||||
|
@ -68,8 +68,7 @@
|
||||||
<RouterView :keepalive="{ exclude: ['timeline'] }"/>
|
<RouterView :keepalive="{ exclude: ['timeline'] }"/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<XSide ref="side" class="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
|
<div class="side widgets">
|
||||||
<div class="side widgets" :class="{ sideViewOpening }">
|
|
||||||
<XWidgets/>
|
<XWidgets/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -95,7 +94,6 @@ import { instanceName, url } from '@/config';
|
||||||
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
||||||
import XWidgets from './chat/widgets.vue';
|
import XWidgets from './chat/widgets.vue';
|
||||||
import XCommon from './_common_/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
import XSide from './chat/side.vue';
|
|
||||||
import XHeaderClock from './chat/header-clock.vue';
|
import XHeaderClock from './chat/header-clock.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -106,20 +104,25 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||||
import { store } from './chat/store';
|
import { store } from './chat/store';
|
||||||
import { openAccountMenu, $i } from '@/account';
|
import { openAccountMenu, $i } from '@/account';
|
||||||
|
import { instance } from '@/instance';
|
||||||
const side = ref();
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: '/',
|
path: '/',
|
||||||
hash: 'src',
|
hash: 'tlSrc',
|
||||||
component: $i ? page(() => import('./chat/pages/timeline.vue')) : page(() => import('../pages/welcome.vue')),
|
component: $i ? page(() => import('./chat/pages/timeline.vue')) : page(() => import('../pages/welcome.vue')),
|
||||||
globalCacheKey: 'index',
|
globalCacheKey: 'index',
|
||||||
|
}, {
|
||||||
|
path: '/channels/:channelId',
|
||||||
|
props: { tlSrc: 'channel' },
|
||||||
|
component: page(() => import('./chat/pages/timeline.vue')),
|
||||||
},
|
},
|
||||||
...defaultRoutes,
|
...defaultRoutes,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||||
|
const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||||
|
|
||||||
const mainRouter = useMainRouter(routes);
|
const mainRouter = useMainRouter(routes);
|
||||||
|
|
||||||
|
@ -140,18 +143,7 @@ mainRouter.on('change', () => {
|
||||||
|
|
||||||
watch(path, () => console.log(path));
|
watch(path, () => console.log(path));
|
||||||
|
|
||||||
const sideViewRef = ref(null);
|
|
||||||
provide('sideViewHook', (path) => {
|
|
||||||
sideViewRef.value.navigate(path);
|
|
||||||
});
|
|
||||||
|
|
||||||
const menu = computed(() => [{
|
const menu = computed(() => [{
|
||||||
icon: 'fas fa-columns',
|
|
||||||
text: i18n.ts.openInSideView,
|
|
||||||
action: () => {
|
|
||||||
sideViewRef.value.navigate(path);
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
icon: 'fas fa-window-maximize',
|
icon: 'fas fa-window-maximize',
|
||||||
text: i18n.ts.openInWindow,
|
text: i18n.ts.openInWindow,
|
||||||
action: () => {
|
action: () => {
|
||||||
|
@ -239,12 +231,6 @@ function onContextmenu(e) {
|
||||||
os.contextMenu([{
|
os.contextMenu([{
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: path,
|
text: path,
|
||||||
}, {
|
|
||||||
icon: 'fas fa-columns',
|
|
||||||
text: i18n.ts.openInSideView,
|
|
||||||
action: () => {
|
|
||||||
side.navigate(path);
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-window-maximize',
|
icon: 'fas fa-window-maximize',
|
||||||
text: i18n.ts.openInWindow,
|
text: i18n.ts.openInWindow,
|
||||||
|
@ -471,32 +457,12 @@ onMounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
> .header {
|
|
||||||
z-index: 1000;
|
|
||||||
height: $header-height;
|
|
||||||
background-color: var(--panel);
|
|
||||||
border-bottom: solid 0.5px var(--divider);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .side {
|
> .side {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
border-left: solid 4px var(--divider);
|
border-left: solid 4px var(--divider);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
|
|
||||||
&.widgets.sideViewOpening {
|
|
||||||
@media (max-width: 1400px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .menuDrawer-back {
|
> .menuDrawer-back {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(this.reversed ? 'div' : TransitionGroup, {
|
return h(TransitionGroup, {
|
||||||
class: 'hmjzthxl',
|
class: 'hmjzthxl',
|
||||||
name: this.reversed ? 'list-reversed' : 'list',
|
name: this.reversed ? 'list-reversed' : 'list',
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
|
|
|
@ -4,20 +4,28 @@
|
||||||
<div v-if="date" class="info">
|
<div v-if="date" class="info">
|
||||||
<MkInfo>{{ i18n.ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ i18n.ts.clear }}</button></MkInfo>
|
<MkInfo>{{ i18n.ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ i18n.ts.clear }}</button></MkInfo>
|
||||||
</div>
|
</div>
|
||||||
<div class="body" class="{ channel: channelId }">
|
<div class="body" :class="{ reverse: props.channelId }">
|
||||||
<div class="postform">
|
<div class="postform">
|
||||||
<XPostForm/>
|
<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>
|
||||||
<div ref="scroller" class="tl">
|
<div ref="scroller" class="tl">
|
||||||
<div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ i18n.ts.newNoteRecived }}</button></div>
|
<div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||||
<XNotes ref="tl" class="tl" :pagination="pagination" @queue="queueUpdated"/>
|
<XNotes ref="tlElement" class="tl" :pagination="pagination" @queue="queueUpdated" v-follow="!!props.channelId"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, markRaw, onBeforeUnmount, ref, reactive, withDefaults, watchEffect } from 'vue';
|
import { computed, markRaw, onBeforeUnmount, ref, reactive, withDefaults, watchEffect, watch } from 'vue';
|
||||||
import XNotes from '../notes.vue';
|
import XNotes from '../notes.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
|
@ -28,96 +36,113 @@ import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tlSrc: string;
|
tlSrc?: string;
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
};
|
};
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
tlSrc: 'home',
|
tlSrc: 'home',
|
||||||
channelId: null,
|
channelId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['note']);
|
const emit = defineEmits(['note']);
|
||||||
|
|
||||||
|
const metadata = ref({ icon: 'fas fa-home', title: i18n.ts._timelines.home });
|
||||||
|
definePageMetadata(computed(() => metadata.value));
|
||||||
|
|
||||||
|
const tlElement = ref();
|
||||||
|
|
||||||
|
const prepend = note => {
|
||||||
|
(tlElement.value as any).prepend(note);
|
||||||
|
emit('note');
|
||||||
|
sound.play(note.userId === $i.id ? 'noteMy' : 'note');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeFollowing = () => {
|
||||||
|
if (!tlElement.value.backed) {
|
||||||
|
tlElement.value.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let channel = ref(null);
|
||||||
|
watch(() => props.channelId, async () => {
|
||||||
|
if (props.channelId) {
|
||||||
|
channel.value = await os.api('channels/show', {
|
||||||
|
channelId: props.channelId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const baseQuery = {
|
const baseQuery = {
|
||||||
includeMyRenotes: defaultStore.state.showMyRenotes,
|
includeMyRenotes: defaultStore.state.showMyRenotes,
|
||||||
includeRenotedMyNotes: defaultStore.state.showRenotedMyNotes,
|
includeRenotedMyNotes: defaultStore.state.showRenotedMyNotes,
|
||||||
includeLocalRenotes: defaultStore.state.showLocalRenotes
|
includeLocalRenotes: defaultStore.state.showLocalRenotes
|
||||||
};
|
};
|
||||||
|
let query = reactive({});
|
||||||
const query = reactive({});
|
let endpoint = ref(null);
|
||||||
const queue = ref(0);
|
let connections = reactive([]);
|
||||||
const width = ref(0);
|
let typers = ref([]);
|
||||||
const top = ref(0);
|
let date = ref(null);
|
||||||
const bottom = ref(0);
|
|
||||||
const typers = ref([]);
|
|
||||||
const date = ref(null);
|
|
||||||
|
|
||||||
const tl = ref();
|
|
||||||
const scroller = ref();
|
|
||||||
|
|
||||||
definePageMetadata(computed(() => ({
|
|
||||||
title: i18n.ts.timeline,
|
|
||||||
icon: 'fas fa-home',
|
|
||||||
actions: [{
|
|
||||||
icon: 'fas fa-calendar-alt',
|
|
||||||
text: i18n.ts.jumpToSpecifiedDate,
|
|
||||||
handler: timetravel
|
|
||||||
}],
|
|
||||||
})));
|
|
||||||
|
|
||||||
const prepend = note => {
|
|
||||||
(tl.value as any).prepend(note);
|
|
||||||
|
|
||||||
emit('note');
|
|
||||||
|
|
||||||
sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeFollowing = () => {
|
|
||||||
if (!tl.value.backed) {
|
|
||||||
tl.value.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const connection = ref(null);
|
|
||||||
const connection2 = ref(null);
|
|
||||||
const endpoint = ref(null);
|
|
||||||
|
|
||||||
watchEffect((onCleanup) => {
|
watchEffect((onCleanup) => {
|
||||||
if (props.tlSrc == 'home') {
|
const tlSrcMap = {
|
||||||
endpoint.value = 'notes/timeline';
|
home: {
|
||||||
connection.value = markRaw(stream.useChannel('homeTimeline'));
|
channel: ['homeTimeline'],
|
||||||
connection.value.on('note', prepend);
|
endpoint: 'notes/timeline',
|
||||||
|
metadata: { icon: 'fas fa-home', title: i18n.ts._timelines.home },
|
||||||
connection2.value = markRaw(stream.useChannel('main'));
|
},
|
||||||
connection2.value.on('follow', onChangeFollowing);
|
local: {
|
||||||
connection2.value.on('unfollow', onChangeFollowing);
|
channel: ['localTimeline'],
|
||||||
} else if (props.tlSrc == 'local') {
|
endpoint: 'notes/local-timeline',
|
||||||
endpoint.value = 'notes/local-timeline';
|
metadata: { icon: 'fas fa-comments', title: i18n.ts._timelines.local },
|
||||||
connection.value = markRaw(stream.useChannel('localTimeline'));
|
},
|
||||||
connection.value.on('note', prepend);
|
social: {
|
||||||
} else if (props.tlSrc == 'social') {
|
channel: ['hybridTimeline'],
|
||||||
endpoint.value = 'notes/hybrid-timeline';
|
endpoint: 'notes/hybrid-timeline',
|
||||||
connection.value = markRaw(stream.useChannel('hybridTimeline'));
|
metadata: { icon: 'fas fa-share-alt', title: i18n.ts._timelines.social },
|
||||||
connection.value.on('note', prepend);
|
},
|
||||||
} else if (props.tlSrc == 'global') {
|
global: {
|
||||||
endpoint.value = 'notes/global-timeline';
|
channel: ['globalTimeline'],
|
||||||
connection.value = markRaw(stream.useChannel('globalTimeline'));
|
endpoint: 'notes/global-timeline',
|
||||||
connection.value.on('note', prepend);
|
metadata: { icon: 'fas fa-globe', title: i18n.ts._timelines.global },
|
||||||
} else if (props.tlSrc == 'channel') {
|
},
|
||||||
|
channel: {
|
||||||
|
channel: ['channel', { channelId: props.channelId }],
|
||||||
|
endpoint: 'channels/timeline',
|
||||||
|
metadata: { icon: 'fas fa-satellite-dish', title: channel.value?.name, subtitle: channel.value?.description },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { channel: streamChannelParams, endpoint: _endpoint, metadata: _metadata } = tlSrcMap[props.tlSrc];
|
||||||
|
endpoint.value = _endpoint;
|
||||||
|
metadata.value = _metadata;
|
||||||
|
const tlStream = markRaw(stream.useChannel(...streamChannelParams));
|
||||||
|
tlStream.on('note', prepend);
|
||||||
|
connections.push(tlStream);
|
||||||
|
|
||||||
|
if (props.tlSrc == 'home' || props.tlSrc == 'social') {
|
||||||
|
const mainStream = markRaw(stream.useChannel('main'));
|
||||||
|
mainStream.on('follow', onChangeFollowing);
|
||||||
|
mainStream.on('unfollow', onChangeFollowing);
|
||||||
|
connections.push(mainStream);
|
||||||
|
}
|
||||||
|
if (props.tlSrc == 'channel') {
|
||||||
|
tlStream.on('typers', _typers => {
|
||||||
|
typers.value = $i ? _typers.filter(u => u.id !== $i.id) : _typers;
|
||||||
|
});
|
||||||
|
query.channelId = props.channelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
connection.value.dispose();
|
connections.forEach((connection) => connection.dispose());
|
||||||
if (connection2.value) connection2.value.dispose();
|
connections = [];
|
||||||
|
query = {};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const pagination = computed(() => ({
|
const pagination = computed(() => ({
|
||||||
endpoint: endpoint.value,
|
endpoint: endpoint.value,
|
||||||
|
reversed: props.tlSrc == 'channel',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: init => ({
|
params: init => ({
|
||||||
untilDate: date.value?.getTime(),
|
untilDate: date.value?.getTime(),
|
||||||
|
@ -125,6 +150,16 @@ const pagination = computed(() => ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function timetravel(target?: Date) {
|
||||||
|
date.value = target;
|
||||||
|
tlElement.value.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const top = ref(0);
|
||||||
|
const bottom = ref(0);
|
||||||
|
const width = ref(0);
|
||||||
|
const scroller = ref();
|
||||||
|
const queue = ref(0);
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
scroller.value.focus();
|
scroller.value.focus();
|
||||||
|
@ -144,11 +179,6 @@ function queueUpdated(q) {
|
||||||
}
|
}
|
||||||
queue.value = q;
|
queue.value = q;
|
||||||
};
|
};
|
||||||
|
|
||||||
function timetravel(target?: Date) {
|
|
||||||
date.value = target;
|
|
||||||
tl.value.reload();
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -162,10 +192,45 @@ function timetravel(target?: Date) {
|
||||||
padding: 16px 16px 0 16px;
|
padding: 16px 16px 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.postform {
|
> .body {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .postform {
|
||||||
padding: 16px 16px 0 16px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
padding: 0 16px 16px 16px;
|
padding: 0 16px 16px 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -192,25 +257,6 @@ function timetravel(target?: Date) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -388,7 +388,7 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
os.popup(import('@/components/MkVisibilityPicker.vue'), {
|
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
|
||||||
currentVisibility: this.visibility,
|
currentVisibility: this.visibility,
|
||||||
currentLocalOnly: this.localOnly,
|
currentLocalOnly: this.localOnly,
|
||||||
src: this.$refs.visibilityButton
|
src: this.$refs.visibilityButton
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="component" class="mrajymqm _narrow_">
|
|
||||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
|
||||||
<MkHeader class="title" :info="pageInfo" :center="false"/>
|
|
||||||
</header>
|
|
||||||
<component :is="component" v-bind="props" class="body"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, reactive, computed } from 'vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { useRouter } from '@/router';
|
|
||||||
import { url as baseUrl } from '@/config';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const history = reactive([]);
|
|
||||||
const path = ref(null);
|
|
||||||
const component = ref(null);
|
|
||||||
const props = ref({});
|
|
||||||
|
|
||||||
const emit = defineEmits(['open', 'close']);
|
|
||||||
|
|
||||||
const url = computed(() => baseUrl + path.value)
|
|
||||||
|
|
||||||
const pageInfo = null;
|
|
||||||
|
|
||||||
function navigate(path, record = true) {
|
|
||||||
if (record && path.value) history.push(path.value);
|
|
||||||
path.value = path;
|
|
||||||
const { component: newComponent, props: newProps } = router.resolve(path);
|
|
||||||
component.value = newComponent;
|
|
||||||
props.value = newProps;
|
|
||||||
emit('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
function back() {
|
|
||||||
navigate(history.pop(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
path.value = null;
|
|
||||||
component.value = null;
|
|
||||||
props.value = {};
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onContextmenu(e) {
|
|
||||||
os.contextMenu([{
|
|
||||||
type: 'label',
|
|
||||||
text: this.path,
|
|
||||||
}, {
|
|
||||||
icon: 'fas fa-expand-alt',
|
|
||||||
text: i18n.ts.showInPage,
|
|
||||||
action: () => {
|
|
||||||
router.push(path);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
icon: 'fas fa-window-maximize',
|
|
||||||
text: i18n.ts.openInWindow,
|
|
||||||
action: () => {
|
|
||||||
os.pageWindow(path);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}, null, {
|
|
||||||
icon: 'fas fa-external-link-alt',
|
|
||||||
text: i18n.ts.openInNewTab,
|
|
||||||
action: () => {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
icon: 'fas fa-link',
|
|
||||||
text: i18n.ts.copyLink,
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(url);
|
|
||||||
}
|
|
||||||
}], e);
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ navHook: navigate });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mrajymqm {
|
|
||||||
$header-height: 55px; // TODO: どこかに集約したい
|
|
||||||
|
|
||||||
--root-margin: 16px;
|
|
||||||
--margin: var(--marginHalf);
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
> .header {
|
|
||||||
display: flex;
|
|
||||||
position: sticky;
|
|
||||||
z-index: 1000;
|
|
||||||
top: 0;
|
|
||||||
height: $header-height;
|
|
||||||
width: 100%;
|
|
||||||
font-weight: bold;
|
|
||||||
//background-color: var(--panel);
|
|
||||||
-webkit-backdrop-filter: var(--blur, blur(32px));
|
|
||||||
backdrop-filter: var(--blur, blur(32px));
|
|
||||||
background-color: var(--header);
|
|
||||||
border-bottom: solid 0.5px var(--divider);
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
> ._button {
|
|
||||||
height: $header-height;
|
|
||||||
width: $header-height;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--fgHighlighted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
Reference in New Issue