commit
						d21da0211c
					
				|  | @ -606,6 +606,7 @@ desktop/views/components/ui.header.account.vue: | |||
| 
 | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ import updateAvatar from './api/update-avatar'; | |||
| import updateBanner from './api/update-banner'; | ||||
| 
 | ||||
| import MkIndex from './views/pages/index.vue'; | ||||
| import MkDeck from './views/pages/deck/deck.vue'; | ||||
| import MkUser from './views/pages/user/user.vue'; | ||||
| import MkFavorites from './views/pages/favorites.vue'; | ||||
| import MkSelectDrive from './views/pages/selectdrive.vue'; | ||||
|  | @ -50,6 +51,7 @@ init(async (launch) => { | |||
| 		mode: 'history', | ||||
| 		routes: [ | ||||
| 			{ path: '/', name: 'index', component: MkIndex }, | ||||
| 			{ path: '/deck', name: 'deck', component: MkDeck }, | ||||
| 			{ path: '/i/customize-home', component: MkHomeCustomize }, | ||||
| 			{ path: '/i/favorites', component: MkFavorites }, | ||||
| 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | ||||
|  |  | |||
|  | @ -8,6 +8,12 @@ | |||
| 					<p>%i18n:@home%</p> | ||||
| 				</router-link> | ||||
| 			</li> | ||||
| 			<li class="deck" :class="{ active: $route.name == 'deck' }"> | ||||
| 				<router-link to="/deck"> | ||||
| 					%fa:columns% | ||||
| 					<p>%i18n:@deck% <small>(beta)</small></p> | ||||
| 				</router-link> | ||||
| 			</li> | ||||
| 			<li class="messaging"> | ||||
| 				<a @click="messaging"> | ||||
| 					%fa:comments% | ||||
|  |  | |||
|  | @ -37,7 +37,16 @@ export default Vue.extend({ | |||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| .mk-ui | ||||
| 	display flex | ||||
| 	flex-direction column | ||||
| 	flex 1 | ||||
| 
 | ||||
| 	> .header | ||||
| 		@media (max-width 1000px) | ||||
| 			display none | ||||
| 
 | ||||
| 	> .content | ||||
| 		display flex | ||||
| 		flex-direction column | ||||
| 		flex 1 | ||||
| </style> | ||||
|  |  | |||
|  | @ -0,0 +1,75 @@ | |||
| <template> | ||||
| <div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs"> | ||||
| 	<header> | ||||
| 		<slot name="header"></slot> | ||||
| 	</header> | ||||
| 	<div ref="body"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XTl from './deck.tl.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XTl | ||||
| 	}, | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			getColumn() { | ||||
| 				return this; | ||||
| 			}, | ||||
| 			getScrollContainer() { | ||||
| 				return this.$refs.body; | ||||
| 			} | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			this.$emit('mounted'); | ||||
| 
 | ||||
| 			setInterval(() => { | ||||
| 				this.$emit('mounted'); | ||||
| 			}, 100); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
| 
 | ||||
| root(isDark) | ||||
| 	flex 1 | ||||
| 	min-width 330px | ||||
| 	max-width 330px | ||||
| 	height 100% | ||||
| 	margin-right 16px | ||||
| 	background isDark ? #282C37 : #fff | ||||
| 	border-radius 6px | ||||
| 	box-shadow 0 2px 16px rgba(#000, 0.1) | ||||
| 	overflow hidden | ||||
| 
 | ||||
| 	> header | ||||
| 		z-index 1 | ||||
| 		line-height 42px | ||||
| 		padding 0 16px | ||||
| 		color isDark ? #e3e5e8 : #888 | ||||
| 		background isDark ? #313543 : #fff | ||||
| 		box-shadow 0 1px rgba(#000, 0.15) | ||||
| 
 | ||||
| 	> div | ||||
| 		height calc(100% - 42px) | ||||
| 		overflow auto | ||||
| 		overflow-x hidden | ||||
| 
 | ||||
| .dnpfarvgbnfmyzbdquhhzyxcmstpdqzs[data-darkmode] | ||||
| 	root(true) | ||||
| 
 | ||||
| .dnpfarvgbnfmyzbdquhhzyxcmstpdqzs:not([data-darkmode]) | ||||
| 	root(false) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -0,0 +1,153 @@ | |||
| <template> | ||||
| <div class="fnlfosztlhtptnongximhlbykxblytcq"> | ||||
| 	<mk-avatar class="avatar" :user="note.user"/> | ||||
| 	<div class="main"> | ||||
| 		<header> | ||||
| 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> | ||||
| 			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span> | ||||
| 			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span> | ||||
| 			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span> | ||||
| 			<span class="username"><mk-acct :user="note.user"/></span> | ||||
| 			<div class="info"> | ||||
| 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> | ||||
| 				<router-link class="created-at" :to="note | notePage"> | ||||
| 					<mk-time :time="note.createdAt"/> | ||||
| 				</router-link> | ||||
| 				<span class="visibility" v-if="note.visibility != 'public'"> | ||||
| 					<template v-if="note.visibility == 'home'">%fa:home%</template> | ||||
| 					<template v-if="note.visibility == 'followers'">%fa:unlock%</template> | ||||
| 					<template v-if="note.visibility == 'specified'">%fa:envelope%</template> | ||||
| 					<template v-if="note.visibility == 'private'">%fa:lock%</template> | ||||
| 				</span> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		<div class="body"> | ||||
| 			<mk-sub-note-content class="text" :note="note"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		// TODO | ||||
| 		truncate: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	display flex | ||||
| 	padding 16px | ||||
| 	font-size 10px | ||||
| 	background isDark ? #21242d : #fcfcfc | ||||
| 
 | ||||
| 	&.smart | ||||
| 		> .main | ||||
| 			width 100% | ||||
| 
 | ||||
| 			> header | ||||
| 				align-items center | ||||
| 
 | ||||
| 	> .avatar | ||||
| 		flex-shrink 0 | ||||
| 		display block | ||||
| 		margin 0 8px 0 0 | ||||
| 		width 38px | ||||
| 		height 38px | ||||
| 		border-radius 8px | ||||
| 
 | ||||
| 	> .main | ||||
| 		flex 1 | ||||
| 		min-width 0 | ||||
| 
 | ||||
| 		> header | ||||
| 			display flex | ||||
| 			align-items baseline | ||||
| 			margin-bottom 2px | ||||
| 			white-space nowrap | ||||
| 
 | ||||
| 			> .avatar | ||||
| 				flex-shrink 0 | ||||
| 				margin-right 8px | ||||
| 				width 18px | ||||
| 				height 18px | ||||
| 				border-radius 100% | ||||
| 
 | ||||
| 			> .name | ||||
| 				display block | ||||
| 				margin 0 0.5em 0 0 | ||||
| 				padding 0 | ||||
| 				overflow hidden | ||||
| 				color isDark ? #fff : #607073 | ||||
| 				font-size 1em | ||||
| 				font-weight 700 | ||||
| 				text-align left | ||||
| 				text-decoration none | ||||
| 				text-overflow ellipsis | ||||
| 
 | ||||
| 				&:hover | ||||
| 					text-decoration underline | ||||
| 
 | ||||
| 			> .is-admin | ||||
| 			> .is-bot | ||||
| 			> .is-cat | ||||
| 				align-self center | ||||
| 				margin 0 0.5em 0 0 | ||||
| 				padding 1px 5px | ||||
| 				font-size 0.8em | ||||
| 				color isDark ? #758188 : #aaa | ||||
| 				border solid 1px isDark ? #57616f : #ddd | ||||
| 				border-radius 3px | ||||
| 
 | ||||
| 				&.is-admin | ||||
| 					border-color isDark ? #d42c41 : #f56a7b | ||||
| 					color isDark ? #d42c41 : #f56a7b | ||||
| 
 | ||||
| 			> .username | ||||
| 				text-align left | ||||
| 				margin 0 | ||||
| 				color isDark ? #606984 : #d1d8da | ||||
| 
 | ||||
| 			> .info | ||||
| 				margin-left auto | ||||
| 				font-size 0.9em | ||||
| 
 | ||||
| 				> * | ||||
| 					color isDark ? #606984 : #b2b8bb | ||||
| 
 | ||||
| 				> .mobile | ||||
| 					margin-right 6px | ||||
| 
 | ||||
| 				> .visibility | ||||
| 					margin-left 6px | ||||
| 
 | ||||
| 		> .body | ||||
| 
 | ||||
| 			> .text | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				color isDark ? #959ba7 : #717171 | ||||
| 
 | ||||
| 				pre | ||||
| 					max-height 120px | ||||
| 					font-size 80% | ||||
| 
 | ||||
| .fnlfosztlhtptnongximhlbykxblytcq[data-darkmode] | ||||
| 	root(true) | ||||
| 
 | ||||
| .fnlfosztlhtptnongximhlbykxblytcq:not([data-darkmode]) | ||||
| 	root(false) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -0,0 +1,539 @@ | |||
| <template> | ||||
| <div class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }"> | ||||
| 	<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> | ||||
| 		<x-sub :note="p.reply"/> | ||||
| 	</div> | ||||
| 	<div class="renote" v-if="isRenote"> | ||||
| 		<mk-avatar class="avatar" :user="note.user"/> | ||||
| 		%fa:retweet% | ||||
| 		<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span> | ||||
| 		<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> | ||||
| 		<span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span> | ||||
| 		<mk-time :time="note.createdAt"/> | ||||
| 	</div> | ||||
| 	<article> | ||||
| 		<mk-avatar class="avatar" :user="p.user"/> | ||||
| 		<div class="main"> | ||||
| 			<header> | ||||
| 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> | ||||
| 				<span class="is-admin" v-if="p.user.isAdmin">admin</span> | ||||
| 				<span class="is-bot" v-if="p.user.isBot">bot</span> | ||||
| 				<span class="is-cat" v-if="p.user.isCat">cat</span> | ||||
| 				<span class="username"><mk-acct :user="p.user"/></span> | ||||
| 				<div class="info"> | ||||
| 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> | ||||
| 					<router-link class="created-at" :to="p | notePage"> | ||||
| 						<mk-time :time="p.createdAt"/> | ||||
| 					</router-link> | ||||
| 					<span class="visibility" v-if="p.visibility != 'public'"> | ||||
| 						<template v-if="p.visibility == 'home'">%fa:home%</template> | ||||
| 						<template v-if="p.visibility == 'followers'">%fa:unlock%</template> | ||||
| 						<template v-if="p.visibility == 'specified'">%fa:envelope%</template> | ||||
| 						<template v-if="p.visibility == 'private'">%fa:lock%</template> | ||||
| 					</span> | ||||
| 				</div> | ||||
| 			</header> | ||||
| 			<div class="body"> | ||||
| 				<p v-if="p.cw != null" class="cw"> | ||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||
| 					<span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span> | ||||
| 				</p> | ||||
| 				<div class="content" v-show="p.cw == null || showContent"> | ||||
| 					<div class="text"> | ||||
| 						<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> | ||||
| 						<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> | ||||
| 						<a class="reply" v-if="p.reply">%fa:reply%</a> | ||||
| 						<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i"/> | ||||
| 						<a class="rp" v-if="p.renote != null">RP:</a> | ||||
| 					</div> | ||||
| 					<div class="media" v-if="p.media.length > 0"> | ||||
| 						<mk-media-list :media-list="p.media"/> | ||||
| 					</div> | ||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||
| 					<div class="tags" v-if="p.tags && p.tags.length > 0"> | ||||
| 						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> | ||||
| 					</div> | ||||
| 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 					<div class="renote" v-if="p.renote"> | ||||
| 						<mk-note-preview :note="p.renote"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> | ||||
| 			</div> | ||||
| 			<footer> | ||||
| 				<mk-reactions-viewer :note="p" ref="reactionsViewer"/> | ||||
| 				<button @click="reply"> | ||||
| 					<template v-if="p.reply">%fa:reply-all%</template> | ||||
| 					<template v-else>%fa:reply%</template> | ||||
| 				</button> | ||||
| 				<button @click="renote" title="Renote">%fa:retweet%</button> | ||||
| 				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">%fa:plus%</button> | ||||
| 				<button class="menu" @click="menu" ref="menuButton">%fa:ellipsis-h%</button> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 	</article> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import parse from '../../../../../../text/parse'; | ||||
| import canHideText from '../../../../common/scripts/can-hide-text'; | ||||
| 
 | ||||
| import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; | ||||
| import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './deck.note.sub.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSub | ||||
| 	}, | ||||
| 
 | ||||
| 	props: ['note'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		isRenote(): boolean { | ||||
| 			return (this.note.renote && | ||||
| 				this.note.text == null && | ||||
| 				this.note.mediaIds.length == 0 && | ||||
| 				this.note.poll == null); | ||||
| 		}, | ||||
| 
 | ||||
| 		p(): any { | ||||
| 			return this.isRenote ? this.note.renote : this.note; | ||||
| 		}, | ||||
| 
 | ||||
| 		reactionsCount(): number { | ||||
| 			return this.p.reactionCounts | ||||
| 				? Object.keys(this.p.reactionCounts) | ||||
| 					.map(key => this.p.reactionCounts[key]) | ||||
| 					.reduce((a, b) => a + b) | ||||
| 				: 0; | ||||
| 		}, | ||||
| 
 | ||||
| 		urls(): string[] { | ||||
| 			if (this.p.text) { | ||||
| 				const ast = parse(this.p.text); | ||||
| 				return ast | ||||
| 					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) | ||||
| 					.map(t => t.url); | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.capture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.on('_connected_', this.onStreamConnected); | ||||
| 		} | ||||
| 
 | ||||
| 		// Draw map | ||||
| 		if (this.p.geo) { | ||||
| 			const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true; | ||||
| 			if (shouldShowMap) { | ||||
| 				(this as any).os.getGoogleMaps().then(maps => { | ||||
| 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); | ||||
| 					const map = new maps.Map(this.$refs.map, { | ||||
| 						center: uluru, | ||||
| 						zoom: 15 | ||||
| 					}); | ||||
| 					new maps.Marker({ | ||||
| 						position: uluru, | ||||
| 						map: map | ||||
| 					}); | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.decapture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('_connected_', this.onStreamConnected); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		canHideText, | ||||
| 
 | ||||
| 		capture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'capture', | ||||
| 					id: this.p.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		decapture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'decapture', | ||||
| 					id: this.p.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamConnected() { | ||||
| 			this.capture(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamNoteUpdated(data) { | ||||
| 			const note = data.note; | ||||
| 			if (note.id == this.note.id) { | ||||
| 				this.$emit('update:note', note); | ||||
| 			} else if (note.id == this.note.renoteId) { | ||||
| 				this.note.renote = note; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		reply() { | ||||
| 			(this as any).apis.post({ | ||||
| 				reply: this.p | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		renote() { | ||||
| 			(this as any).apis.post({ | ||||
| 				renote: this.p | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		react() { | ||||
| 			(this as any).os.new(MkReactionPicker, { | ||||
| 				source: this.$refs.reactButton, | ||||
| 				note: this.p, | ||||
| 				compact: true | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		menu() { | ||||
| 			(this as any).os.new(MkNoteMenu, { | ||||
| 				source: this.$refs.menuButton, | ||||
| 				note: this.p, | ||||
| 				compact: true | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
| 
 | ||||
| root(isDark) | ||||
| 	font-size 12px | ||||
| 	border-bottom solid 1px isDark ? #1c2023 : #eaeaea | ||||
| 
 | ||||
| 	&:last-of-type | ||||
| 		border-bottom none | ||||
| 
 | ||||
| 	&.smart | ||||
| 		> article | ||||
| 			> .main | ||||
| 				> header | ||||
| 					align-items center | ||||
| 					margin-bottom 4px | ||||
| 
 | ||||
| 	> .renote | ||||
| 		display flex | ||||
| 		align-items center | ||||
| 		padding 8px 16px | ||||
| 		line-height 28px | ||||
| 		white-space pre | ||||
| 		color #9dbb00 | ||||
| 		background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%) | ||||
| 
 | ||||
| 		.avatar | ||||
| 			flex-shrink 0 | ||||
| 			display inline-block | ||||
| 			width 20px | ||||
| 			height 20px | ||||
| 			margin 0 8px 0 0 | ||||
| 			border-radius 6px | ||||
| 
 | ||||
| 		[data-fa] | ||||
| 			margin-right 4px | ||||
| 
 | ||||
| 		> span | ||||
| 			flex-shrink 0 | ||||
| 
 | ||||
| 			&:last-of-type | ||||
| 				margin-right 8px | ||||
| 
 | ||||
| 		.name | ||||
| 			overflow hidden | ||||
| 			flex-shrink 1 | ||||
| 			text-overflow ellipsis | ||||
| 			white-space nowrap | ||||
| 			font-weight bold | ||||
| 
 | ||||
| 		> .mk-time | ||||
| 			display block | ||||
| 			margin-left auto | ||||
| 			flex-shrink 0 | ||||
| 			font-size 0.9em | ||||
| 
 | ||||
| 		& + article | ||||
| 			padding-top 8px | ||||
| 
 | ||||
| 	> article | ||||
| 		display flex | ||||
| 		padding 16px 16px 9px | ||||
| 
 | ||||
| 		> .avatar | ||||
| 			flex-shrink 0 | ||||
| 			display block | ||||
| 			margin 0 10px 8px 0 | ||||
| 			width 42px | ||||
| 			height 42px | ||||
| 			border-radius 6px | ||||
| 			//position -webkit-sticky | ||||
| 			//position sticky | ||||
| 			//top 62px | ||||
| 
 | ||||
| 		> .main | ||||
| 			flex 1 | ||||
| 			min-width 0 | ||||
| 
 | ||||
| 			> header | ||||
| 				display flex | ||||
| 				align-items baseline | ||||
| 				white-space nowrap | ||||
| 
 | ||||
| 				> .avatar | ||||
| 					flex-shrink 0 | ||||
| 					margin-right 8px | ||||
| 					width 20px | ||||
| 					height 20px | ||||
| 					border-radius 100% | ||||
| 
 | ||||
| 				> .name | ||||
| 					display block | ||||
| 					margin 0 0.5em 0 0 | ||||
| 					padding 0 | ||||
| 					overflow hidden | ||||
| 					color isDark ? #fff : #627079 | ||||
| 					font-weight bold | ||||
| 					text-decoration none | ||||
| 					text-overflow ellipsis | ||||
| 
 | ||||
| 				> .is-admin | ||||
| 				> .is-bot | ||||
| 				> .is-cat | ||||
| 					align-self center | ||||
| 					margin 0 0.5em 0 0 | ||||
| 					padding 1px 6px | ||||
| 					font-size 0.8em | ||||
| 					color isDark ? #758188 : #aaa | ||||
| 					border solid 1px isDark ? #57616f : #ddd | ||||
| 					border-radius 3px | ||||
| 
 | ||||
| 					&.is-admin | ||||
| 						border-color isDark ? #d42c41 : #f56a7b | ||||
| 						color isDark ? #d42c41 : #f56a7b | ||||
| 
 | ||||
| 				> .username | ||||
| 					margin 0 0.5em 0 0 | ||||
| 					overflow hidden | ||||
| 					text-overflow ellipsis | ||||
| 					color isDark ? #606984 : #ccc | ||||
| 
 | ||||
| 				> .info | ||||
| 					margin-left auto | ||||
| 					font-size 0.9em | ||||
| 
 | ||||
| 					> * | ||||
| 						color isDark ? #606984 : #c0c0c0 | ||||
| 
 | ||||
| 					> .mobile | ||||
| 						margin-right 6px | ||||
| 
 | ||||
| 					> .visibility | ||||
| 						margin-left 6px | ||||
| 
 | ||||
| 			> .body | ||||
| 
 | ||||
| 				> .cw | ||||
| 					cursor default | ||||
| 					display block | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					overflow-wrap break-word | ||||
| 					color isDark ? #fff : #717171 | ||||
| 
 | ||||
| 					> .text | ||||
| 						margin-right 8px | ||||
| 
 | ||||
| 					> .toggle | ||||
| 						display inline-block | ||||
| 						padding 4px 8px | ||||
| 						font-size 0.7em | ||||
| 						color isDark ? #393f4f : #fff | ||||
| 						background isDark ? #687390 : #b1b9c1 | ||||
| 						border-radius 2px | ||||
| 						cursor pointer | ||||
| 						user-select none | ||||
| 
 | ||||
| 						&:hover | ||||
| 							background isDark ? #707b97 : #bbc4ce | ||||
| 
 | ||||
| 				> .content | ||||
| 
 | ||||
| 					> .text | ||||
| 						display block | ||||
| 						margin 0 | ||||
| 						padding 0 | ||||
| 						overflow-wrap break-word | ||||
| 						color isDark ? #fff : #717171 | ||||
| 
 | ||||
| 						>>> .title | ||||
| 							display block | ||||
| 							margin-bottom 4px | ||||
| 							padding 4px | ||||
| 							font-size 90% | ||||
| 							text-align center | ||||
| 							background isDark ? #2f3944 : #eef1f3 | ||||
| 							border-radius 4px | ||||
| 
 | ||||
| 						>>> .code | ||||
| 							margin 8px 0 | ||||
| 
 | ||||
| 						>>> .quote | ||||
| 							margin 8px | ||||
| 							padding 6px 12px | ||||
| 							color isDark ? #6f808e : #aaa | ||||
| 							border-left solid 3px isDark ? #637182 : #eee | ||||
| 
 | ||||
| 						> .reply | ||||
| 							margin-right 8px | ||||
| 							color isDark ? #99abbf : #717171 | ||||
| 
 | ||||
| 						> .rp | ||||
| 							margin-left 4px | ||||
| 							font-style oblique | ||||
| 							color #a0bf46 | ||||
| 
 | ||||
| 						[data-is-me]:after | ||||
| 							content "you" | ||||
| 							padding 0 4px | ||||
| 							margin-left 4px | ||||
| 							font-size 80% | ||||
| 							color $theme-color-foreground | ||||
| 							background $theme-color | ||||
| 							border-radius 4px | ||||
| 
 | ||||
| 					.mk-url-preview | ||||
| 						margin-top 8px | ||||
| 
 | ||||
| 					> .tags | ||||
| 						margin 4px 0 0 0 | ||||
| 
 | ||||
| 						> * | ||||
| 							display inline-block | ||||
| 							margin 0 8px 0 0 | ||||
| 							padding 2px 8px 2px 16px | ||||
| 							font-size 90% | ||||
| 							color #8d969e | ||||
| 							background isDark ? #313543 : #edf0f3 | ||||
| 							border-radius 4px | ||||
| 
 | ||||
| 							&:before | ||||
| 								content "" | ||||
| 								display block | ||||
| 								position absolute | ||||
| 								top 0 | ||||
| 								bottom 0 | ||||
| 								left 4px | ||||
| 								width 8px | ||||
| 								height 8px | ||||
| 								margin auto 0 | ||||
| 								background isDark ? #282c37 : #fff | ||||
| 								border-radius 100% | ||||
| 
 | ||||
| 					> .media | ||||
| 						> img | ||||
| 							display block | ||||
| 							max-width 100% | ||||
| 
 | ||||
| 					> .location | ||||
| 						margin 4px 0 | ||||
| 						font-size 12px | ||||
| 						color #ccc | ||||
| 
 | ||||
| 					> .map | ||||
| 						width 100% | ||||
| 						height 200px | ||||
| 
 | ||||
| 						&:empty | ||||
| 							display none | ||||
| 
 | ||||
| 					> .mk-poll | ||||
| 						font-size 80% | ||||
| 
 | ||||
| 					> .renote | ||||
| 						margin 8px 0 | ||||
| 
 | ||||
| 						> .mk-note-preview | ||||
| 							padding 16px | ||||
| 							border dashed 1px isDark ? #4e945e : #c0dac6 | ||||
| 							border-radius 8px | ||||
| 
 | ||||
| 				> .app | ||||
| 					font-size 12px | ||||
| 					color #ccc | ||||
| 
 | ||||
| 			> footer | ||||
| 				> button | ||||
| 					margin 0 | ||||
| 					padding 8px | ||||
| 					background transparent | ||||
| 					border none | ||||
| 					box-shadow none | ||||
| 					font-size 1em | ||||
| 					color isDark ? #606984 : #ddd | ||||
| 					cursor pointer | ||||
| 
 | ||||
| 					&:not(:last-child) | ||||
| 						margin-right 28px | ||||
| 
 | ||||
| 					&:hover | ||||
| 						color isDark ? #9198af : #666 | ||||
| 
 | ||||
| 					> .count | ||||
| 						display inline | ||||
| 						margin 0 0 0 8px | ||||
| 						color #999 | ||||
| 
 | ||||
| 					&.reacted | ||||
| 						color $theme-color | ||||
| 
 | ||||
| .zyjjkidcqjnlegkqebitfviomuqmseqk[data-darkmode] | ||||
| 	root(true) | ||||
| 
 | ||||
| .zyjjkidcqjnlegkqebitfviomuqmseqk:not([data-darkmode]) | ||||
| 	root(false) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -0,0 +1,252 @@ | |||
| <template> | ||||
| <div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> | ||||
| 	<div class="newer-indicator" v-show="queue.length > 0"></div> | ||||
| 
 | ||||
| 	<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> | ||||
| 
 | ||||
| 	<div v-if="!fetching && requestInitPromise != null"> | ||||
| 		<p>%i18n:@error%</p> | ||||
| 		<button @click="resolveInitPromise">%i18n:@retry%</button> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<transition-group name="mk-notes" class="transition"> | ||||
| 		<template v-for="(note, i) in _notes"> | ||||
| 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | ||||
| 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | ||||
| 				<span>%fa:angle-up%{{ note._datetext }}</span> | ||||
| 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> | ||||
| 			</p> | ||||
| 		</template> | ||||
| 	</transition-group> | ||||
| 
 | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 			<template v-if="!moreFetching">%i18n:@load-more%</template> | ||||
| 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template> | ||||
| 		</button> | ||||
| 	</footer> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { url } from '../../../config'; | ||||
| import getNoteSummary from '../../../../../renderers/get-note-summary'; | ||||
| 
 | ||||
| import XNote from './deck.note.vue'; | ||||
| 
 | ||||
| const displayLimit = 30; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNote | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		more: { | ||||
| 			type: Function, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			rootEl: null, | ||||
| 			requestInitPromise: null as () => Promise<any[]>, | ||||
| 			notes: [], | ||||
| 			queue: [], | ||||
| 			unreadCount: 0, | ||||
| 			fetching: true, | ||||
| 			moreFetching: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		_notes(): any[] { | ||||
| 			return (this.notes as any).map(note => { | ||||
| 				const date = new Date(note.createdAt).getDate(); | ||||
| 				const month = new Date(note.createdAt).getMonth() + 1; | ||||
| 				note._date = date; | ||||
| 				note._datetext = `${month}月 ${date}日`; | ||||
| 				return note; | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	inject: ['getColumn', 'getScrollContainer'], | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.getColumn().$once('mounted', () => { | ||||
| 			this.rootEl = this.getScrollContainer(); | ||||
| 			this.rootEl.addEventListener('scroll', this.onScroll); | ||||
| 		}) | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.rootEl.removeEventListener('scroll', this.onScroll); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		isScrollTop() { | ||||
| 			if (this.rootEl == null) return true; | ||||
| 			return this.rootEl.scrollTop <= 8; | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$el as any).children[0].focus(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onNoteUpdated(i, note) { | ||||
| 			Vue.set((this as any).notes, i, note); | ||||
| 		}, | ||||
| 
 | ||||
| 		init(promiseGenerator: () => Promise<any[]>) { | ||||
| 			this.requestInitPromise = promiseGenerator; | ||||
| 			this.resolveInitPromise(); | ||||
| 		}, | ||||
| 
 | ||||
| 		resolveInitPromise() { | ||||
| 			this.queue = []; | ||||
| 			this.notes = []; | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			const promise = this.requestInitPromise(); | ||||
| 
 | ||||
| 			promise.then(notes => { | ||||
| 				this.notes = notes; | ||||
| 				this.requestInitPromise = null; | ||||
| 				this.fetching = false; | ||||
| 			}, e => { | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		prepend(note, silent = false) { | ||||
| 			//#region 弾く | ||||
| 			const isMyNote = note.userId == this.$store.state.i.id; | ||||
| 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; | ||||
| 
 | ||||
| 			if (this.$store.state.settings.showMyRenotes === false) { | ||||
| 				if (isMyNote && isPureRenote) { | ||||
| 					return; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if (this.$store.state.settings.showRenotedMyNotes === false) { | ||||
| 				if (isPureRenote && (note.renote.userId == this.$store.state.i.id)) { | ||||
| 					return; | ||||
| 				} | ||||
| 			} | ||||
| 			//#endregion | ||||
| 
 | ||||
| 			if (this.isScrollTop()) { | ||||
| 				// Prepend the note | ||||
| 				this.notes.unshift(note); | ||||
| 
 | ||||
| 				// オーバーフローしたら古い投稿は捨てる | ||||
| 				if (this.notes.length >= displayLimit) { | ||||
| 					this.notes = this.notes.slice(0, displayLimit); | ||||
| 				} | ||||
| 			} else { | ||||
| 				this.queue.push(note); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		append(note) { | ||||
| 			this.notes.push(note); | ||||
| 		}, | ||||
| 
 | ||||
| 		tail() { | ||||
| 			return this.notes[this.notes.length - 1]; | ||||
| 		}, | ||||
| 
 | ||||
| 		releaseQueue() { | ||||
| 			this.queue.forEach(n => this.prepend(n, true)); | ||||
| 			this.queue = []; | ||||
| 		}, | ||||
| 
 | ||||
| 		async loadMore() { | ||||
| 			if (this.more == null) return; | ||||
| 			if (this.moreFetching) return; | ||||
| 
 | ||||
| 			this.moreFetching = true; | ||||
| 			await this.more(); | ||||
| 			this.moreFetching = false; | ||||
| 		}, | ||||
| 
 | ||||
| 		onScroll() { | ||||
| 			if (this.isScrollTop()) { | ||||
| 				this.releaseQueue(); | ||||
| 			} | ||||
| 
 | ||||
| 			if (this.rootEl && this.$store.state.settings.fetchOnScroll !== false) { | ||||
| 				const current = this.rootEl.scrollTop + this.rootEl.clientHeight; | ||||
| 				if (current > this.rootEl.scrollHeight - 8) this.loadMore(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
| 
 | ||||
| root(isDark) | ||||
| 	.transition | ||||
| 		.mk-notes-enter | ||||
| 		.mk-notes-leave-to | ||||
| 			opacity 0 | ||||
| 			transform translateY(-30px) | ||||
| 
 | ||||
| 		> * | ||||
| 			transition transform .3s ease, opacity .3s ease | ||||
| 
 | ||||
| 		> .date | ||||
| 			display block | ||||
| 			margin 0 | ||||
| 			line-height 32px | ||||
| 			font-size 14px | ||||
| 			text-align center | ||||
| 			color isDark ? #666b79 : #aaa | ||||
| 			background isDark ? #242731 : #fdfdfd | ||||
| 			border-bottom solid 1px isDark ? #1c2023 : #eaeaea | ||||
| 
 | ||||
| 			span | ||||
| 				margin 0 16px | ||||
| 
 | ||||
| 			[data-fa] | ||||
| 				margin-right 8px | ||||
| 
 | ||||
| 	> .newer-indicator | ||||
| 		position -webkit-sticky | ||||
| 		position sticky | ||||
| 		z-index 100 | ||||
| 		height 3px | ||||
| 		background $theme-color | ||||
| 
 | ||||
| 	> footer | ||||
| 		> button | ||||
| 			display block | ||||
| 			margin 0 | ||||
| 			padding 16px | ||||
| 			width 100% | ||||
| 			text-align center | ||||
| 			color #ccc | ||||
| 			background isDark ? #282C37 : #fff | ||||
| 			border-top solid 1px isDark ? #1c2023 : #eaeaea | ||||
| 			border-bottom-left-radius 6px | ||||
| 			border-bottom-right-radius 6px | ||||
| 
 | ||||
| 			&:hover | ||||
| 				background isDark ? #2e3440 : #f5f5f5 | ||||
| 
 | ||||
| 			&:active | ||||
| 				background isDark ? #21242b : #eee | ||||
| 
 | ||||
| .eamppglmnmimdhrlzhplwpvyeaqmmhxu[data-darkmode] | ||||
| 	root(true) | ||||
| 
 | ||||
| .eamppglmnmimdhrlzhplwpvyeaqmmhxu:not([data-darkmode]) | ||||
| 	root(false) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -0,0 +1,22 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<x-column> | ||||
| 		<span slot="header">%fa:bell R% %i18n:@notifications%</span> | ||||
| 
 | ||||
| 		<x-notifications/> | ||||
| 	</x-column> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XNotifications from './deck.notifications.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XNotifications | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,335 @@ | |||
| <template> | ||||
| <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> | ||||
| 	<div class="notifications" v-if="notifications.length != 0"> | ||||
| 		<transition-group name="mk-notifications" class="transition"> | ||||
| 			<template v-for="(notification, i) in _notifications"> | ||||
| 				<div class="notification" :class="notification.type" :key="notification.id"> | ||||
| 					<mk-time :time="notification.createdAt"/> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'reaction'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p> | ||||
| 								<mk-reaction-icon :reaction="notification.reaction"/> | ||||
| 								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> | ||||
| 							</p> | ||||
| 							<router-link class="note-ref" :to="notification.note | notePage"> | ||||
| 								%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% | ||||
| 							</router-link> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'renote'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.note.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p>%fa:retweet% | ||||
| 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> | ||||
| 							</p> | ||||
| 							<router-link class="note-ref" :to="notification.note | notePage"> | ||||
| 								%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% | ||||
| 							</router-link> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'quote'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.note.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p>%fa:quote-left% | ||||
| 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> | ||||
| 							</p> | ||||
| 							<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'follow'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p>%fa:user-plus% | ||||
| 								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'receiveFollowRequest'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p>%fa:user-clock% | ||||
| 								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'reply'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.note.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p>%fa:reply% | ||||
| 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> | ||||
| 							</p> | ||||
| 							<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'mention'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.note.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p>%fa:at% | ||||
| 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> | ||||
| 							</p> | ||||
| 							<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'poll_vote'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p> | ||||
| 							<router-link class="note-ref" :to="notification.note | notePage"> | ||||
| 								%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% | ||||
| 							</router-link> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> | ||||
| 					<span>%fa:angle-up%{{ notification._datetext }}</span> | ||||
| 					<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> | ||||
| 				</p> | ||||
| 			</template> | ||||
| 		</transition-group> | ||||
| 	</div> | ||||
| 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> | ||||
| 		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} | ||||
| 	</button> | ||||
| 	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> | ||||
| 	<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import getNoteSummary from '../../../../../../renderers/get-note-summary'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			fetchingMoreNotifications: false, | ||||
| 			notifications: [], | ||||
| 			moreNotifications: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			getNoteSummary | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		_notifications(): any[] { | ||||
| 			return (this.notifications as any).map(notification => { | ||||
| 				const date = new Date(notification.createdAt).getDate(); | ||||
| 				const month = new Date(notification.createdAt).getMonth() + 1; | ||||
| 				notification._date = date; | ||||
| 				notification._datetext = `${month}月 ${date}日`; | ||||
| 				return notification; | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 
 | ||||
| 		this.connection.on('notification', this.onNotification); | ||||
| 
 | ||||
| 		const max = 10; | ||||
| 
 | ||||
| 		(this as any).api('i/notifications', { | ||||
| 			limit: max + 1 | ||||
| 		}).then(notifications => { | ||||
| 			if (notifications.length == max + 1) { | ||||
| 				this.moreNotifications = true; | ||||
| 				notifications.pop(); | ||||
| 			} | ||||
| 
 | ||||
| 			this.notifications = notifications; | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('notification', this.onNotification); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetchMoreNotifications() { | ||||
| 			this.fetchingMoreNotifications = true; | ||||
| 
 | ||||
| 			const max = 30; | ||||
| 
 | ||||
| 			(this as any).api('i/notifications', { | ||||
| 				limit: max + 1, | ||||
| 				untilId: this.notifications[this.notifications.length - 1].id | ||||
| 			}).then(notifications => { | ||||
| 				if (notifications.length == max + 1) { | ||||
| 					this.moreNotifications = true; | ||||
| 					notifications.pop(); | ||||
| 				} else { | ||||
| 					this.moreNotifications = false; | ||||
| 				} | ||||
| 				this.notifications = this.notifications.concat(notifications); | ||||
| 				this.fetchingMoreNotifications = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 		onNotification(notification) { | ||||
| 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | ||||
| 			this.connection.send({ | ||||
| 				type: 'read_notification', | ||||
| 				id: notification.id | ||||
| 			}); | ||||
| 
 | ||||
| 			this.notifications.unshift(notification); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	.transition | ||||
| 		.mk-notifications-enter | ||||
| 		.mk-notifications-leave-to | ||||
| 			opacity 0 | ||||
| 			transform translateY(-30px) | ||||
| 
 | ||||
| 		> * | ||||
| 			transition transform .3s ease, opacity .3s ease | ||||
| 
 | ||||
| 	> .notifications | ||||
| 		> * | ||||
| 			> .notification | ||||
| 				margin 0 | ||||
| 				padding 16px | ||||
| 				overflow-wrap break-word | ||||
| 				font-size 0.9em | ||||
| 				border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05) | ||||
| 
 | ||||
| 				&:last-child | ||||
| 					border-bottom none | ||||
| 
 | ||||
| 				> .mk-time | ||||
| 					display inline | ||||
| 					position absolute | ||||
| 					top 16px | ||||
| 					right 12px | ||||
| 					vertical-align top | ||||
| 					color isDark ? #606984 : rgba(#000, 0.6) | ||||
| 					font-size small | ||||
| 
 | ||||
| 				&:after | ||||
| 					content "" | ||||
| 					display block | ||||
| 					clear both | ||||
| 
 | ||||
| 				> .avatar | ||||
| 					display block | ||||
| 					float left | ||||
| 					position -webkit-sticky | ||||
| 					position sticky | ||||
| 					top 16px | ||||
| 					width 36px | ||||
| 					height 36px | ||||
| 					border-radius 6px | ||||
| 
 | ||||
| 				> .text | ||||
| 					float right | ||||
| 					width calc(100% - 36px) | ||||
| 					padding-left 8px | ||||
| 
 | ||||
| 					p | ||||
| 						margin 0 | ||||
| 
 | ||||
| 						i, .mk-reaction-icon | ||||
| 							margin-right 4px | ||||
| 
 | ||||
| 				.note-preview | ||||
| 					color isDark ? #c2cad4 : rgba(#000, 0.7) | ||||
| 
 | ||||
| 				.note-ref | ||||
| 					color isDark ? #c2cad4 : rgba(#000, 0.7) | ||||
| 
 | ||||
| 					[data-fa] | ||||
| 						font-size 1em | ||||
| 						font-weight normal | ||||
| 						font-style normal | ||||
| 						display inline-block | ||||
| 						margin-right 3px | ||||
| 
 | ||||
| 				&.renote, &.quote | ||||
| 					.text p i | ||||
| 						color #77B255 | ||||
| 
 | ||||
| 				&.follow | ||||
| 					.text p i | ||||
| 						color #53c7ce | ||||
| 
 | ||||
| 				&.receiveFollowRequest | ||||
| 					.text p i | ||||
| 						color #888 | ||||
| 
 | ||||
| 				&.reply, &.mention | ||||
| 					.text p i | ||||
| 						color #555 | ||||
| 
 | ||||
| 			> .date | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				line-height 32px | ||||
| 				text-align center | ||||
| 				font-size 0.8em | ||||
| 				color isDark ? #666b79 : #aaa | ||||
| 				background isDark ? #242731 : #fdfdfd | ||||
| 				border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05) | ||||
| 
 | ||||
| 				span | ||||
| 					margin 0 16px | ||||
| 
 | ||||
| 				[data-fa] | ||||
| 					margin-right 8px | ||||
| 
 | ||||
| 	> .more | ||||
| 		display block | ||||
| 		width 100% | ||||
| 		padding 16px | ||||
| 		color #555 | ||||
| 		border-top solid 1px rgba(#000, 0.05) | ||||
| 
 | ||||
| 		&:hover | ||||
| 			background rgba(#000, 0.025) | ||||
| 
 | ||||
| 		&:active | ||||
| 			background rgba(#000, 0.05) | ||||
| 
 | ||||
| 		&.fetching | ||||
| 			cursor wait | ||||
| 
 | ||||
| 		> [data-fa] | ||||
| 			margin-right 4px | ||||
| 
 | ||||
| 	> .empty | ||||
| 		margin 0 | ||||
| 		padding 16px | ||||
| 		text-align center | ||||
| 		color #aaa | ||||
| 
 | ||||
| 	> .loading | ||||
| 		margin 0 | ||||
| 		padding 16px | ||||
| 		text-align center | ||||
| 		color #aaa | ||||
| 
 | ||||
| 		> [data-fa] | ||||
| 			margin-right 4px | ||||
| 
 | ||||
| .oxynyeqmfvracxnglgulyqfgqxnxmehl[data-darkmode] | ||||
| 	root(true) | ||||
| 
 | ||||
| .oxynyeqmfvracxnglgulyqfgqxnxmehl:not([data-darkmode]) | ||||
| 	root(false) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -0,0 +1,33 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<x-column> | ||||
| 		<span slot="header"> | ||||
| 			<template v-if="src == 'home'">%fa:home% %i18n:@home%</template> | ||||
| 			<template v-if="src == 'local'">%fa:R comments% %i18n:@local%</template> | ||||
| 			<template v-if="src == 'global'">%fa:globe% %i18n:@global%</template> | ||||
| 			<template v-if="src == 'list'">%fa:list% {{ list.title }}</template> | ||||
| 		</span> | ||||
| 		<x-tl :src="src"/> | ||||
| 	</x-column> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XTl from './deck.tl.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XTl | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		src: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,139 @@ | |||
| <template> | ||||
| 	<x-notes ref="timeline" :more="existMore ? more : null"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNotes | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		src: { | ||||
| 			type: String, | ||||
| 			required: false, | ||||
| 			default: 'home' | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			unreadCount: 0, | ||||
| 			date: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		stream(): any { | ||||
| 			return this.src == 'home' | ||||
| 				? (this as any).os.stream | ||||
| 				: this.src == 'local' | ||||
| 					? (this as any).os.streams.localTimelineStream | ||||
| 					: (this as any).os.streams.globalTimelineStream; | ||||
| 		}, | ||||
| 
 | ||||
| 		endpoint(): string { | ||||
| 			return this.src == 'home' | ||||
| 				? 'notes/timeline' | ||||
| 				: this.src == 'local' | ||||
| 					? 'notes/local-timeline' | ||||
| 					: 'notes/global-timeline'; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = this.stream.getConnection(); | ||||
| 		this.connectionId = this.stream.use(); | ||||
| 
 | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 			this.connection.on('follow', this.onChangeFollowing); | ||||
| 			this.connection.on('unfollow', this.onChangeFollowing); | ||||
| 		} | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 			this.connection.off('follow', this.onChangeFollowing); | ||||
| 			this.connection.off('unfollow', this.onChangeFollowing); | ||||
| 		} | ||||
| 		this.stream.dispose(this.connectionId); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		mount(root) { | ||||
| 			this.$refs.timeline.mount(root); | ||||
| 		}, | ||||
| 
 | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api(this.endpoint, { | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes); | ||||
| 					this.fetching = false; | ||||
| 					this.$emit('loaded'); | ||||
| 				}, rej); | ||||
| 			})); | ||||
| 		}, | ||||
| 
 | ||||
| 		more() { | ||||
| 			this.moreFetching = true; | ||||
| 
 | ||||
| 			const promise = (this as any).api(this.endpoint, { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes | ||||
| 			}); | ||||
| 
 | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
| 
 | ||||
| 			return promise; | ||||
| 		}, | ||||
| 
 | ||||
| 		onNote(note) { | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}, | ||||
| 
 | ||||
| 		onChangeFollowing() { | ||||
| 			this.fetch(); | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,45 @@ | |||
| <template> | ||||
| <mk-ui :class="$style.root"> | ||||
| 	<div class="qlvquzbjribqcaozciifydkngcwtyzje"> | ||||
| 		<x-tl-column src="home"/> | ||||
| 		<x-notifications-column/> | ||||
| 		<x-tl-column src="local"/> | ||||
| 		<x-tl-column src="global"/> | ||||
| 	</div> | ||||
| </mk-ui> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XTlColumn from './deck.tl-column.vue'; | ||||
| import XNotificationsColumn from './deck.notifications-column.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XTlColumn, | ||||
| 		XNotificationsColumn | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" module> | ||||
| .root | ||||
| 	height 100vh | ||||
| </style> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
| 
 | ||||
| root(isDark) | ||||
| 	display flex | ||||
| 	flex 1 | ||||
| 	padding 16px 0 16px 16px | ||||
| 	overflow auto | ||||
| 
 | ||||
| .qlvquzbjribqcaozciifydkngcwtyzje[data-darkmode] | ||||
| 	root(true) | ||||
| 
 | ||||
| .qlvquzbjribqcaozciifydkngcwtyzje:not([data-darkmode]) | ||||
| 	root(false) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -221,7 +221,9 @@ export default async (user: IUser, data: { | |||
| 		} | ||||
| 
 | ||||
| 		// Publish note to global timeline stream
 | ||||
| 		publishGlobalTimelineStream(noteObj); | ||||
| 		if (note.visibility == 'public' && note.replyId == null) { | ||||
| 			publishGlobalTimelineStream(noteObj); | ||||
| 		} | ||||
| 
 | ||||
| 		if (note.visibility == 'specified') { | ||||
| 			data.visibleUsers.forEach(async u => { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue