ハッシュタグタイムラインを実装
This commit is contained in:
		
							parent
							
								
									433dbe179d
								
							
						
					
					
						commit
						109738ccb9
					
				
					 19 changed files with 555 additions and 92 deletions
				
			
		| 
						 | 
					@ -166,6 +166,7 @@ common:
 | 
				
			||||||
    home: "ホーム"
 | 
					    home: "ホーム"
 | 
				
			||||||
    local: "ローカル"
 | 
					    local: "ローカル"
 | 
				
			||||||
    hybrid: "ソーシャル"
 | 
					    hybrid: "ソーシャル"
 | 
				
			||||||
 | 
					    hashtag: "ハッシュタグ"
 | 
				
			||||||
    global: "グローバル"
 | 
					    global: "グローバル"
 | 
				
			||||||
    mentions: "あなた宛て"
 | 
					    mentions: "あなた宛て"
 | 
				
			||||||
    notifications: "通知"
 | 
					    notifications: "通知"
 | 
				
			||||||
| 
						 | 
					@ -916,6 +917,10 @@ desktop/views/components/timeline.vue:
 | 
				
			||||||
  global: "グローバル"
 | 
					  global: "グローバル"
 | 
				
			||||||
  mentions: "あなた宛て"
 | 
					  mentions: "あなた宛て"
 | 
				
			||||||
  list: "リスト"
 | 
					  list: "リスト"
 | 
				
			||||||
 | 
					  hashtag: "ハッシュタグ"
 | 
				
			||||||
 | 
					  add-tag-timeline: "ハッシュタグを追加"
 | 
				
			||||||
 | 
					  add-list: "リストを追加"
 | 
				
			||||||
 | 
					  list-name: "リスト名"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desktop/views/components/ui.header.vue:
 | 
					desktop/views/components/ui.header.vue:
 | 
				
			||||||
  welcome-back: "おかえりなさい、"
 | 
					  welcome-back: "おかえりなさい、"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/client/app/common/scripts/streaming/hashtag.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/client/app/common/scripts/streaming/hashtag.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import Stream from './stream';
 | 
				
			||||||
 | 
					import MiOS from '../../../mios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class HashtagStream extends Stream {
 | 
				
			||||||
 | 
						constructor(os: MiOS, me, q) {
 | 
				
			||||||
 | 
							super(os, 'hashtag', me ? {
 | 
				
			||||||
 | 
								i: me.token,
 | 
				
			||||||
 | 
								q: JSON.stringify(q)
 | 
				
			||||||
 | 
							} : {
 | 
				
			||||||
 | 
								q: JSON.stringify(q)
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,19 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
 | 
					<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
 | 
				
			||||||
	<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
 | 
						<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
 | 
				
			||||||
	<mk-settings @done="close"/>
 | 
						<mk-settings :initial-page="initialPage" @done="close"/>
 | 
				
			||||||
</mk-window>
 | 
					</mk-window>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							initialPage: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		close() {
 | 
							close() {
 | 
				
			||||||
			(this as any).$refs.window.close();
 | 
								(this as any).$refs.window.close();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										65
									
								
								src/client/app/desktop/views/components/settings.tags.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/client/app/desktop/views/components/settings.tags.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="vfcitkilproprqtbnpoertpsziierwzi">
 | 
				
			||||||
 | 
						<div v-for="timeline in timelines" class="timeline">
 | 
				
			||||||
 | 
							<ui-input v-model="timeline.title" @change="save">
 | 
				
			||||||
 | 
								<span>%i18n:@title%</span>
 | 
				
			||||||
 | 
							</ui-input>
 | 
				
			||||||
 | 
							<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)">
 | 
				
			||||||
 | 
								<span>%i18n:@query%</span>
 | 
				
			||||||
 | 
							</ui-textarea>
 | 
				
			||||||
 | 
							<ui-button class="save" @click="save">%i18n:@save%</ui-button>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<ui-button class="add" @click="add">%i18n:@add%</ui-button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import * as uuid from 'uuid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								timelines: this.$store.state.settings.tagTimelines
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							add() {
 | 
				
			||||||
 | 
								this.timelines.push({
 | 
				
			||||||
 | 
									id: uuid(),
 | 
				
			||||||
 | 
									title: '',
 | 
				
			||||||
 | 
									query: ''
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.save();
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							save() {
 | 
				
			||||||
 | 
								this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							onQueryChange(timeline, value) {
 | 
				
			||||||
 | 
								timeline.query = value.split('\n').map(tags => tags.split(' '));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					root(isDark)
 | 
				
			||||||
 | 
						> .timeline
 | 
				
			||||||
 | 
							padding-bottom 16px
 | 
				
			||||||
 | 
							border-bottom solid 1px rgba(#000, 0.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .add
 | 
				
			||||||
 | 
							margin-top 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vfcitkilproprqtbnpoertpsziierwzi[data-darkmode]
 | 
				
			||||||
 | 
						root(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vfcitkilproprqtbnpoertpsziierwzi:not([data-darkmode])
 | 
				
			||||||
 | 
						root(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
		<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
 | 
							<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
 | 
				
			||||||
		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
 | 
							<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
 | 
				
			||||||
		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
 | 
							<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
 | 
				
			||||||
 | 
							<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
 | 
				
			||||||
		<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
 | 
							<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
 | 
				
			||||||
		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
 | 
							<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
 | 
				
			||||||
		<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
 | 
							<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
 | 
				
			||||||
| 
						 | 
					@ -138,6 +139,11 @@
 | 
				
			||||||
			<x-drive/>
 | 
								<x-drive/>
 | 
				
			||||||
		</section>
 | 
							</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<section class="hashtags" v-show="page == 'hashtags'">
 | 
				
			||||||
 | 
								<h1>%i18n:@tags%</h1>
 | 
				
			||||||
 | 
								<x-tags/>
 | 
				
			||||||
 | 
							</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<section class="mute" v-show="page == 'mute'">
 | 
							<section class="mute" v-show="page == 'mute'">
 | 
				
			||||||
			<h1>%i18n:@mute%</h1>
 | 
								<h1>%i18n:@mute%</h1>
 | 
				
			||||||
			<x-mute/>
 | 
								<x-mute/>
 | 
				
			||||||
| 
						 | 
					@ -222,6 +228,7 @@ import XApi from './settings.api.vue';
 | 
				
			||||||
import XApps from './settings.apps.vue';
 | 
					import XApps from './settings.apps.vue';
 | 
				
			||||||
import XSignins from './settings.signins.vue';
 | 
					import XSignins from './settings.signins.vue';
 | 
				
			||||||
import XDrive from './settings.drive.vue';
 | 
					import XDrive from './settings.drive.vue';
 | 
				
			||||||
 | 
					import XTags from './settings.tags.vue';
 | 
				
			||||||
import { url, langs, version } from '../../../config';
 | 
					import { url, langs, version } from '../../../config';
 | 
				
			||||||
import checkForUpdate from '../../../common/scripts/check-for-update';
 | 
					import checkForUpdate from '../../../common/scripts/check-for-update';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -234,11 +241,18 @@ export default Vue.extend({
 | 
				
			||||||
		XApi,
 | 
							XApi,
 | 
				
			||||||
		XApps,
 | 
							XApps,
 | 
				
			||||||
		XSignins,
 | 
							XSignins,
 | 
				
			||||||
		XDrive
 | 
							XDrive,
 | 
				
			||||||
 | 
							XTags
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							initialPage: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			page: 'profile',
 | 
								page: this.initialPage || 'profile',
 | 
				
			||||||
			meta: null,
 | 
								meta: null,
 | 
				
			||||||
			version,
 | 
								version,
 | 
				
			||||||
			langs,
 | 
								langs,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fetchLimit = 10;
 | 
					const fetchLimit = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +24,9 @@ export default Vue.extend({
 | 
				
			||||||
		src: {
 | 
							src: {
 | 
				
			||||||
			type: String,
 | 
								type: String,
 | 
				
			||||||
			required: true
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							tagTl: {
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,6 +35,7 @@ export default Vue.extend({
 | 
				
			||||||
			fetching: true,
 | 
								fetching: true,
 | 
				
			||||||
			moreFetching: false,
 | 
								moreFetching: false,
 | 
				
			||||||
			existMore: false,
 | 
								existMore: false,
 | 
				
			||||||
 | 
								streamManager: null,
 | 
				
			||||||
			connection: null,
 | 
								connection: null,
 | 
				
			||||||
			connectionId: null,
 | 
								connectionId: null,
 | 
				
			||||||
			date: null
 | 
								date: null
 | 
				
			||||||
| 
						 | 
					@ -42,16 +47,6 @@ export default Vue.extend({
 | 
				
			||||||
			return this.$store.state.i.followingCount == 0;
 | 
								return this.$store.state.i.followingCount == 0;
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		stream(): any {
 | 
					 | 
				
			||||||
			switch (this.src) {
 | 
					 | 
				
			||||||
				case 'home': return (this as any).os.stream;
 | 
					 | 
				
			||||||
				case 'local': return (this as any).os.streams.localTimelineStream;
 | 
					 | 
				
			||||||
				case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
 | 
					 | 
				
			||||||
				case 'global': return (this as any).os.streams.globalTimelineStream;
 | 
					 | 
				
			||||||
				case 'mentions': return (this as any).os.stream;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		endpoint(): string {
 | 
							endpoint(): string {
 | 
				
			||||||
			switch (this.src) {
 | 
								switch (this.src) {
 | 
				
			||||||
				case 'home': return 'notes/timeline';
 | 
									case 'home': return 'notes/timeline';
 | 
				
			||||||
| 
						 | 
					@ -59,6 +54,7 @@ export default Vue.extend({
 | 
				
			||||||
				case 'hybrid': return 'notes/hybrid-timeline';
 | 
									case 'hybrid': return 'notes/hybrid-timeline';
 | 
				
			||||||
				case 'global': return 'notes/global-timeline';
 | 
									case 'global': return 'notes/global-timeline';
 | 
				
			||||||
				case 'mentions': return 'notes/mentions';
 | 
									case 'mentions': return 'notes/mentions';
 | 
				
			||||||
 | 
									case 'tag': return 'notes/search_by_tag';
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,13 +64,36 @@ export default Vue.extend({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
						mounted() {
 | 
				
			||||||
		this.connection = this.stream.getConnection();
 | 
							if (this.src == 'tag') {
 | 
				
			||||||
		this.connectionId = this.stream.use();
 | 
								this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
		this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
 | 
							} else if (this.src == 'home') {
 | 
				
			||||||
		if (this.src == 'home') {
 | 
								this.streamManager = (this as any).os.stream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
			this.connection.on('follow', this.onChangeFollowing);
 | 
								this.connection.on('follow', this.onChangeFollowing);
 | 
				
			||||||
			this.connection.on('unfollow', this.onChangeFollowing);
 | 
								this.connection.on('unfollow', this.onChangeFollowing);
 | 
				
			||||||
 | 
							} else if (this.src == 'local') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.streams.localTimelineStream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
 | 
							} else if (this.src == 'hybrid') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.streams.hybridTimelineStream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
 | 
							} else if (this.src == 'global') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.streams.globalTimelineStream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
 | 
							} else if (this.src == 'mentions') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.stream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('mention', this.onNote);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		document.addEventListener('keydown', this.onKeydown);
 | 
							document.addEventListener('keydown', this.onKeydown);
 | 
				
			||||||
| 
						 | 
					@ -83,12 +102,27 @@ export default Vue.extend({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	beforeDestroy() {
 | 
						beforeDestroy() {
 | 
				
			||||||
		this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
 | 
							if (this.src == 'tag') {
 | 
				
			||||||
		if (this.src == 'home') {
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.connection.close();
 | 
				
			||||||
 | 
							} else if (this.src == 'home') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
			this.connection.off('follow', this.onChangeFollowing);
 | 
								this.connection.off('follow', this.onChangeFollowing);
 | 
				
			||||||
			this.connection.off('unfollow', this.onChangeFollowing);
 | 
								this.connection.off('unfollow', this.onChangeFollowing);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'local') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'hybrid') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'global') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'mentions') {
 | 
				
			||||||
 | 
								this.connection.off('mention', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.stream.dispose(this.connectionId);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		document.removeEventListener('keydown', this.onKeydown);
 | 
							document.removeEventListener('keydown', this.onKeydown);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					@ -103,7 +137,8 @@ export default Vue.extend({
 | 
				
			||||||
					untilDate: this.date ? this.date.getTime() : undefined,
 | 
										untilDate: this.date ? this.date.getTime() : undefined,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
					includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
 | 
				
			||||||
 | 
										query: this.tagTl ? this.tagTl.query : undefined
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -126,7 +161,8 @@ export default Vue.extend({
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
 | 
				
			||||||
 | 
									query: this.tagTl ? this.tagTl.query : undefined
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,14 +6,19 @@
 | 
				
			||||||
		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
 | 
							<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
 | 
				
			||||||
		<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
 | 
							<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
 | 
				
			||||||
		<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
 | 
							<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
 | 
				
			||||||
 | 
							<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
 | 
				
			||||||
		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
 | 
							<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
 | 
				
			||||||
		<button @click="chooseList" title="%i18n:@list%">%fa:list%</button>
 | 
							<div class="buttons">
 | 
				
			||||||
 | 
								<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
 | 
				
			||||||
 | 
								<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
	</header>
 | 
						</header>
 | 
				
			||||||
	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
 | 
						<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 == 'local'" ref="tl" key="local" src="local"/>
 | 
				
			||||||
	<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
 | 
						<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 == 'global'" ref="tl" key="global" src="global"/>
 | 
				
			||||||
	<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
 | 
						<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
 | 
				
			||||||
 | 
						<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"/>
 | 
						<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -21,7 +26,8 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
import XCore from './timeline.core.vue';
 | 
					import XCore from './timeline.core.vue';
 | 
				
			||||||
import MkUserListsWindow from './user-lists-window.vue';
 | 
					import Menu from '../../../common/views/components/menu.vue';
 | 
				
			||||||
 | 
					import MkSettingsWindow from './settings-window.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
| 
						 | 
					@ -32,6 +38,7 @@ export default Vue.extend({
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			src: 'home',
 | 
								src: 'home',
 | 
				
			||||||
			list: null,
 | 
								list: null,
 | 
				
			||||||
 | 
								tagTl: null,
 | 
				
			||||||
			enableLocalTimeline: false
 | 
								enableLocalTimeline: false
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					@ -41,8 +48,14 @@ export default Vue.extend({
 | 
				
			||||||
			this.saveSrc();
 | 
								this.saveSrc();
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		list() {
 | 
							list(x) {
 | 
				
			||||||
			this.saveSrc();
 | 
								this.saveSrc();
 | 
				
			||||||
 | 
								if (x != null) this.tagTl = null;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tagTl(x) {
 | 
				
			||||||
 | 
								this.saveSrc();
 | 
				
			||||||
 | 
								if (x != null) this.list = null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,6 +68,8 @@ export default Vue.extend({
 | 
				
			||||||
			this.src = this.$store.state.device.tl.src;
 | 
								this.src = this.$store.state.device.tl.src;
 | 
				
			||||||
			if (this.src == 'list') {
 | 
								if (this.src == 'list') {
 | 
				
			||||||
				this.list = this.$store.state.device.tl.arg;
 | 
									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) {
 | 
							} else if (this.$store.state.i.followingCount == 0) {
 | 
				
			||||||
			this.src = 'hybrid';
 | 
								this.src = 'hybrid';
 | 
				
			||||||
| 
						 | 
					@ -71,7 +86,7 @@ export default Vue.extend({
 | 
				
			||||||
		saveSrc() {
 | 
							saveSrc() {
 | 
				
			||||||
			this.$store.commit('device/setTl', {
 | 
								this.$store.commit('device/setTl', {
 | 
				
			||||||
				src: this.src,
 | 
									src: this.src,
 | 
				
			||||||
				arg: this.list
 | 
									arg: this.src == 'list' ? this.list : this.tagTl
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,12 +94,74 @@ export default Vue.extend({
 | 
				
			||||||
			(this.$refs.tl as any).warp(date);
 | 
								(this.$refs.tl as any).warp(date);
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		chooseList() {
 | 
							async chooseList() {
 | 
				
			||||||
			const w = (this as any).os.new(MkUserListsWindow);
 | 
								const lists = await (this as any).api('users/lists/list');
 | 
				
			||||||
			w.$once('choosen', list => {
 | 
					
 | 
				
			||||||
				this.list = list;
 | 
								let menu = [{
 | 
				
			||||||
				this.src = 'list';
 | 
									icon: '%fa:plus%',
 | 
				
			||||||
				w.close();
 | 
									text: '%i18n:@add-list%',
 | 
				
			||||||
 | 
									action: () => {
 | 
				
			||||||
 | 
										(this as any).apis.input({
 | 
				
			||||||
 | 
											title: '%i18n:@list-name%',
 | 
				
			||||||
 | 
										}).then(async title => {
 | 
				
			||||||
 | 
											const list = await (this as any).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: '%fa:list%',
 | 
				
			||||||
 | 
									text: list.title,
 | 
				
			||||||
 | 
									action: () => {
 | 
				
			||||||
 | 
										this.list = list;
 | 
				
			||||||
 | 
										this.src = 'list';
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.os.new(Menu, {
 | 
				
			||||||
 | 
									source: this.$refs.listButton,
 | 
				
			||||||
 | 
									compact: false,
 | 
				
			||||||
 | 
									items: menu
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							chooseTag() {
 | 
				
			||||||
 | 
								let menu = [{
 | 
				
			||||||
 | 
									icon: '%fa:plus%',
 | 
				
			||||||
 | 
									text: '%i18n:@add-tag-timeline%',
 | 
				
			||||||
 | 
									action: () => {
 | 
				
			||||||
 | 
										(this as any).os.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: '%fa:hashtag%',
 | 
				
			||||||
 | 
									text: t.title,
 | 
				
			||||||
 | 
									action: () => {
 | 
				
			||||||
 | 
										this.tagTl = t;
 | 
				
			||||||
 | 
										this.src = 'tag';
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.os.new(Menu, {
 | 
				
			||||||
 | 
									source: this.$refs.tagButton,
 | 
				
			||||||
 | 
									compact: false,
 | 
				
			||||||
 | 
									items: menu
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -106,22 +183,24 @@ root(isDark)
 | 
				
			||||||
		border-radius 6px 6px 0 0
 | 
							border-radius 6px 6px 0 0
 | 
				
			||||||
		box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
 | 
							box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		> button
 | 
							> .buttons
 | 
				
			||||||
			position absolute
 | 
								position absolute
 | 
				
			||||||
			z-index 2
 | 
								z-index 2
 | 
				
			||||||
			top 0
 | 
								top 0
 | 
				
			||||||
			right 0
 | 
								right 0
 | 
				
			||||||
			padding 0
 | 
					 | 
				
			||||||
			width 42px
 | 
					 | 
				
			||||||
			font-size 0.9em
 | 
					 | 
				
			||||||
			line-height 42px
 | 
					 | 
				
			||||||
			color isDark ? #9baec8 : #ccc
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			&:hover
 | 
								> button
 | 
				
			||||||
				color isDark ? #b2c1d5 : #aaa
 | 
									padding 0
 | 
				
			||||||
 | 
									width 42px
 | 
				
			||||||
 | 
									font-size 0.9em
 | 
				
			||||||
 | 
									line-height 42px
 | 
				
			||||||
 | 
									color isDark ? #9baec8 : #ccc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			&:active
 | 
									&:hover
 | 
				
			||||||
				color isDark ? #b2c1d5 : #999
 | 
										color isDark ? #b2c1d5 : #aaa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&:active
 | 
				
			||||||
 | 
										color isDark ? #b2c1d5 : #999
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		> span
 | 
							> span
 | 
				
			||||||
			display inline-block
 | 
								display inline-block
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@
 | 
				
			||||||
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/>
 | 
					<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/>
 | 
				
			||||||
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
 | 
					<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
 | 
				
			||||||
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
 | 
					<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
 | 
				
			||||||
 | 
					<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/>
 | 
				
			||||||
<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
 | 
					<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										117
									
								
								src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,117 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import XNotes from './deck.notes.vue';
 | 
				
			||||||
 | 
					import { HashtagStream } from '../../../../common/scripts/streaming/hashtag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fetchLimit = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XNotes
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							tagTl: {
 | 
				
			||||||
 | 
								type: Object,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							mediaOnly: {
 | 
				
			||||||
 | 
								type: Boolean,
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: false
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							mediaView: {
 | 
				
			||||||
 | 
								type: Boolean,
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								fetching: true,
 | 
				
			||||||
 | 
								moreFetching: false,
 | 
				
			||||||
 | 
								existMore: false,
 | 
				
			||||||
 | 
								connection: null
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						watch: {
 | 
				
			||||||
 | 
							mediaOnly() {
 | 
				
			||||||
 | 
								this.fetch();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mounted() {
 | 
				
			||||||
 | 
							if (this.connection) this.connection.close();
 | 
				
			||||||
 | 
							this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
 | 
				
			||||||
 | 
							this.connection.on('note', this.onNote);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.fetch();
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						beforeDestroy() {
 | 
				
			||||||
 | 
							this.connection.close();
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							fetch() {
 | 
				
			||||||
 | 
								this.fetching = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
 | 
				
			||||||
 | 
									(this as any).api('notes/search_by_tag', {
 | 
				
			||||||
 | 
										limit: fetchLimit + 1,
 | 
				
			||||||
 | 
										withFiles: this.mediaOnly,
 | 
				
			||||||
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
 | 
				
			||||||
 | 
										query: this.tagTl.query
 | 
				
			||||||
 | 
									}).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('notes/search_by_tag', {
 | 
				
			||||||
 | 
									limit: fetchLimit + 1,
 | 
				
			||||||
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
 | 
									withFiles: this.mediaOnly,
 | 
				
			||||||
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
 | 
				
			||||||
 | 
									query: this.tagTl.query
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								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) {
 | 
				
			||||||
 | 
								if (this.mediaOnly && note.files.length == 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Prepend a note
 | 
				
			||||||
 | 
								(this.$refs.timeline as any).prepend(note);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@
 | 
				
			||||||
		<template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
 | 
							<template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
 | 
				
			||||||
		<template v-if="column.type == 'global'">%fa:globe%</template>
 | 
							<template v-if="column.type == 'global'">%fa:globe%</template>
 | 
				
			||||||
		<template v-if="column.type == 'list'">%fa:list%</template>
 | 
							<template v-if="column.type == 'list'">%fa:list%</template>
 | 
				
			||||||
 | 
							<template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
 | 
				
			||||||
		<span>{{ name }}</span>
 | 
							<span>{{ name }}</span>
 | 
				
			||||||
	</span>
 | 
						</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +15,7 @@
 | 
				
			||||||
		<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
 | 
							<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
 | 
						<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
 | 
				
			||||||
 | 
						<x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
 | 
				
			||||||
	<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
 | 
						<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
 | 
				
			||||||
</x-column>
 | 
					</x-column>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -23,12 +25,14 @@ import Vue from 'vue';
 | 
				
			||||||
import XColumn from './deck.column.vue';
 | 
					import XColumn from './deck.column.vue';
 | 
				
			||||||
import XTl from './deck.tl.vue';
 | 
					import XTl from './deck.tl.vue';
 | 
				
			||||||
import XListTl from './deck.list-tl.vue';
 | 
					import XListTl from './deck.list-tl.vue';
 | 
				
			||||||
 | 
					import XHashtagTl from './deck.hashtag-tl.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
		XColumn,
 | 
							XColumn,
 | 
				
			||||||
		XTl,
 | 
							XTl,
 | 
				
			||||||
		XListTl
 | 
							XListTl,
 | 
				
			||||||
 | 
							XHashtagTl
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
| 
						 | 
					@ -65,6 +69,7 @@ export default Vue.extend({
 | 
				
			||||||
				case 'hybrid': return '%i18n:common.deck.hybrid%';
 | 
									case 'hybrid': return '%i18n:common.deck.hybrid%';
 | 
				
			||||||
				case 'global': return '%i18n:common.deck.global%';
 | 
									case 'global': return '%i18n:common.deck.global%';
 | 
				
			||||||
				case 'list': return this.column.list.title;
 | 
									case 'list': return this.column.list.title;
 | 
				
			||||||
 | 
									case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -161,6 +161,20 @@ export default Vue.extend({
 | 
				
			||||||
							w.close();
 | 
												w.close();
 | 
				
			||||||
						});
 | 
											});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										icon: '%fa:hashtag%',
 | 
				
			||||||
 | 
										text: '%i18n:common.deck.hashtag%',
 | 
				
			||||||
 | 
										action: () => {
 | 
				
			||||||
 | 
											(this as any).apis.input({
 | 
				
			||||||
 | 
												title: '%i18n:@enter-hashtag-tl-title%'
 | 
				
			||||||
 | 
											}).then(title => {
 | 
				
			||||||
 | 
												this.$store.dispatch('settings/addDeckColumn', {
 | 
				
			||||||
 | 
													id: uuid(),
 | 
				
			||||||
 | 
													type: 'hashtag',
 | 
				
			||||||
 | 
													tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
 | 
				
			||||||
 | 
												});
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
				}, {
 | 
									}, {
 | 
				
			||||||
					icon: '%fa:bell R%',
 | 
										icon: '%fa:bell R%',
 | 
				
			||||||
					text: '%i18n:common.deck.notifications%',
 | 
										text: '%i18n:common.deck.notifications%',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fetchLimit = 10;
 | 
					const fetchLimit = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +22,9 @@ export default Vue.extend({
 | 
				
			||||||
		src: {
 | 
							src: {
 | 
				
			||||||
			type: String,
 | 
								type: String,
 | 
				
			||||||
			required: true
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							tagTl: {
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +33,7 @@ export default Vue.extend({
 | 
				
			||||||
			fetching: true,
 | 
								fetching: true,
 | 
				
			||||||
			moreFetching: false,
 | 
								moreFetching: false,
 | 
				
			||||||
			existMore: false,
 | 
								existMore: false,
 | 
				
			||||||
 | 
								streamManager: null,
 | 
				
			||||||
			connection: null,
 | 
								connection: null,
 | 
				
			||||||
			connectionId: null,
 | 
								connectionId: null,
 | 
				
			||||||
			unreadCount: 0,
 | 
								unreadCount: 0,
 | 
				
			||||||
| 
						 | 
					@ -41,16 +46,6 @@ export default Vue.extend({
 | 
				
			||||||
			return this.$store.state.i.followingCount == 0;
 | 
								return this.$store.state.i.followingCount == 0;
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		stream(): any {
 | 
					 | 
				
			||||||
			switch (this.src) {
 | 
					 | 
				
			||||||
				case 'home': return (this as any).os.stream;
 | 
					 | 
				
			||||||
				case 'local': return (this as any).os.streams.localTimelineStream;
 | 
					 | 
				
			||||||
				case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
 | 
					 | 
				
			||||||
				case 'global': return (this as any).os.streams.globalTimelineStream;
 | 
					 | 
				
			||||||
				case 'mentions': return (this as any).os.stream;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		endpoint(): string {
 | 
							endpoint(): string {
 | 
				
			||||||
			switch (this.src) {
 | 
								switch (this.src) {
 | 
				
			||||||
				case 'home': return 'notes/timeline';
 | 
									case 'home': return 'notes/timeline';
 | 
				
			||||||
| 
						 | 
					@ -58,6 +53,7 @@ export default Vue.extend({
 | 
				
			||||||
				case 'hybrid': return 'notes/hybrid-timeline';
 | 
									case 'hybrid': return 'notes/hybrid-timeline';
 | 
				
			||||||
				case 'global': return 'notes/global-timeline';
 | 
									case 'global': return 'notes/global-timeline';
 | 
				
			||||||
				case 'mentions': return 'notes/mentions';
 | 
									case 'mentions': return 'notes/mentions';
 | 
				
			||||||
 | 
									case 'tag': return 'notes/search_by_tag';
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,25 +63,63 @@ export default Vue.extend({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
						mounted() {
 | 
				
			||||||
		this.connection = this.stream.getConnection();
 | 
							if (this.src == 'tag') {
 | 
				
			||||||
		this.connectionId = this.stream.use();
 | 
								this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
		this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
 | 
							} else if (this.src == 'home') {
 | 
				
			||||||
		if (this.src == 'home') {
 | 
								this.streamManager = (this as any).os.stream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
			this.connection.on('follow', this.onChangeFollowing);
 | 
								this.connection.on('follow', this.onChangeFollowing);
 | 
				
			||||||
			this.connection.on('unfollow', this.onChangeFollowing);
 | 
								this.connection.on('unfollow', this.onChangeFollowing);
 | 
				
			||||||
 | 
							} else if (this.src == 'local') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.streams.localTimelineStream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
 | 
							} else if (this.src == 'hybrid') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.streams.hybridTimelineStream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
 | 
							} else if (this.src == 'global') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.streams.globalTimelineStream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('note', this.onNote);
 | 
				
			||||||
 | 
							} else if (this.src == 'mentions') {
 | 
				
			||||||
 | 
								this.streamManager = (this as any).os.stream;
 | 
				
			||||||
 | 
								this.connection = this.streamManager.getConnection();
 | 
				
			||||||
 | 
								this.connectionId = this.streamManager.use();
 | 
				
			||||||
 | 
								this.connection.on('mention', this.onNote);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.fetch();
 | 
							this.fetch();
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	beforeDestroy() {
 | 
						beforeDestroy() {
 | 
				
			||||||
		this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
 | 
							if (this.src == 'tag') {
 | 
				
			||||||
		if (this.src == 'home') {
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.connection.close();
 | 
				
			||||||
 | 
							} else if (this.src == 'home') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
			this.connection.off('follow', this.onChangeFollowing);
 | 
								this.connection.off('follow', this.onChangeFollowing);
 | 
				
			||||||
			this.connection.off('unfollow', this.onChangeFollowing);
 | 
								this.connection.off('unfollow', this.onChangeFollowing);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'local') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'hybrid') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'global') {
 | 
				
			||||||
 | 
								this.connection.off('note', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
 | 
							} else if (this.src == 'mentions') {
 | 
				
			||||||
 | 
								this.connection.off('mention', this.onNote);
 | 
				
			||||||
 | 
								this.streamManager.dispose(this.connectionId);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.stream.dispose(this.connectionId);
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
| 
						 | 
					@ -98,7 +132,8 @@ export default Vue.extend({
 | 
				
			||||||
					untilDate: this.date ? this.date.getTime() : undefined,
 | 
										untilDate: this.date ? this.date.getTime() : undefined,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
					includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
 | 
				
			||||||
 | 
										query: this.tagTl ? this.tagTl.query : undefined
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -121,7 +156,8 @@ export default Vue.extend({
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
 | 
				
			||||||
 | 
									query: this.tagTl ? this.tagTl.query : undefined
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@
 | 
				
			||||||
			<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
 | 
								<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
 | 
				
			||||||
			<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
 | 
								<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
 | 
				
			||||||
			<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
 | 
								<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
 | 
				
			||||||
 | 
								<span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
 | 
				
			||||||
		</span>
 | 
							</span>
 | 
				
			||||||
		<span style="margin-left:8px">
 | 
							<span style="margin-left:8px">
 | 
				
			||||||
			<template v-if="!showNav">%fa:angle-down%</template>
 | 
								<template v-if="!showNav">%fa:angle-down%</template>
 | 
				
			||||||
| 
						 | 
					@ -32,6 +33,7 @@
 | 
				
			||||||
					<template v-if="lists">
 | 
										<template v-if="lists">
 | 
				
			||||||
						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
 | 
											<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
 | 
				
			||||||
					</template>
 | 
										</template>
 | 
				
			||||||
 | 
										<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
| 
						 | 
					@ -42,6 +44,7 @@
 | 
				
			||||||
			<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
 | 
								<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
 | 
				
			||||||
			<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
 | 
								<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
 | 
				
			||||||
			<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
 | 
								<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
 | 
				
			||||||
 | 
								<x-tl 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"/>
 | 
								<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</main>
 | 
						</main>
 | 
				
			||||||
| 
						 | 
					@ -63,6 +66,7 @@ export default Vue.extend({
 | 
				
			||||||
			src: 'home',
 | 
								src: 'home',
 | 
				
			||||||
			list: null,
 | 
								list: null,
 | 
				
			||||||
			lists: null,
 | 
								lists: null,
 | 
				
			||||||
 | 
								tagTl: null,
 | 
				
			||||||
			showNav: false,
 | 
								showNav: false,
 | 
				
			||||||
			enableLocalTimeline: false
 | 
								enableLocalTimeline: false
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
| 
						 | 
					@ -74,9 +78,16 @@ export default Vue.extend({
 | 
				
			||||||
			this.saveSrc();
 | 
								this.saveSrc();
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		list() {
 | 
							list(x) {
 | 
				
			||||||
			this.showNav = false;
 | 
								this.showNav = false;
 | 
				
			||||||
			this.saveSrc();
 | 
								this.saveSrc();
 | 
				
			||||||
 | 
								if (x != null) this.tagTl = null;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tagTl(x) {
 | 
				
			||||||
 | 
								this.showNav = false;
 | 
				
			||||||
 | 
								this.saveSrc();
 | 
				
			||||||
 | 
								if (x != null) this.list = null;
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		showNav(v) {
 | 
							showNav(v) {
 | 
				
			||||||
| 
						 | 
					@ -97,6 +108,8 @@ export default Vue.extend({
 | 
				
			||||||
			this.src = this.$store.state.device.tl.src;
 | 
								this.src = this.$store.state.device.tl.src;
 | 
				
			||||||
			if (this.src == 'list') {
 | 
								if (this.src == 'list') {
 | 
				
			||||||
				this.list = this.$store.state.device.tl.arg;
 | 
									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) {
 | 
							} else if (this.$store.state.i.followingCount == 0) {
 | 
				
			||||||
			this.src = 'hybrid';
 | 
								this.src = 'hybrid';
 | 
				
			||||||
| 
						 | 
					@ -121,7 +134,7 @@ export default Vue.extend({
 | 
				
			||||||
		saveSrc() {
 | 
							saveSrc() {
 | 
				
			||||||
			this.$store.commit('device/setTl', {
 | 
								this.$store.commit('device/setTl', {
 | 
				
			||||||
				src: this.src,
 | 
									src: this.src,
 | 
				
			||||||
				arg: this.list
 | 
									arg: this.src == 'list' ? this.list : this.tagTl
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ const defaultSettings = {
 | 
				
			||||||
	home: null,
 | 
						home: null,
 | 
				
			||||||
	mobileHome: [],
 | 
						mobileHome: [],
 | 
				
			||||||
	deck: null,
 | 
						deck: null,
 | 
				
			||||||
 | 
						tagTimelines: [],
 | 
				
			||||||
	fetchOnScroll: true,
 | 
						fetchOnScroll: true,
 | 
				
			||||||
	showMaps: true,
 | 
						showMaps: true,
 | 
				
			||||||
	showPostFormOnTopOfTl: false,
 | 
						showPostFormOnTopOfTl: false,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,12 +13,18 @@ export const meta = {
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	params: {
 | 
						params: {
 | 
				
			||||||
		tag: $.str.note({
 | 
							tag: $.str.optional.note({
 | 
				
			||||||
			desc: {
 | 
								desc: {
 | 
				
			||||||
				'ja-JP': 'タグ'
 | 
									'ja-JP': 'タグ'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							query: $.arr($.arr($.str)).optional.note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									'ja-JP': 'クエリ'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		includeUserIds: $.arr($.type(ID)).optional.note({
 | 
							includeUserIds: $.arr($.type(ID)).optional.note({
 | 
				
			||||||
			default: []
 | 
								default: []
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
| 
						 | 
					@ -59,11 +65,9 @@ export const meta = {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		withFiles: $.bool.optional.nullable.note({
 | 
							withFiles: $.bool.optional.note({
 | 
				
			||||||
			default: null,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			desc: {
 | 
								desc: {
 | 
				
			||||||
				'ja-JP': 'ファイルが添付された投稿に限定するか否か'
 | 
									'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -126,8 +130,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const q: any = {
 | 
						const q: any = {
 | 
				
			||||||
		$and: [{
 | 
							$and: [ps.tag ? {
 | 
				
			||||||
			tagsLower: ps.tag.toLowerCase()
 | 
								tagsLower: ps.tag.toLowerCase()
 | 
				
			||||||
 | 
							} : {
 | 
				
			||||||
 | 
								$or: ps.query.map(tags => ({
 | 
				
			||||||
 | 
									$and: tags.map(t => ({
 | 
				
			||||||
 | 
										tagsLower: t.toLowerCase()
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
 | 
								}))
 | 
				
			||||||
		}],
 | 
							}],
 | 
				
			||||||
		deletedAt: { $exists: false }
 | 
							deletedAt: { $exists: false }
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
| 
						 | 
					@ -281,25 +291,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
 | 
						const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (withFiles != null) {
 | 
						if (withFiles) {
 | 
				
			||||||
		if (withFiles) {
 | 
							push({
 | 
				
			||||||
			push({
 | 
								fileIds: { $exists: true, $ne: [] }
 | 
				
			||||||
				fileIds: {
 | 
							});
 | 
				
			||||||
					$exists: true,
 | 
					 | 
				
			||||||
					$ne: null
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			push({
 | 
					 | 
				
			||||||
				$or: [{
 | 
					 | 
				
			||||||
					fileIds: {
 | 
					 | 
				
			||||||
						$exists: false
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					fileIds: null
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (ps.poll != null) {
 | 
						if (ps.poll != null) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										48
									
								
								src/server/api/stream/hashtag.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/server/api/stream/hashtag.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					import * as websocket from 'websocket';
 | 
				
			||||||
 | 
					import Xev from 'xev';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { IUser } from '../../../models/user';
 | 
				
			||||||
 | 
					import Mute from '../../../models/mute';
 | 
				
			||||||
 | 
					import { pack } from '../../../models/note';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function(
 | 
				
			||||||
 | 
						request: websocket.request,
 | 
				
			||||||
 | 
						connection: websocket.connection,
 | 
				
			||||||
 | 
						subscriber: Xev,
 | 
				
			||||||
 | 
						user?: IUser
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						const mute = user ? await Mute.find({ muterId: user._id }) : null;
 | 
				
			||||||
 | 
						const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const q: Array<string[]> = JSON.parse((request.resourceURL.query as any).q);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Subscribe stream
 | 
				
			||||||
 | 
						subscriber.on('hashtag', async note => {
 | 
				
			||||||
 | 
							const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
 | 
				
			||||||
 | 
							if (!matched) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Renoteなら再pack
 | 
				
			||||||
 | 
							if (note.renoteId != null) {
 | 
				
			||||||
 | 
								note.renote = await pack(note.renoteId, user, {
 | 
				
			||||||
 | 
									detail: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | 
				
			||||||
 | 
							if (mutedUserIds.indexOf(note.userId) != -1) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							connection.send(JSON.stringify({
 | 
				
			||||||
 | 
								type: 'note',
 | 
				
			||||||
 | 
								body: note
 | 
				
			||||||
 | 
							}));
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ import reversiGameStream from './stream/games/reversi-game';
 | 
				
			||||||
import reversiStream from './stream/games/reversi';
 | 
					import reversiStream from './stream/games/reversi';
 | 
				
			||||||
import serverStatsStream from './stream/server-stats';
 | 
					import serverStatsStream from './stream/server-stats';
 | 
				
			||||||
import notesStatsStream from './stream/notes-stats';
 | 
					import notesStatsStream from './stream/notes-stats';
 | 
				
			||||||
 | 
					import hashtagStream from './stream/hashtag';
 | 
				
			||||||
import { ParsedUrlQuery } from 'querystring';
 | 
					import { ParsedUrlQuery } from 'querystring';
 | 
				
			||||||
import authenticate from './authenticate';
 | 
					import authenticate from './authenticate';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,6 +58,11 @@ module.exports = (server: http.Server) => {
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (request.resourceURL.pathname === '/hashtag') {
 | 
				
			||||||
 | 
								hashtagStream(request, connection, ev, user);
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (user == null) {
 | 
							if (user == null) {
 | 
				
			||||||
			connection.send('authentication-failed');
 | 
								connection.send('authentication-failed');
 | 
				
			||||||
			connection.close();
 | 
								connection.close();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import es from '../../db/elasticsearch';
 | 
					import es from '../../db/elasticsearch';
 | 
				
			||||||
import Note, { pack, INote } from '../../models/note';
 | 
					import Note, { pack, INote } from '../../models/note';
 | 
				
			||||||
import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
 | 
					import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
 | 
				
			||||||
import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream';
 | 
					import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream';
 | 
				
			||||||
import Following from '../../models/following';
 | 
					import Following from '../../models/following';
 | 
				
			||||||
import { deliver } from '../../queue';
 | 
					import { deliver } from '../../queue';
 | 
				
			||||||
import renderNote from '../../remote/activitypub/renderer/note';
 | 
					import renderNote from '../../remote/activitypub/renderer/note';
 | 
				
			||||||
| 
						 | 
					@ -181,6 +181,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 | 
				
			||||||
		noteObj.isFirstNote = true;
 | 
							noteObj.isFirstNote = true;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (tags.length > 0) {
 | 
				
			||||||
 | 
							publishHashtagStream(noteObj);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const nm = new NotificationManager(user, note);
 | 
						const nm = new NotificationManager(user, note);
 | 
				
			||||||
	const nmRelatedPromises = [];
 | 
						const nmRelatedPromises = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,6 +78,10 @@ class Publisher {
 | 
				
			||||||
	public publishGlobalTimelineStream = (note: any): void => {
 | 
						public publishGlobalTimelineStream = (note: any): void => {
 | 
				
			||||||
		this.publish('global-timeline', null, note);
 | 
							this.publish('global-timeline', null, note);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public publishHashtagStream = (note: any): void => {
 | 
				
			||||||
 | 
							this.publish('hashtag', null, note);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const publisher = new Publisher();
 | 
					const publisher = new Publisher();
 | 
				
			||||||
| 
						 | 
					@ -95,3 +99,4 @@ export const publishReversiGameStream = publisher.publishReversiGameStream;
 | 
				
			||||||
export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
 | 
					export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
 | 
				
			||||||
export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
 | 
					export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
 | 
				
			||||||
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
 | 
					export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
 | 
				
			||||||
 | 
					export const publishHashtagStream = publisher.publishHashtagStream;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue