enhance(client): リスト、アンテナタイムラインを個別ページとして分割
This commit is contained in:
parent
b35ca3b739
commit
72a49f334a
9 changed files with 348 additions and 70 deletions
|
@ -8,13 +8,14 @@
|
|||
-->
|
||||
|
||||
## 12.x.x (unreleased)
|
||||
- ActivityPub: deliverキューのメモリ使用量を削減
|
||||
|
||||
### Improvements
|
||||
- ActivityPub: リモートユーザーのDeleteアクティビティに対応
|
||||
- ActivityPub: add resolver check for blocked instance
|
||||
- ActivityPub: deliverキューのメモリ使用量を削減
|
||||
- アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように
|
||||
- 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように
|
||||
- リスト、アンテナタイムラインを個別ページとして分割
|
||||
- UIの改善
|
||||
|
||||
### Bugfixes
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { defineComponent, ref, unref } from 'vue';
|
||||
import { focusPrev, focusNext } from '@client/scripts/focus';
|
||||
import contains from '@client/scripts/contains';
|
||||
|
||||
|
@ -79,21 +79,26 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const items = ref(this.items.filter(item => item !== undefined));
|
||||
watch: {
|
||||
items: {
|
||||
handler() {
|
||||
const items = ref(unref(this.items).filter(item => item !== undefined));
|
||||
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const item = items.value[i];
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const item = items.value[i];
|
||||
|
||||
if (item && item.then) { // if item is Promise
|
||||
items.value[i] = { type: 'pending' };
|
||||
item.then(actualItem => {
|
||||
items.value[i] = actualItem;
|
||||
});
|
||||
}
|
||||
if (item && item.then) { // if item is Promise
|
||||
items.value[i] = { type: 'pending' };
|
||||
item.then(actualItem => {
|
||||
items.value[i] = actualItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._items = items;
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
|
||||
this._items = items;
|
||||
},
|
||||
mounted() {
|
||||
if (this.viaKeyboard) {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { search } from '@client/scripts/search';
|
||||
import * as os from '@client/os';
|
||||
import { i18n } from '@client/i18n';
|
||||
import { $i } from './account';
|
||||
import { unisonReload } from '@client/scripts/unison-reload';
|
||||
import { router } from './router';
|
||||
|
||||
export const menuDef = {
|
||||
notifications: {
|
||||
|
@ -58,7 +59,26 @@ export const menuDef = {
|
|||
title: 'lists',
|
||||
icon: 'fas fa-list-ul',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/lists',
|
||||
active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')),
|
||||
action: (ev) => {
|
||||
const items = ref([{
|
||||
type: 'pending'
|
||||
}]);
|
||||
os.api('users/lists/list').then(lists => {
|
||||
const _items = [...lists.map(list => ({
|
||||
type: 'link',
|
||||
text: list.name,
|
||||
to: `/timeline/list/${list.id}`
|
||||
})), null, {
|
||||
type: 'link',
|
||||
to: '/my/lists',
|
||||
text: i18n.locale.manageLists,
|
||||
icon: 'fas fa-cog',
|
||||
}];
|
||||
items.value = _items;
|
||||
});
|
||||
os.popupMenu(items, ev.currentTarget || ev.target);
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
title: 'groups',
|
||||
|
|
|
@ -372,7 +372,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
|
|||
});
|
||||
}
|
||||
|
||||
export function popupMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
|
||||
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let dispose;
|
||||
popup(import('@client/components/ui/popup-menu.vue'), {
|
||||
|
|
147
src/client/pages/antenna-timeline.vue
Normal file
147
src/client/pages/antenna-timeline.vue
Normal file
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }">
|
||||
<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
|
||||
<div class="tl _block">
|
||||
<XTimeline ref="tl" class="tl"
|
||||
:key="antennaId"
|
||||
src="antenna"
|
||||
:antenna="antennaId"
|
||||
:sound="true"
|
||||
@before="before()"
|
||||
@after="after()"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, computed } from 'vue';
|
||||
import Progress from '@client/scripts/loading';
|
||||
import XTimeline from '@client/components/timeline.vue';
|
||||
import { scroll } from '@client/scripts/scroll';
|
||||
import * as os from '@client/os';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XTimeline,
|
||||
},
|
||||
|
||||
props: {
|
||||
antennaId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
antenna: null,
|
||||
queue: 0,
|
||||
[symbols.PAGE_INFO]: computed(() => this.antenna ? {
|
||||
title: this.antenna.name,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: this.$ts.jumpToSpecifiedDate,
|
||||
handler: this.timetravel
|
||||
}, {
|
||||
icon: 'fas fa-cog',
|
||||
text: this.$ts.settings,
|
||||
handler: this.settings
|
||||
}],
|
||||
} : null),
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
't': this.focus
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
antennaId: {
|
||||
async handler() {
|
||||
this.antenna = await os.api('antennas/show', {
|
||||
antennaId: this.antennaId
|
||||
});
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
before() {
|
||||
Progress.start();
|
||||
},
|
||||
|
||||
after() {
|
||||
Progress.done();
|
||||
},
|
||||
|
||||
queueUpdated(q) {
|
||||
this.queue = q;
|
||||
},
|
||||
|
||||
top() {
|
||||
scroll(this.$el, 0);
|
||||
},
|
||||
|
||||
async timetravel() {
|
||||
const { canceled, result: date } = await os.dialog({
|
||||
title: this.$ts.date,
|
||||
input: {
|
||||
type: 'date'
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.$refs.tl.timetravel(new Date(date));
|
||||
},
|
||||
|
||||
settings() {
|
||||
this.$router.push(`/my/antennas/${this.antennaId}`);
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.tl as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tqmomfks {
|
||||
padding: var(--margin);
|
||||
|
||||
> .new {
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 16px);
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
|
||||
> button {
|
||||
display: block;
|
||||
margin: var(--margin) auto 0 auto;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
> .tl {
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
&.min-width_800px {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -6,11 +6,8 @@
|
|||
<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
|
||||
<div class="tl _block">
|
||||
<XTimeline ref="tl" class="tl"
|
||||
:key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
|
||||
:key="src"
|
||||
:src="src"
|
||||
:list="list ? list.id : null"
|
||||
:antenna="antenna ? antenna.id : null"
|
||||
:channel="channel ? channel.id : null"
|
||||
:sound="true"
|
||||
@before="before()"
|
||||
@after="after()"
|
||||
|
@ -41,10 +38,6 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
src: 'home',
|
||||
list: null,
|
||||
antenna: null,
|
||||
channel: null,
|
||||
menuOpened: false,
|
||||
queue: 0,
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: this.$ts.timeline,
|
||||
|
@ -116,32 +109,10 @@ export default defineComponent({
|
|||
src() {
|
||||
this.showNav = false;
|
||||
},
|
||||
list(x) {
|
||||
this.showNav = false;
|
||||
if (x != null) this.antenna = null;
|
||||
if (x != null) this.channel = null;
|
||||
},
|
||||
antenna(x) {
|
||||
this.showNav = false;
|
||||
if (x != null) this.list = null;
|
||||
if (x != null) this.channel = null;
|
||||
},
|
||||
channel(x) {
|
||||
this.showNav = false;
|
||||
if (x != null) this.antenna = null;
|
||||
if (x != null) this.list = null;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.src = this.$store.state.tl.src;
|
||||
if (this.src === 'list') {
|
||||
this.list = this.$store.state.tl.arg;
|
||||
} else if (this.src === 'antenna') {
|
||||
this.antenna = this.$store.state.tl.arg;
|
||||
} else if (this.src === 'channel') {
|
||||
this.channel = this.$store.state.tl.arg;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -164,12 +135,9 @@ export default defineComponent({
|
|||
async chooseList(ev) {
|
||||
const lists = await os.api('users/lists/list');
|
||||
const items = lists.map(list => ({
|
||||
type: 'link',
|
||||
text: list.name,
|
||||
action: () => {
|
||||
this.list = list;
|
||||
this.src = 'list';
|
||||
this.saveSrc();
|
||||
}
|
||||
to: `/timeline/list/${list.id}`
|
||||
}));
|
||||
os.popupMenu(items, ev.currentTarget || ev.target);
|
||||
},
|
||||
|
@ -177,13 +145,10 @@ export default defineComponent({
|
|||
async chooseAntenna(ev) {
|
||||
const antennas = await os.api('antennas/list');
|
||||
const items = antennas.map(antenna => ({
|
||||
type: 'link',
|
||||
text: antenna.name,
|
||||
indicate: antenna.hasUnreadNote,
|
||||
action: () => {
|
||||
this.antenna = antenna;
|
||||
this.src = 'antenna';
|
||||
this.saveSrc();
|
||||
}
|
||||
to: `/timeline/antenna/${antenna.id}`
|
||||
}));
|
||||
os.popupMenu(items, ev.currentTarget || ev.target);
|
||||
},
|
||||
|
@ -191,15 +156,10 @@ export default defineComponent({
|
|||
async chooseChannel(ev) {
|
||||
const channels = await os.api('channels/followed');
|
||||
const items = channels.map(channel => ({
|
||||
type: 'link',
|
||||
text: channel.name,
|
||||
indicate: channel.hasUnreadNote,
|
||||
action: () => {
|
||||
// NOTE: チャンネルタイムラインをこのコンポーネントで表示するようにすると投稿フォームはどうするかなどの問題が生じるのでとりあえずページ遷移で
|
||||
//this.channel = channel;
|
||||
//this.src = 'channel';
|
||||
//this.saveSrc();
|
||||
this.$router.push(`/channels/${channel.id}`);
|
||||
}
|
||||
to: `/channels/${channel.id}`
|
||||
}));
|
||||
os.popupMenu(items, ev.currentTarget || ev.target);
|
||||
},
|
||||
|
@ -207,10 +167,6 @@ export default defineComponent({
|
|||
saveSrc() {
|
||||
this.$store.set('tl', {
|
||||
src: this.src,
|
||||
arg:
|
||||
this.src === 'list' ? this.list :
|
||||
this.src === 'antenna' ? this.antenna :
|
||||
this.channel
|
||||
});
|
||||
},
|
||||
|
||||
|
|
147
src/client/pages/user-list-timeline.vue
Normal file
147
src/client/pages/user-list-timeline.vue
Normal file
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }">
|
||||
<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
|
||||
<div class="tl _block">
|
||||
<XTimeline ref="tl" class="tl"
|
||||
:key="listId"
|
||||
src="list"
|
||||
:list="listId"
|
||||
:sound="true"
|
||||
@before="before()"
|
||||
@after="after()"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, computed } from 'vue';
|
||||
import Progress from '@client/scripts/loading';
|
||||
import XTimeline from '@client/components/timeline.vue';
|
||||
import { scroll } from '@client/scripts/scroll';
|
||||
import * as os from '@client/os';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XTimeline,
|
||||
},
|
||||
|
||||
props: {
|
||||
listId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
list: null,
|
||||
queue: 0,
|
||||
[symbols.PAGE_INFO]: computed(() => this.list ? {
|
||||
title: this.list.name,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: this.$ts.jumpToSpecifiedDate,
|
||||
handler: this.timetravel
|
||||
}, {
|
||||
icon: 'fas fa-cog',
|
||||
text: this.$ts.settings,
|
||||
handler: this.settings
|
||||
}],
|
||||
} : null),
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
't': this.focus
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
listId: {
|
||||
async handler() {
|
||||
this.list = await os.api('users/lists/show', {
|
||||
listId: this.listId
|
||||
});
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
before() {
|
||||
Progress.start();
|
||||
},
|
||||
|
||||
after() {
|
||||
Progress.done();
|
||||
},
|
||||
|
||||
queueUpdated(q) {
|
||||
this.queue = q;
|
||||
},
|
||||
|
||||
top() {
|
||||
scroll(this.$el, 0);
|
||||
},
|
||||
|
||||
settings() {
|
||||
this.$router.push(`/my/lists/${this.listId}`);
|
||||
},
|
||||
|
||||
async timetravel() {
|
||||
const { canceled, result: date } = await os.dialog({
|
||||
title: this.$ts.date,
|
||||
input: {
|
||||
type: 'date'
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.$refs.tl.timetravel(new Date(date));
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.tl as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.eqqrhokj {
|
||||
padding: var(--margin);
|
||||
|
||||
> .new {
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 16px);
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
|
||||
> button {
|
||||
display: block;
|
||||
margin: var(--margin) auto 0 auto;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
> .tl {
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
&.min-width_800px {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -48,6 +48,8 @@ const defaultRoutes = [
|
|||
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
|
||||
{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
|
||||
{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
|
||||
{ path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) },
|
||||
{ path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) },
|
||||
{ path: '/my/notifications', component: page('notifications') },
|
||||
{ path: '/my/favorites', component: page('favorites') },
|
||||
{ path: '/my/messages', component: page('messages') },
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</MkA>
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
|
||||
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
|
||||
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</component>
|
||||
|
|
Loading…
Reference in a new issue