Compare commits

..

4 Commits

Author SHA1 Message Date
Derek ead8a4eb3b Bump 2022-11-16 10:32:59 -05:00
Derek 1a90d4f5f6 Chatui part 5 2022-11-16 10:32:24 -05:00
Derek ddb612853b Mascot swap! 2022-10-24 17:25:25 -04:00
Derek 581bbc6021 ChatUI modernization part 4 2022-10-24 13:41:09 -04:00
13 changed files with 193 additions and 290 deletions

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "12.119.0+birb3-2",
"version": "12.119.0+birb4",
"codename": "indigo",
"repository": {
"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

View File

@ -15,23 +15,34 @@ import { useRouter } from '@/router';
const props = withDefaults(defineProps<{
to: string;
exact?: boolean;
activeClass?: null | string;
behavior?: null | 'window' | 'browser' | 'modalWindow';
}>(), {
activeClass: null,
behavior: null,
exact: false,
});
const router = useRouter();
const currentPath = $ref(router.getCurrentPath());
router.addListener('change', ({ path }) => {
currentPath = path;
});
const active = $computed(() => {
if (props.activeClass == null) return false;
const resolved = router.resolve(props.to);
if (resolved == null) return false;
if (resolved.route.path === router.currentRoute.value.path) return true;
if (resolved.route.name == null) return false;
if (router.currentRoute.value.name == null) return false;
return resolved.route.name === router.currentRoute.value.name;
if (props.exact) {
return currentPath === props.to;
} else {
const resolved = router.resolve(props.to);
if (resolved == null) return false;
if (resolved.route.path === router.currentRoute.value.path) return true;
if (resolved.route.name == null) return false;
if (router.currentRoute.value.name == null) return false;
return resolved.route.name === router.currentRoute.value.name;
}
});
function onContextmenu(ev) {

View File

@ -14,12 +14,15 @@ export default {
const height = container.scrollHeight;
isBottom = (pos + viewHeight > height - 32);
}, { passive: true });
container.scrollTop = container.scrollHeight;
console.log('mount', container.scrollHeight);
container.scroll({ top: container.scrollHeight, behavior: 'smooth' });
const ro = new ResizeObserver((entries, observer) => {
console.log('resize', container.scrollHeight);
if (isBottom) {
const height = container.scrollHeight;
container.scrollTop = height;
container.scroll({ top: height, behavior: 'instant' });
}
});

View File

@ -11,6 +11,7 @@ import anim from './anim';
import clickAnime from './click-anime';
import panel from './panel';
import adaptiveBorder from './adaptive-border';
import follow from './follow-append';
export default function(app: App) {
app.directive('userPreview', userPreview);
@ -25,4 +26,5 @@ export default function(app: App) {
app.directive('click-anime', clickAnime);
app.directive('panel', panel);
app.directive('adaptive-border', adaptiveBorder);
app.directive('follow', follow);
}

View File

@ -12,6 +12,7 @@ export type RouteDef = {
loginRequired?: boolean;
name?: string;
hash?: string;
props?: Record<string, any>;
globalCacheKey?: string;
children?: RouteDef[];
};
@ -65,7 +66,7 @@ export class Router extends EventEmitter<{
beforePath: string;
path: string;
route: RouteDef | null;
props: Map<string, string> | null;
props: Map<string, any> | null;
key: string;
}) => void;
same: () => void;
@ -106,7 +107,7 @@ export class Router extends EventEmitter<{
forEachRouteLoop:
for (const route of routes) {
let parts = [ ..._parts ];
const props = new Map<string, string>();
const props = new Map<string, any>();
pathMatchLoop:
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 {
route,
props,

View File

@ -17,34 +17,34 @@
<div class="container">
<div class="header">{{ $ts.timeline }}</div>
<div class="body">
<MkA to="/#home" class="item" active-class="active"><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="/#social" class="item" :class="{ active: tl === 'social' }"><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="/#home" class="item" active-class="active" exact><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</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" v-if="isLocalTimelineAvailable" class="item" active-class="active" exact><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</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 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="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 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="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 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="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 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="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>
<MkAd class="a" :prefer="['square']"/>
@ -68,8 +68,7 @@
<RouterView :keepalive="{ exclude: ['timeline'] }"/>
</main>
<XSide ref="side" class="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
<div class="side widgets" :class="{ sideViewOpening }">
<div class="side widgets">
<XWidgets/>
</div>
@ -95,7 +94,6 @@ import { instanceName, url } from '@/config';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import XWidgets from './chat/widgets.vue';
import XCommon from './_common_/common.vue';
import XSide from './chat/side.vue';
import XHeaderClock from './chat/header-clock.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
@ -106,20 +104,25 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
import { store } from './chat/store';
import { openAccountMenu, $i } from '@/account';
const side = ref();
import { instance } from '@/instance';
const routes = [
{
name: 'index',
path: '/',
hash: 'src',
hash: 'tlSrc',
component: $i ? page(() => import('./chat/pages/timeline.vue')) : page(() => import('../pages/welcome.vue')),
globalCacheKey: 'index',
}, {
path: '/channels/:channelId',
props: { tlSrc: 'channel' },
component: page(() => import('./chat/pages/timeline.vue')),
},
...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);
@ -140,18 +143,7 @@ mainRouter.on('change', () => {
watch(path, () => console.log(path));
const sideViewRef = ref(null);
provide('sideViewHook', (path) => {
sideViewRef.value.navigate(path);
});
const menu = computed(() => [{
icon: 'fas fa-columns',
text: i18n.ts.openInSideView,
action: () => {
sideViewRef.value.navigate(path);
}
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
@ -239,12 +231,6 @@ function onContextmenu(e) {
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: 'fas fa-columns',
text: i18n.ts.openInSideView,
action: () => {
side.navigate(path);
}
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
@ -471,32 +457,12 @@ onMounted(() => {
position: relative;
background: var(--panel);
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 {
width: 350px;
border-left: solid 4px var(--divider);
background: var(--panel);
&.widgets.sideViewOpening {
@media (max-width: 1400px) {
display: none;
}
}
}
> .menuDrawer-back {

View File

@ -30,7 +30,7 @@ export default defineComponent({
});
}
return h(this.reversed ? 'div' : TransitionGroup, {
return h(TransitionGroup, {
class: 'hmjzthxl',
name: this.reversed ? 'list-reversed' : 'list',
tag: 'div',

View File

@ -4,20 +4,28 @@
<div v-if="date" class="info">
<MkInfo>{{ i18n.ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ i18n.ts.clear }}</button></MkInfo>
</div>
<div class="body" class="{ channel: channelId }">
<div class="body" :class="{ reverse: props.channelId }">
<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 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>
<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>
</template>
<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 * as os from '@/os';
import { stream } from '@/stream';
@ -28,96 +36,113 @@ import MkInfo from '@/components/MkInfo.vue';
import { definePageMetadata } from '@/scripts/page-metadata';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { $i } from '@/account';
type Props = {
tlSrc: string;
tlSrc?: string;
channelId?: string;
};
const props = withDefaults(defineProps<Props>(), {
tlSrc: 'home',
channelId: null,
});
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 = {
includeMyRenotes: defaultStore.state.showMyRenotes,
includeRenotedMyNotes: defaultStore.state.showRenotedMyNotes,
includeLocalRenotes: defaultStore.state.showLocalRenotes
};
const query = reactive({});
const queue = ref(0);
const width = ref(0);
const top = ref(0);
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);
let query = reactive({});
let endpoint = ref(null);
let connections = reactive([]);
let typers = ref([]);
let date = ref(null);
watchEffect((onCleanup) => {
if (props.tlSrc == 'home') {
endpoint.value = 'notes/timeline';
connection.value = markRaw(stream.useChannel('homeTimeline'));
connection.value.on('note', prepend);
connection2.value = markRaw(stream.useChannel('main'));
connection2.value.on('follow', onChangeFollowing);
connection2.value.on('unfollow', onChangeFollowing);
} else if (props.tlSrc == 'local') {
endpoint.value = 'notes/local-timeline';
connection.value = markRaw(stream.useChannel('localTimeline'));
connection.value.on('note', prepend);
} else if (props.tlSrc == 'social') {
endpoint.value = 'notes/hybrid-timeline';
connection.value = markRaw(stream.useChannel('hybridTimeline'));
connection.value.on('note', prepend);
} else if (props.tlSrc == 'global') {
endpoint.value = 'notes/global-timeline';
connection.value = markRaw(stream.useChannel('globalTimeline'));
connection.value.on('note', prepend);
} else if (props.tlSrc == 'channel') {
const tlSrcMap = {
home: {
channel: ['homeTimeline'],
endpoint: 'notes/timeline',
metadata: { icon: 'fas fa-home', title: i18n.ts._timelines.home },
},
local: {
channel: ['localTimeline'],
endpoint: 'notes/local-timeline',
metadata: { icon: 'fas fa-comments', title: i18n.ts._timelines.local },
},
social: {
channel: ['hybridTimeline'],
endpoint: 'notes/hybrid-timeline',
metadata: { icon: 'fas fa-share-alt', title: i18n.ts._timelines.social },
},
global: {
channel: ['globalTimeline'],
endpoint: 'notes/global-timeline',
metadata: { icon: 'fas fa-globe', title: i18n.ts._timelines.global },
},
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(() => {
connection.value.dispose();
if (connection2.value) connection2.value.dispose();
connections.forEach((connection) => connection.dispose());
connections = [];
query = {};
});
});
const pagination = computed(() => ({
endpoint: endpoint.value,
reversed: props.tlSrc == 'channel',
limit: 10,
params: init => ({
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() {
scroller.value.focus();
@ -144,11 +179,6 @@ function queueUpdated(q) {
}
queue.value = q;
};
function timetravel(target?: Date) {
date.value = target;
tl.value.reload();
};
</script>
<style lang="scss" scoped>
@ -162,8 +192,43 @@ function timetravel(target?: Date) {
padding: 16px 16px 0 16px;
}
.postform {
padding: 16px 16px 0 16px;
> .body {
overflow: auto;
flex: 1;
display: flex;
flex-direction: column;
> .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;
}
}
}
}
.bottom {
@ -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>

View File

@ -388,7 +388,7 @@ export default defineComponent({
return;
}
os.popup(import('@/components/MkVisibilityPicker.vue'), {
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
currentVisibility: this.visibility,
currentLocalOnly: this.localOnly,
src: this.$refs.visibilityButton

View File

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