diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2018-09-18 02:14:12 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2018-09-18 02:14:12 +0900 |
| commit | ab83e08bc7deb244d35e2315abead473d536d2c3 (patch) | |
| tree | 1a6b441f69460b0bf620694d7b3f2019dfe4c830 /src | |
| parent | Refactor (diff) | |
| download | sharkey-ab83e08bc7deb244d35e2315abead473d536d2c3.tar.gz sharkey-ab83e08bc7deb244d35e2315abead473d536d2c3.tar.bz2 sharkey-ab83e08bc7deb244d35e2315abead473d536d2c3.zip | |
メッセージタイムラインを追加
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/app/desktop/views/components/timeline.core.vue | 148 | ||||
| -rw-r--r-- | src/client/app/desktop/views/components/timeline.vue | 18 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/deck/deck.column-core.vue | 5 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/deck/deck.direct-column.vue | 38 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/deck/deck.direct.vue | 97 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/deck/deck.vue | 9 | ||||
| -rw-r--r-- | src/client/app/mobile/views/pages/home.timeline.vue | 147 | ||||
| -rw-r--r-- | src/client/app/mobile/views/pages/home.vue | 49 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/mentions.ts | 7 | ||||
| -rw-r--r-- | src/services/note/create.ts | 22 |
10 files changed, 390 insertions, 150 deletions
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index d2176dee87..c8aa36f171 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -38,7 +38,14 @@ export default Vue.extend({ streamManager: null, connection: null, connectionId: null, - date: null + date: null, + baseQuery: { + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }, + query: {}, + endpoint: null }; }, @@ -47,53 +54,102 @@ export default Vue.extend({ return this.$store.state.i.followingCount == 0; }, - endpoint(): string { - switch (this.src) { - case 'home': return 'notes/timeline'; - case 'local': return 'notes/local-timeline'; - case 'hybrid': return 'notes/hybrid-timeline'; - case 'global': return 'notes/global-timeline'; - case 'mentions': return 'notes/mentions'; - case 'tag': return 'notes/search_by_tag'; - } - }, - canFetchMore(): boolean { return !this.moreFetching && !this.fetching && this.existMore; } }, mounted() { + const prepend = note => { + (this.$refs.timeline as any).prepend(note); + }; + if (this.src == 'tag') { + this.endpoint = 'notes/search_by_tag'; + this.query = { + query: this.tagTl.query + }; this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.connection.close(); + }); } else if (this.src == 'home') { + this.endpoint = 'notes/timeline'; + const onChangeFollowing = () => { + this.fetch(); + }; 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('unfollow', this.onChangeFollowing); + this.connection.on('note', prepend); + this.connection.on('follow', onChangeFollowing); + this.connection.on('unfollow', onChangeFollowing); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.connection.off('follow', onChangeFollowing); + this.connection.off('unfollow', onChangeFollowing); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'local') { + this.endpoint = 'notes/local-timeline'; this.streamManager = (this as any).os.streams.localTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'hybrid') { + this.endpoint = 'notes/hybrid-timeline'; this.streamManager = (this as any).os.streams.hybridTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'global') { + this.endpoint = 'notes/global-timeline'; this.streamManager = (this as any).os.streams.globalTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'mentions') { + this.endpoint = 'notes/mentions'; + this.streamManager = (this as any).os.stream; + this.connection = this.streamManager.getConnection(); + this.connectionId = this.streamManager.use(); + this.connection.on('mention', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('mention', prepend); + this.streamManager.dispose(this.connectionId); + }); + } else if (this.src == 'messages') { + this.endpoint = 'notes/mentions'; + this.query = { + visibility: 'specified' + }; + const onNote = note => { + if (note.visibility == 'specified') { + prepend(note); + } + }; this.streamManager = (this as any).os.stream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('mention', this.onNote); + this.connection.on('mention', onNote); + this.$once('beforeDestroy', () => { + this.connection.off('mention', onNote); + this.streamManager.dispose(this.connectionId); + }); } document.addEventListener('keydown', this.onKeydown); @@ -102,28 +158,7 @@ export default Vue.extend({ }, beforeDestroy() { - if (this.src == 'tag') { - 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('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.$emit('beforeDestroy'); document.removeEventListener('keydown', this.onKeydown); }, @@ -132,14 +167,10 @@ export default Vue.extend({ this.fetching = true; (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - (this as any).api(this.endpoint, { + (this as any).api(this.endpoint, Object.assign({ limit: fetchLimit + 1, - untilDate: this.date ? this.date.getTime() : undefined, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl ? this.tagTl.query : undefined - }).then(notes => { + untilDate: this.date ? this.date.getTime() : undefined + }, this.baseQuery, this.query)).then(notes => { if (notes.length == fetchLimit + 1) { notes.pop(); this.existMore = true; @@ -156,14 +187,10 @@ export default Vue.extend({ this.moreFetching = true; - const promise = (this as any).api(this.endpoint, { + const promise = (this as any).api(this.endpoint, Object.assign({ limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl ? this.tagTl.query : undefined - }); + untilId: (this.$refs.timeline as any).tail().id + }, this.baseQuery, this.query)); promise.then(notes => { if (notes.length == fetchLimit + 1) { @@ -178,15 +205,6 @@ export default Vue.extend({ return promise; }, - onNote(note) { - // Prepend a note - (this.$refs.timeline as any).prepend(note); - }, - - onChangeFollowing() { - this.fetch(); - }, - focus() { (this.$refs.timeline as any).focus(); }, diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index 2dc84004df..ccc35f95ff 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -5,10 +5,11 @@ <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</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 == '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> <div class="buttons"> + <button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%</button> + <button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</button> <button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button> <button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button> </div> @@ -18,6 +19,7 @@ <x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> <x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> <x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> + <x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> <x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> </div> @@ -202,6 +204,20 @@ root(isDark) &:active color isDark ? #b2c1d5 : #999 + &[data-active] + color $theme-color + cursor default + + &:before + content "" + display block + position absolute + bottom 0 + left 0 + width 100% + height 2px + background $theme-color + > span display inline-block padding 0 10px diff --git a/src/client/app/desktop/views/pages/deck/deck.column-core.vue b/src/client/app/desktop/views/pages/deck/deck.column-core.vue index a320f697b3..e1490cb0e4 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column-core.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column-core.vue @@ -8,6 +8,7 @@ <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-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/> </template> <script lang="ts"> @@ -16,13 +17,15 @@ import XTlColumn from './deck.tl-column.vue'; import XNotificationsColumn from './deck.notifications-column.vue'; import XWidgetsColumn from './deck.widgets-column.vue'; import XMentionsColumn from './deck.mentions-column.vue'; +import XDirectColumn from './deck.direct-column.vue'; export default Vue.extend({ components: { XTlColumn, XNotificationsColumn, XWidgetsColumn, - XMentionsColumn + XMentionsColumn, + XDirectColumn }, props: { diff --git a/src/client/app/desktop/views/pages/deck/deck.direct-column.vue b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue new file mode 100644 index 0000000000..d5093761f4 --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue @@ -0,0 +1,38 @@ +<template> +<x-column :name="name" :column="column" :is-stacked="isStacked"> + <span slot="header">%fa:envelope R%{{ name }}</span> + + <x-direct/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XColumn from './deck.column.vue'; +import XDirect from './deck.direct.vue'; + +export default Vue.extend({ + components: { + XColumn, + XDirect + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + computed: { + name(): string { + if (this.column.name) return this.column.name; + return '%i18n:common.deck.direct%'; + } + }, +}); +</script> diff --git a/src/client/app/desktop/views/pages/deck/deck.direct.vue b/src/client/app/desktop/views/pages/deck/deck.direct.vue new file mode 100644 index 0000000000..ec9e6b9c3d --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.direct.vue @@ -0,0 +1,97 @@ +<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: { + }, + + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null, + connectionId: null + }; + }, + + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('mention', this.onNote); + + this.fetch(); + }, + + beforeDestroy() { + this.connection.off('mention', this.onNote); + (this as any).os.stream.dispose(this.connectionId); + }, + + methods: { + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('notes/mentions', { + limit: fetchLimit + 1, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes, + visibility: 'specified' + }).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/mentions', { + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes, + visibility: 'specified' + }); + + 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 + if (note.visibility == 'specified') { + (this.$refs.timeline as any).prepend(note); + } + } + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index aafe9a45d3..e5aeba251a 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -148,6 +148,15 @@ export default Vue.extend({ }); } }, { + icon: '%fa:envelope R%', + text: '%i18n:common.deck.direct%', + action: () => { + this.$store.dispatch('settings/addDeckColumn', { + id: uuid(), + type: 'direct' + }); + } + }, { icon: '%fa:list%', text: '%i18n:common.deck.list%', action: () => { diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index fecb2384ba..225abcff6b 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -37,7 +37,14 @@ export default Vue.extend({ connection: null, connectionId: null, unreadCount: 0, - date: null + date: null, + baseQuery: { + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }, + query: {}, + endpoint: null }; }, @@ -46,80 +53,109 @@ export default Vue.extend({ return this.$store.state.i.followingCount == 0; }, - endpoint(): string { - switch (this.src) { - case 'home': return 'notes/timeline'; - case 'local': return 'notes/local-timeline'; - case 'hybrid': return 'notes/hybrid-timeline'; - case 'global': return 'notes/global-timeline'; - case 'mentions': return 'notes/mentions'; - case 'tag': return 'notes/search_by_tag'; - } - }, - canFetchMore(): boolean { return !this.moreFetching && !this.fetching && this.existMore; } }, mounted() { + const prepend = note => { + (this.$refs.timeline as any).prepend(note); + }; + if (this.src == 'tag') { + this.endpoint = 'notes/search_by_tag'; + this.query = { + query: this.tagTl.query + }; this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.connection.close(); + }); } else if (this.src == 'home') { + this.endpoint = 'notes/timeline'; + const onChangeFollowing = () => { + this.fetch(); + }; 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('unfollow', this.onChangeFollowing); + this.connection.on('note', prepend); + this.connection.on('follow', onChangeFollowing); + this.connection.on('unfollow', onChangeFollowing); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.connection.off('follow', onChangeFollowing); + this.connection.off('unfollow', onChangeFollowing); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'local') { + this.endpoint = 'notes/local-timeline'; this.streamManager = (this as any).os.streams.localTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'hybrid') { + this.endpoint = 'notes/hybrid-timeline'; this.streamManager = (this as any).os.streams.hybridTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'global') { + this.endpoint = 'notes/global-timeline'; this.streamManager = (this as any).os.streams.globalTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'mentions') { + this.endpoint = 'notes/mentions'; + this.streamManager = (this as any).os.stream; + this.connection = this.streamManager.getConnection(); + this.connectionId = this.streamManager.use(); + this.connection.on('mention', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('mention', prepend); + this.streamManager.dispose(this.connectionId); + }); + } else if (this.src == 'messages') { + this.endpoint = 'notes/mentions'; + this.query = { + visibility: 'specified' + }; + const onNote = note => { + if (note.visibility == 'specified') { + prepend(note); + } + }; this.streamManager = (this as any).os.stream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('mention', this.onNote); + this.connection.on('mention', onNote); + this.$once('beforeDestroy', () => { + this.connection.off('mention', onNote); + this.streamManager.dispose(this.connectionId); + }); } this.fetch(); }, beforeDestroy() { - if (this.src == 'tag') { - 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('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.$emit('beforeDestroy'); }, methods: { @@ -127,14 +163,10 @@ export default Vue.extend({ this.fetching = true; (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - (this as any).api(this.endpoint, { + (this as any).api(this.endpoint, Object.assign({ limit: fetchLimit + 1, - untilDate: this.date ? this.date.getTime() : undefined, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl ? this.tagTl.query : undefined - }).then(notes => { + untilDate: this.date ? this.date.getTime() : undefined + }, this.baseQuery, this.query)).then(notes => { if (notes.length == fetchLimit + 1) { notes.pop(); this.existMore = true; @@ -151,14 +183,10 @@ export default Vue.extend({ this.moreFetching = true; - const promise = (this as any).api(this.endpoint, { + const promise = (this as any).api(this.endpoint, Object.assign({ limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl ? this.tagTl.query : undefined - }); + untilId: (this.$refs.timeline as any).tail().id + }, this.baseQuery, this.query)); promise.then(notes => { if (notes.length == fetchLimit + 1) { @@ -173,15 +201,6 @@ export default Vue.extend({ return promise; }, - onNote(note) { - // Prepend a note - (this.$refs.timeline as any).prepend(note); - }, - - onChangeFollowing() { - this.fetch(); - }, - focus() { (this.$refs.timeline as any).focus(); }, diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 3ec2f16b75..e61916fe18 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -7,6 +7,7 @@ <span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</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 == 'messages'">%fa:envelope R%%i18n:@messages%</span> <span v-if="src == 'list'">%fa:list%{{ list.title }}</span> <span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span> </span> @@ -23,16 +24,21 @@ <main :data-darkmode="$store.state.device.darkmode"> <div class="nav" v-if="showNav"> <div class="bg" @click="showNav = false"></div> + <div class="pointer"></div> <div class="body"> <div> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</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> + <div class="hr"></div> <span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span> + <span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span> <template v-if="lists"> + <div class="hr"></div> <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> + <div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div> <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> @@ -44,6 +50,7 @@ <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 == 'mentions'" ref="tl" key="mentions" src="mentions"/> + <x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> <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"/> </div> @@ -150,6 +157,26 @@ export default Vue.extend({ root(isDark) > .nav + > .pointer + position fixed + z-index 10002 + top 56px + left 0 + right 0 + + $size = 16px + + &:after + content "" + display block + position absolute + top -($size * 2) + left s('calc(50% - %s)', $size) + border-top solid $size transparent + border-left solid $size transparent + border-right solid $size transparent + border-bottom solid $size isDark ? #272f3a : #fff + > .bg position fixed z-index 10000 @@ -166,28 +193,22 @@ root(isDark) left 0 right 0 width 300px + max-height calc(100% - 70px) margin 0 auto + overflow auto + -webkit-overflow-scrolling touch background isDark ? #272f3a : #fff border-radius 8px box-shadow 0 0 16px rgba(#000, 0.1) - $balloon-size = 16px - - &:after - content "" - display block - position absolute - top -($balloon-size * 2) + 1.5px - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size isDark ? #272f3a : #fff - > div padding 8px 0 - > * + > .hr + margin 8px 0 + border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1) + + > *:not(.hr) display block padding 8px 16px color isDark ? #cdd0d8 : #666 diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 3b2e262e4f..8675a9f562 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -27,6 +27,9 @@ export const meta = { untilId: $.type(ID).optional.note({ }), + + visibility: $.str.optional.note({ + }), } }; @@ -52,6 +55,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = _id: -1 }; + if (ps.visibility) { + query.visibility = ps.visibility; + } + if (ps.following) { const followingIds = await getFriendIds(user._id); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 7daf83b294..7c1e71dcb3 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -142,6 +142,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); } + if (data.visibility == 'specified') { + data.visibleUsers.forEach(u => { + if (!mentionedUsers.some(x => x._id.equals(u._id))) { + mentionedUsers.push(u); + } + }); + } + const note = await insertNote(user, data, tags, mentionedUsers); res(note); @@ -188,7 +196,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const nm = new NotificationManager(user, note); const nmRelatedPromises = []; - createMentionedEvents(mentionedUsers, noteObj, nm); + createMentionedEvents(mentionedUsers, note, nm); const noteActivity = await renderActivity(data, note); @@ -318,7 +326,7 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren if (['public', 'home', 'followers'].includes(note.visibility)) { // フォロワーに配信 - publishToFollowers(note, noteObj, user, noteActivity); + publishToFollowers(note, user, noteActivity); } // リストに配信 @@ -456,7 +464,7 @@ async function publishToUserLists(note: INote, noteObj: any) { }); } -async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteActivity: any) { +async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { const detailPackedNote = await pack(note, null, { detail: true, skipHide: true @@ -505,9 +513,13 @@ function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocal }); } -function createMentionedEvents(mentionedUsers: IUser[], noteObj: any, nm: NotificationManager) { +function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) { mentionedUsers.filter(u => isLocalUser(u)).forEach(async (u) => { - publishUserStream(u._id, 'mention', noteObj); + const detailPackedNote = await pack(note, u, { + detail: true + }); + + publishUserStream(u._id, 'mention', detailPackedNote); // Create notification nm.push(u._id, 'mention'); |