Improve desktop UX (#4262)
* wip * wip * wip * wip * wip * wip * Merge * wip * wip * wip * wip * wip * wip
This commit is contained in:
		| @@ -1,396 +0,0 @@ | ||||
| <template> | ||||
| <div class="mk-home" :data-customize="customize"> | ||||
| 	<div class="customize" v-if="customize"> | ||||
| 		<router-link to="/"><fa icon="check"/>{{ $t('done') }}</router-link> | ||||
| 		<div> | ||||
| 			<div class="adder"> | ||||
| 				<p>{{ $t('add-widget') }}</p> | ||||
| 				<select v-model="widgetAdderSelected"> | ||||
| 					<option value="profile">{{ $t('@.widgets.profile') }}</option> | ||||
| 					<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> | ||||
| 					<option value="calendar">{{ $t('@.widgets.calendar') }}</option> | ||||
| 					<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option> | ||||
| 					<option value="activity">{{ $t('@.widgets.activity') }}</option> | ||||
| 					<option value="rss">{{ $t('@.widgets.rss') }}</option> | ||||
| 					<option value="trends">{{ $t('@.widgets.trends') }}</option> | ||||
| 					<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> | ||||
| 					<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> | ||||
| 					<option value="version">{{ $t('@.widgets.version') }}</option> | ||||
| 					<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option> | ||||
| 					<option value="notifications">{{ $t('@.widgets.notifications') }}</option> | ||||
| 					<option value="users">{{ $t('@.widgets.users') }}</option> | ||||
| 					<option value="polls">{{ $t('@.widgets.polls') }}</option> | ||||
| 					<option value="post-form">{{ $t('@.widgets.post-form') }}</option> | ||||
| 					<option value="messaging">{{ $t('@.widgets.messaging') }}</option> | ||||
| 					<option value="memo">{{ $t('@.widgets.memo') }}</option> | ||||
| 					<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> | ||||
| 					<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> | ||||
| 					<option value="server">{{ $t('@.widgets.server') }}</option> | ||||
| 					<option value="nav">{{ $t('@.widgets.nav') }}</option> | ||||
| 					<option value="tips">{{ $t('@.widgets.tips') }}</option> | ||||
| 				</select> | ||||
| 				<button @click="addWidget">{{ $t('add') }}</button> | ||||
| 			</div> | ||||
| 			<div class="trash"> | ||||
| 				<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable> | ||||
| 				<p>{{ $t('@.trash') }}</p> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }"> | ||||
| 		<template v-if="customize"> | ||||
| 			<x-draggable v-for="place in ['left', 'right']" | ||||
| 				:list="widgets[place]" | ||||
| 				:class="place" | ||||
| 				:data-place="place" | ||||
| 				:options="{ group: 'x', animation: 150 }" | ||||
| 				@sort="onWidgetSort" | ||||
| 				:key="place" | ||||
| 			> | ||||
| 				<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)"> | ||||
| 					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/> | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| 			<div class="main"> | ||||
| 				<a @click="hint">{{ $t('@.customization-tips.title') }}</a> | ||||
| 				<div> | ||||
| 					<mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/> | ||||
| 					<mk-timeline ref="tl" @loaded="onTlLoaded"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 		<template v-else> | ||||
| 			<div v-for="place in ['left', 'right']" :class="place"> | ||||
| 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp" platform="desktop"/> | ||||
| 			</div> | ||||
| 			<div class="main"> | ||||
| 				<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/> | ||||
| 				<mk-timeline class="tl" ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import * as uuid from 'uuid'; | ||||
|  | ||||
| const defaultDesktopHomeWidgets = { | ||||
| 	left: [ | ||||
| 		'profile', | ||||
| 		'calendar', | ||||
| 		'activity', | ||||
| 		'rss', | ||||
| 		'hashtags', | ||||
| 		'photo-stream', | ||||
| 		'version' | ||||
| 	], | ||||
| 	right: [ | ||||
| 		'broadcast', | ||||
| 		'notifications', | ||||
| 		'users', | ||||
| 		'polls', | ||||
| 		'server', | ||||
| 		'nav', | ||||
| 		'tips' | ||||
| 	] | ||||
| }; | ||||
|  | ||||
| //#region Construct home data | ||||
| const _defaultDesktopHomeWidgets = []; | ||||
|  | ||||
| for (const widget of defaultDesktopHomeWidgets.left) { | ||||
| 	_defaultDesktopHomeWidgets.push({ | ||||
| 		name: widget, | ||||
| 		id: uuid(), | ||||
| 		place: 'left', | ||||
| 		data: {} | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| for (const widget of defaultDesktopHomeWidgets.right) { | ||||
| 	_defaultDesktopHomeWidgets.push({ | ||||
| 		name: widget, | ||||
| 		id: uuid(), | ||||
| 		place: 'right', | ||||
| 		data: {} | ||||
| 	}); | ||||
| } | ||||
| //#endregion | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/home.vue'), | ||||
| 	components: { | ||||
| 		XDraggable | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		customize: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		mode: { | ||||
| 			type: String, | ||||
| 			default: 'timeline' | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			connection: null, | ||||
| 			widgetAdderSelected: null, | ||||
| 			trash: [] | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		home(): any[] { | ||||
| 			return this.$store.state.settings.home || []; | ||||
| 		}, | ||||
| 		left(): any[] { | ||||
| 			return this.home.filter(w => w.place == 'left'); | ||||
| 		}, | ||||
| 		right(): any[] { | ||||
| 			return this.home.filter(w => w.place == 'right'); | ||||
| 		}, | ||||
| 		widgets(): any { | ||||
| 			return { | ||||
| 				left: this.left, | ||||
| 				right: this.right | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		if (this.$store.state.settings.home == null) { | ||||
| 			this.$root.api('i/update_home', { | ||||
| 				home: _defaultDesktopHomeWidgets | ||||
| 			}).then(() => { | ||||
| 				this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		hint() { | ||||
| 			this.$root.dialog({ | ||||
| 				title: this.$t('@.customization-tips.title'), | ||||
| 				text: this.$t('@.customization-tips.paragraph') | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onTlLoaded() { | ||||
| 			this.$emit('loaded'); | ||||
| 		}, | ||||
|  | ||||
| 		onWidgetContextmenu(widgetId) { | ||||
| 			const w = (this.$refs[widgetId] as any)[0]; | ||||
| 			if (w.func) w.func(); | ||||
| 		}, | ||||
|  | ||||
| 		onWidgetSort() { | ||||
| 			this.saveHome(); | ||||
| 		}, | ||||
|  | ||||
| 		onTrash(evt) { | ||||
| 			this.saveHome(); | ||||
| 		}, | ||||
|  | ||||
| 		addWidget() { | ||||
| 			this.$store.dispatch('settings/addHomeWidget', { | ||||
| 				name: this.widgetAdderSelected, | ||||
| 				id: uuid(), | ||||
| 				place: 'left', | ||||
| 				data: {} | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		saveHome() { | ||||
| 			const left = this.widgets.left; | ||||
| 			const right = this.widgets.right; | ||||
| 			this.$store.commit('settings/setHome', left.concat(right)); | ||||
| 			for (const w of left) w.place = 'left'; | ||||
| 			for (const w of right) w.place = 'right'; | ||||
| 			this.$root.api('i/update_home', { | ||||
| 				home: this.home | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		warp(date) { | ||||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-home | ||||
| 	display block | ||||
|  | ||||
| 	&[data-customize] | ||||
| 		padding-top 48px | ||||
| 		background-image url('/assets/desktop/grid.svg') | ||||
|  | ||||
| 		> .main > .main | ||||
| 			> a | ||||
| 				display block | ||||
| 				margin-bottom 8px | ||||
| 				text-align center | ||||
|  | ||||
| 			> div | ||||
| 				cursor not-allowed !important | ||||
|  | ||||
| 				> * | ||||
| 					pointer-events none | ||||
|  | ||||
| 	&:not([data-customize]) | ||||
| 		> .main > *:empty | ||||
| 			display none | ||||
|  | ||||
| 	> .customize | ||||
| 		position fixed | ||||
| 		z-index 1000 | ||||
| 		top 0 | ||||
| 		left 0 | ||||
| 		width 100% | ||||
| 		height 48px | ||||
| 		color var(--text) | ||||
| 		background var(--desktopHeaderBg) | ||||
| 		box-shadow 0 1px 1px rgba(#000, 0.075) | ||||
|  | ||||
| 		> a | ||||
| 			display block | ||||
| 			position absolute | ||||
| 			z-index 1001 | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding 0 16px | ||||
| 			line-height 48px | ||||
| 			text-decoration none | ||||
| 			color var(--primaryForeground) | ||||
| 			background var(--primary) | ||||
| 			transition background 0.1s ease | ||||
|  | ||||
| 			&:hover | ||||
| 				background var(--primaryLighten10) | ||||
|  | ||||
| 			&:active | ||||
| 				background var(--primaryDarken10) | ||||
| 				transition background 0s ease | ||||
|  | ||||
| 			> [data-icon] | ||||
| 				margin-right 8px | ||||
|  | ||||
| 		> div | ||||
| 			display flex | ||||
| 			margin 0 auto | ||||
| 			max-width 1220px - 32px | ||||
|  | ||||
| 			> div | ||||
| 				width 50% | ||||
|  | ||||
| 				&.adder | ||||
| 					> p | ||||
| 						display inline | ||||
| 						line-height 48px | ||||
|  | ||||
| 				&.trash | ||||
| 					border-left solid 1px var(--faceDivider) | ||||
|  | ||||
| 					> div | ||||
| 						width 100% | ||||
| 						height 100% | ||||
|  | ||||
| 					> p | ||||
| 						position absolute | ||||
| 						top 0 | ||||
| 						left 0 | ||||
| 						width 100% | ||||
| 						line-height 48px | ||||
| 						margin 0 | ||||
| 						text-align center | ||||
| 						pointer-events none | ||||
|  | ||||
| 	> .main | ||||
| 		display flex | ||||
| 		justify-content center | ||||
| 		margin 0 auto | ||||
| 		max-width 1240px | ||||
|  | ||||
| 		> * | ||||
| 			.customize-container | ||||
| 				cursor move | ||||
| 				border-radius 6px | ||||
|  | ||||
| 				&:hover | ||||
| 					box-shadow 0 0 8px rgba(64, 120, 200, 0.3) | ||||
|  | ||||
| 				> * | ||||
| 					pointer-events none | ||||
|  | ||||
| 		> .main | ||||
| 			padding 16px | ||||
| 			width calc(100% - 280px * 2) | ||||
| 			order 2 | ||||
|  | ||||
| 			> .form | ||||
| 				margin-bottom 16px | ||||
| 				box-shadow var(--shadow) | ||||
| 				border-radius var(--round) | ||||
|  | ||||
| 		&.side | ||||
| 			> .main | ||||
| 				width calc(100% - 280px) | ||||
| 				max-width 680px | ||||
|  | ||||
| 		> *:not(.main) | ||||
| 			width 280px | ||||
| 			padding 16px 0 16px 0 | ||||
|  | ||||
| 			> *:not(:last-child) | ||||
| 				margin-bottom 16px | ||||
|  | ||||
| 		> .left | ||||
| 			padding-left 16px | ||||
| 			order 1 | ||||
|  | ||||
| 		> .right | ||||
| 			padding-right 16px | ||||
| 			order 3 | ||||
|  | ||||
| 		&.side | ||||
| 			@media (max-width 1000px) | ||||
| 				> *:not(.main) | ||||
| 					display none | ||||
|  | ||||
| 				> .main | ||||
| 					width 100% | ||||
| 					max-width 700px | ||||
| 					margin 0 auto | ||||
|  | ||||
| 		&:not(.side) | ||||
| 			@media (max-width 1200px) | ||||
| 				> *:not(.main) | ||||
| 					display none | ||||
|  | ||||
| 				> .main | ||||
| 					width 100% | ||||
| 					max-width 700px | ||||
| 					margin 0 auto | ||||
|  | ||||
| </style> | ||||
| @@ -2,8 +2,6 @@ import Vue from 'vue'; | ||||
|  | ||||
| import ui from './ui.vue'; | ||||
| import uiNotification from './ui-notification.vue'; | ||||
| import home from './home.vue'; | ||||
| import timeline from './timeline.vue'; | ||||
| import notes from './notes.vue'; | ||||
| import subNoteContent from './sub-note-content.vue'; | ||||
| import window from './window.vue'; | ||||
| @@ -24,8 +22,6 @@ import widgetContainer from './widget-container.vue'; | ||||
|  | ||||
| Vue.component('mk-ui', ui); | ||||
| Vue.component('mk-ui-notification', uiNotification); | ||||
| Vue.component('mk-home', home); | ||||
| Vue.component('mk-timeline', timeline); | ||||
| Vue.component('mk-notes', notes); | ||||
| Vue.component('mk-sub-note-content', subNoteContent); | ||||
| Vue.component('mk-window', window); | ||||
|   | ||||
| @@ -31,9 +31,6 @@ | ||||
| 				<ui-switch v-model="autoPopout">{{ $t('auto-popout') }} | ||||
| 					<span slot="desc">{{ $t('auto-popout-desc') }}</span> | ||||
| 				</ui-switch> | ||||
| 				<ui-switch v-model="deckNav">{{ $t('deck-nav') }} | ||||
| 					<span slot="desc">{{ $t('deck-nav-desc') }}</span> | ||||
| 				</ui-switch> | ||||
| 				<ui-switch v-model="keepCw">{{ $t('keep-cw') }} | ||||
| 					<span slot="desc">{{ $t('keep-cw-desc') }}</span> | ||||
| 				</ui-switch> | ||||
| @@ -89,9 +86,6 @@ | ||||
| 				<ui-radio v-model="navbar" value="left">{{ $t('navbar-position-left') }}</ui-radio> | ||||
| 				<ui-radio v-model="navbar" value="right">{{ $t('navbar-position-right') }}</ui-radio> | ||||
| 			</section> | ||||
| 			<section> | ||||
| 				<ui-switch v-model="deckDefault">{{ $t('deck-default') }}</ui-switch> | ||||
| 			</section> | ||||
| 			<section> | ||||
| 				<ui-switch v-model="darkmode">{{ $t('dark-mode') }}</ui-switch> | ||||
| 				<ui-switch v-model="useShadow">{{ $t('use-shadow') }}</ui-switch> | ||||
| @@ -337,11 +331,6 @@ export default Vue.extend({ | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		deckNav: { | ||||
| 			get() { return this.$store.state.settings.deckNav; }, | ||||
| 			set(value) { this.$store.commit('settings/set', { key: 'deckNav', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		keepCw: { | ||||
| 			get() { return this.$store.state.settings.keepCw; }, | ||||
| 			set(value) { this.$store.commit('settings/set', { key: 'keepCw', value }); } | ||||
| @@ -367,11 +356,6 @@ export default Vue.extend({ | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		deckDefault: { | ||||
| 			get() { return this.$store.state.device.deckDefault; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckDefault', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		enableSounds: { | ||||
| 			get() { return this.$store.state.device.enableSounds; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } | ||||
| @@ -534,8 +518,7 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		customizeHome() { | ||||
| 			this.$router.push('/i/customize-home'); | ||||
| 			this.$emit('done'); | ||||
| 			location.href = '/?customize'; | ||||
| 		}, | ||||
| 		updateWallpaper() { | ||||
| 			this.$chooseDriveFile({ | ||||
|   | ||||
| @@ -1,195 +0,0 @@ | ||||
| <template> | ||||
| <div class="mk-timeline-core"> | ||||
| 	<mk-friends-maker v-if="src == 'home' && alone"/> | ||||
|  | ||||
| 	<mk-notes ref="timeline" :more="existMore ? more : null"> | ||||
| 		<p :class="$style.empty" slot="empty"> | ||||
| 			<fa :icon="['far', 'comments']"/>{{ $t('empty') }} | ||||
| 		</p> | ||||
| 	</mk-notes> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
|  | ||||
| const fetchLimit = 10; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/timeline.core.vue'), | ||||
| 	props: { | ||||
| 		src: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		tagTl: { | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null, | ||||
| 			date: null, | ||||
| 			baseQuery: { | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| 			}, | ||||
| 			query: {}, | ||||
| 			endpoint: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		alone(): boolean { | ||||
| 			return this.$store.state.i.followingCount == 0; | ||||
| 		}, | ||||
|  | ||||
| 		canFetchMore(): boolean { | ||||
| 			return !this.moreFetching && !this.fetching && this.existMore; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		const prepend = note => { | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}; | ||||
|  | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.endpoint = 'notes/search_by_tag'; | ||||
| 			this.query = { | ||||
| 				query: this.tagTl.query | ||||
| 			}; | ||||
| 			this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query }); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			this.endpoint = 'notes/timeline'; | ||||
| 			const onChangeFollowing = () => { | ||||
| 				this.fetch(); | ||||
| 			}; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('homeTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.connection.on('follow', onChangeFollowing); | ||||
| 			this.connection.on('unfollow', onChangeFollowing); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.endpoint = 'notes/local-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('localTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.endpoint = 'notes/hybrid-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.endpoint = 'notes/global-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('globalTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('mention', prepend); | ||||
| 		} else if (this.src == 'messages') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.query = { | ||||
| 				visibility: 'specified' | ||||
| 			}; | ||||
| 			const onNote = note => { | ||||
| 				if (note.visibility == 'specified') { | ||||
| 					prepend(note); | ||||
| 				} | ||||
| 			}; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('mention', onNote); | ||||
| 		} | ||||
|  | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				this.$root.api(this.endpoint, Object.assign({ | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined | ||||
| 				}, this.baseQuery, this.query)).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes); | ||||
| 					this.fetching = false; | ||||
| 					this.$emit('loaded'); | ||||
| 				}, rej); | ||||
| 			})); | ||||
| 		}, | ||||
|  | ||||
| 		more() { | ||||
| 			if (!this.canFetchMore) return; | ||||
|  | ||||
| 			this.moreFetching = true; | ||||
|  | ||||
| 			const promise = this.$root.api(this.endpoint, Object.assign({ | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id | ||||
| 			}, this.baseQuery, this.query)); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				for (const n of notes) { | ||||
| 					(this.$refs.timeline as any).append(n); | ||||
| 				} | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
|  | ||||
| 			return promise; | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		}, | ||||
|  | ||||
| 		warp(date) { | ||||
| 			this.date = date; | ||||
| 			this.fetch(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-timeline-core | ||||
| 	> .mk-friends-maker | ||||
| 		border-bottom solid var(--lineWidth) #eee | ||||
|  | ||||
| </style> | ||||
|  | ||||
| <style lang="stylus" module> | ||||
| .empty | ||||
| 	display block | ||||
| 	margin 0 auto | ||||
| 	padding 32px | ||||
| 	max-width 400px | ||||
| 	text-align center | ||||
| 	color #999 | ||||
|  | ||||
| 	> [data-icon] | ||||
| 		display block | ||||
| 		margin-bottom 16px | ||||
| 		font-size 3em | ||||
| 		color #ccc | ||||
|  | ||||
| </style> | ||||
| @@ -1,264 +0,0 @@ | ||||
| <template> | ||||
| <div class="mk-timeline"> | ||||
| 	<header> | ||||
| 		<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> | ||||
| 		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> | ||||
| 		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> | ||||
| 		<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> | ||||
| 		<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> | ||||
| 		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> | ||||
| 		<div class="buttons"> | ||||
| 			<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> | ||||
| 			<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> | ||||
| 			<button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button> | ||||
| 			<button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button> | ||||
| 		</div> | ||||
| 	</header> | ||||
| 	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> | ||||
| 	<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> | ||||
| 	<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||
| 	<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||
| 	<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||
| 	<x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> | ||||
| 	<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> | ||||
| 	<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XCore from './timeline.core.vue'; | ||||
| import Menu from '../../../common/views/components/menu.vue'; | ||||
| import MkSettingsWindow from './settings-window.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/timeline.vue'), | ||||
| 	components: { | ||||
| 		XCore | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			src: 'home', | ||||
| 			list: null, | ||||
| 			tagTl: null, | ||||
| 			enableLocalTimeline: false, | ||||
| 			enableGlobalTimeline: false, | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		src() { | ||||
| 			this.saveSrc(); | ||||
| 		}, | ||||
|  | ||||
| 		list(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.tagTl = null; | ||||
| 		}, | ||||
|  | ||||
| 		tagTl(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.list = null; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.$root.getMeta().then((meta: Record<string, any>) => { | ||||
| 			if (!( | ||||
| 				this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin | ||||
| 			) && this.src === 'global') this.src = 'local'; | ||||
| 			if (!( | ||||
| 				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin | ||||
| 			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; | ||||
| 		}); | ||||
|  | ||||
| 		if (this.$store.state.device.tl) { | ||||
| 			this.src = this.$store.state.device.tl.src; | ||||
| 			if (this.src == 'list') { | ||||
| 				this.list = this.$store.state.device.tl.arg; | ||||
| 			} else if (this.src == 'tag') { | ||||
| 				this.tagTl = this.$store.state.device.tl.arg; | ||||
| 			} | ||||
| 		} else if (this.$store.state.i.followingCount == 0) { | ||||
| 			this.src = 'hybrid'; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		(this.$refs.tl as any).$once('loaded', () => { | ||||
| 			this.$emit('loaded'); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		saveSrc() { | ||||
| 			this.$store.commit('device/setTl', { | ||||
| 				src: this.src, | ||||
| 				arg: this.src == 'list' ? this.list : this.tagTl | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		}, | ||||
|  | ||||
| 		warp(date) { | ||||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		}, | ||||
|  | ||||
| 		async chooseList() { | ||||
| 			const lists = await this.$root.api('users/lists/list'); | ||||
|  | ||||
| 			let menu = [{ | ||||
| 				icon: 'plus', | ||||
| 				text: this.$t('add-list'), | ||||
| 				action: () => { | ||||
| 					this.$root.dialog({ | ||||
| 						title: this.$t('list-name'), | ||||
| 						input: true | ||||
| 					}).then(async ({ canceled, result: title }) => { | ||||
| 						if (canceled) return; | ||||
| 						const list = await this.$root.api('users/lists/create', { | ||||
| 							title | ||||
| 						}); | ||||
|  | ||||
| 						this.list = list; | ||||
| 						this.src = 'list'; | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
|  | ||||
| 			if (lists.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
|  | ||||
| 			menu = menu.concat(lists.map(list => ({ | ||||
| 				icon: 'list', | ||||
| 				text: list.title, | ||||
| 				action: () => { | ||||
| 					this.list = list; | ||||
| 					this.src = 'list'; | ||||
| 				} | ||||
| 			}))); | ||||
|  | ||||
| 			this.$root.new(Menu, { | ||||
| 				source: this.$refs.listButton, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		chooseTag() { | ||||
| 			let menu = [{ | ||||
| 				icon: 'plus', | ||||
| 				text: this.$t('add-tag-timeline'), | ||||
| 				action: () => { | ||||
| 					this.$root.new(MkSettingsWindow, { | ||||
| 						initialPage: 'hashtags' | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
|  | ||||
| 			if (this.$store.state.settings.tagTimelines.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
|  | ||||
| 			menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({ | ||||
| 				icon: 'hashtag', | ||||
| 				text: t.title, | ||||
| 				action: () => { | ||||
| 					this.tagTl = t; | ||||
| 					this.src = 'tag'; | ||||
| 				} | ||||
| 			}))); | ||||
|  | ||||
| 			this.$root.new(Menu, { | ||||
| 				source: this.$refs.tagButton, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-timeline | ||||
| 	background var(--face) | ||||
| 	box-shadow var(--shadow) | ||||
| 	border-radius var(--round) | ||||
| 	overflow hidden | ||||
|  | ||||
| 	> header | ||||
| 		padding 0 8px | ||||
| 		z-index 10 | ||||
| 		background var(--faceHeader) | ||||
| 		box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) | ||||
|  | ||||
| 		> .buttons | ||||
| 			position absolute | ||||
| 			z-index 2 | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding-right 8px | ||||
|  | ||||
| 			> button | ||||
| 				padding 0 8px | ||||
| 				font-size 0.9em | ||||
| 				line-height 42px | ||||
| 				color var(--faceTextButton) | ||||
|  | ||||
| 				> .badge | ||||
| 					position absolute | ||||
| 					top -4px | ||||
| 					right 4px | ||||
| 					font-size 10px | ||||
| 					color var(--notificationIndicator) | ||||
|  | ||||
| 				&:hover | ||||
| 					color var(--faceTextButtonHover) | ||||
|  | ||||
| 				&[data-active] | ||||
| 					color var(--primary) | ||||
| 					cursor default | ||||
|  | ||||
| 					&:before | ||||
| 						content "" | ||||
| 						display block | ||||
| 						position absolute | ||||
| 						bottom 0 | ||||
| 						left 0 | ||||
| 						width 100% | ||||
| 						height 2px | ||||
| 						background var(--primary) | ||||
|  | ||||
| 		> span | ||||
| 			display inline-block | ||||
| 			padding 0 10px | ||||
| 			line-height 42px | ||||
| 			font-size 12px | ||||
| 			user-select none | ||||
|  | ||||
| 			&[data-active] | ||||
| 				color var(--primary) | ||||
| 				cursor default | ||||
| 				font-weight bold | ||||
|  | ||||
| 				&:before | ||||
| 					content "" | ||||
| 					display block | ||||
| 					position absolute | ||||
| 					bottom 0 | ||||
| 					left -8px | ||||
| 					width calc(100% + 16px) | ||||
| 					height 2px | ||||
| 					background var(--primary) | ||||
|  | ||||
| 			&:not([data-active]) | ||||
| 				color var(--desktopTimelineSrc) | ||||
| 				cursor pointer | ||||
|  | ||||
| 				&:hover | ||||
| 					color var(--desktopTimelineSrcHover) | ||||
|  | ||||
| </style> | ||||
| @@ -44,13 +44,6 @@ | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 			<ul> | ||||
| 				<li> | ||||
| 					<router-link to="/i/customize-home"> | ||||
| 						<i><fa icon="wrench"/></i> | ||||
| 						<span>{{ $t('customize') }}</span> | ||||
| 						<i><fa icon="angle-right"/></i> | ||||
| 					</router-link> | ||||
| 				</li> | ||||
| 				<li> | ||||
| 					<router-link to="/i/settings"> | ||||
| 						<i><fa icon="cog"/></i> | ||||
|   | ||||
| @@ -2,20 +2,20 @@ | ||||
| <div class="nav"> | ||||
| 	<ul> | ||||
| 		<template v-if="$store.getters.isSignedIn"> | ||||
| 			<template v-if="$store.state.device.deckDefault"> | ||||
| 				<li class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop"> | ||||
| 			<template v-if="$store.state.device.deckMode"> | ||||
| 				<li class="deck active" @click="goToTop"> | ||||
| 					<router-link to="/"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link> | ||||
| 				</li> | ||||
| 				<li class="home" :class="{ active: $route.name == 'home' }" @click="goToTop"> | ||||
| 					<router-link to="/home"><fa icon="home"/><p>{{ $t('home') }}</p></router-link> | ||||
| 				<li class="home"> | ||||
| 					<a @click="toggleDeckMode(false)"><fa icon="home"/><p>{{ $t('home') }}</p></a> | ||||
| 				</li> | ||||
| 			</template> | ||||
| 			<template v-else> | ||||
| 				<li class="home" :class="{ active: $route.name == 'home' || $route.name == 'index' }" @click="goToTop"> | ||||
| 				<li class="home active" @click="goToTop"> | ||||
| 					<router-link to="/"><fa icon="home"/><p>{{ $t('home') }}</p></router-link> | ||||
| 				</li> | ||||
| 				<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> | ||||
| 					<router-link to="/deck"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link> | ||||
| 				<li class="deck"> | ||||
| 					<a @click="toggleDeckMode(true)"><fa icon="columns"/><p>{{ $t('deck') }}</p></a> | ||||
| 				</li> | ||||
| 			</template> | ||||
| 			<li class="messaging"> | ||||
| @@ -70,6 +70,11 @@ export default Vue.extend({ | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggleDeckMode(deck) { | ||||
| 			this.$store.commit('device/set', { key: 'deckMode', value: deck }); | ||||
| 			location.reload(); | ||||
| 		}, | ||||
|  | ||||
| 		onReversiInvited() { | ||||
| 			this.hasGameInvitations = true; | ||||
| 		}, | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="nav" v-if="$store.getters.isSignedIn"> | ||||
| 			<template v-if="$store.state.device.deckDefault"> | ||||
| 			<template v-if="$store.state.device.deckMode"> | ||||
| 				<div class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop"> | ||||
| 					<router-link to="/"><fa icon="columns"/></router-link> | ||||
| 				</div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo