メッセージタイムラインを追加

This commit is contained in:
syuilo 2018-09-18 02:14:12 +09:00
parent dfb102b8c9
commit f4f9bae111
11 changed files with 393 additions and 150 deletions

View file

@ -169,6 +169,7 @@ common:
hashtag: "ハッシュタグ" hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て" mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -916,6 +917,7 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て" mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ" hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加" add-tag-timeline: "ハッシュタグを追加"
@ -1322,6 +1324,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て" mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"

View file

@ -38,7 +38,14 @@ export default Vue.extend({
streamManager: null, streamManager: null,
connection: null, connection: null,
connectionId: 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; 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 { canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore; return !this.moreFetching && !this.fetching && this.existMore;
} }
}, },
mounted() { mounted() {
const prepend = note => {
(this.$refs.timeline as any).prepend(note);
};
if (this.src == 'tag') { 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 = 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') { } else if (this.src == 'home') {
this.endpoint = 'notes/timeline';
const onChangeFollowing = () => {
this.fetch();
};
this.streamManager = (this as any).os.stream; this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); this.connectionId = this.streamManager.use();
this.connection.on('note', this.onNote); this.connection.on('note', prepend);
this.connection.on('follow', this.onChangeFollowing); this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', this.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') { } else if (this.src == 'local') {
this.endpoint = 'notes/local-timeline';
this.streamManager = (this as any).os.streams.localTimelineStream; this.streamManager = (this as any).os.streams.localTimelineStream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); 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') { } else if (this.src == 'hybrid') {
this.endpoint = 'notes/hybrid-timeline';
this.streamManager = (this as any).os.streams.hybridTimelineStream; this.streamManager = (this as any).os.streams.hybridTimelineStream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); 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') { } else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline';
this.streamManager = (this as any).os.streams.globalTimelineStream; this.streamManager = (this as any).os.streams.globalTimelineStream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); 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') { } else if (this.src == 'mentions') {
this.endpoint = 'notes/mentions';
this.streamManager = (this as any).os.stream; this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); this.connectionId = this.streamManager.use();
this.connection.on('mention', this.onNote); 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', onNote);
this.$once('beforeDestroy', () => {
this.connection.off('mention', onNote);
this.streamManager.dispose(this.connectionId);
});
} }
document.addEventListener('keydown', this.onKeydown); document.addEventListener('keydown', this.onKeydown);
@ -102,28 +158,7 @@ export default Vue.extend({
}, },
beforeDestroy() { beforeDestroy() {
if (this.src == 'tag') { this.$emit('beforeDestroy');
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);
}
document.removeEventListener('keydown', this.onKeydown); document.removeEventListener('keydown', this.onKeydown);
}, },
@ -132,14 +167,10 @@ export default Vue.extend({
this.fetching = true; this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => { (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, limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined, untilDate: this.date ? this.date.getTime() : undefined
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query)).then(notes => {
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
query: this.tagTl ? this.tagTl.query : undefined
}).then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
notes.pop(); notes.pop();
this.existMore = true; this.existMore = true;
@ -156,14 +187,10 @@ export default Vue.extend({
this.moreFetching = true; this.moreFetching = true;
const promise = (this as any).api(this.endpoint, { const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id, untilId: (this.$refs.timeline as any).tail().id
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query));
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
query: this.tagTl ? this.tagTl.query : undefined
});
promise.then(notes => { promise.then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
@ -178,15 +205,6 @@ export default Vue.extend({
return promise; return promise;
}, },
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
this.fetch();
},
focus() { focus() {
(this.$refs.timeline as any).focus(); (this.$refs.timeline as any).focus();
}, },

View file

@ -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 == '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 == '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 == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</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>
<div class="buttons"> <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="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button> <button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
</div> </div>
@ -18,6 +19,7 @@
<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 == 'messages'" ref="tl" key="messages" src="messages"/>
<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> <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>
@ -202,6 +204,20 @@ root(isDark)
&:active &:active
color isDark ? #b2c1d5 : #999 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 > span
display inline-block display inline-block
padding 0 10px padding 0 10px

View file

@ -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 == 'list'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'hashtag'" :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"/>
<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -16,13 +17,15 @@ import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-column.vue'; import XNotificationsColumn from './deck.notifications-column.vue';
import XWidgetsColumn from './deck.widgets-column.vue'; import XWidgetsColumn from './deck.widgets-column.vue';
import XMentionsColumn from './deck.mentions-column.vue'; import XMentionsColumn from './deck.mentions-column.vue';
import XDirectColumn from './deck.direct-column.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XTlColumn, XTlColumn,
XNotificationsColumn, XNotificationsColumn,
XWidgetsColumn, XWidgetsColumn,
XMentionsColumn XMentionsColumn,
XDirectColumn
}, },
props: { props: {

View file

@ -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>

View file

@ -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>

View file

@ -147,6 +147,15 @@ export default Vue.extend({
type: 'mentions' type: 'mentions'
}); });
} }
}, {
icon: '%fa:envelope R%',
text: '%i18n:common.deck.direct%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'direct'
});
}
}, { }, {
icon: '%fa:list%', icon: '%fa:list%',
text: '%i18n:common.deck.list%', text: '%i18n:common.deck.list%',

View file

@ -37,7 +37,14 @@ export default Vue.extend({
connection: null, connection: null,
connectionId: null, connectionId: null,
unreadCount: 0, 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; 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 { canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore; return !this.moreFetching && !this.fetching && this.existMore;
} }
}, },
mounted() { mounted() {
const prepend = note => {
(this.$refs.timeline as any).prepend(note);
};
if (this.src == 'tag') { 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 = 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') { } else if (this.src == 'home') {
this.endpoint = 'notes/timeline';
const onChangeFollowing = () => {
this.fetch();
};
this.streamManager = (this as any).os.stream; this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); this.connectionId = this.streamManager.use();
this.connection.on('note', this.onNote); this.connection.on('note', prepend);
this.connection.on('follow', this.onChangeFollowing); this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', this.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') { } else if (this.src == 'local') {
this.endpoint = 'notes/local-timeline';
this.streamManager = (this as any).os.streams.localTimelineStream; this.streamManager = (this as any).os.streams.localTimelineStream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); 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') { } else if (this.src == 'hybrid') {
this.endpoint = 'notes/hybrid-timeline';
this.streamManager = (this as any).os.streams.hybridTimelineStream; this.streamManager = (this as any).os.streams.hybridTimelineStream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); 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') { } else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline';
this.streamManager = (this as any).os.streams.globalTimelineStream; this.streamManager = (this as any).os.streams.globalTimelineStream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); 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') { } else if (this.src == 'mentions') {
this.endpoint = 'notes/mentions';
this.streamManager = (this as any).os.stream; this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection(); this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use(); this.connectionId = this.streamManager.use();
this.connection.on('mention', this.onNote); 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', onNote);
this.$once('beforeDestroy', () => {
this.connection.off('mention', onNote);
this.streamManager.dispose(this.connectionId);
});
} }
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
if (this.src == 'tag') { this.$emit('beforeDestroy');
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);
}
}, },
methods: { methods: {
@ -127,14 +163,10 @@ export default Vue.extend({
this.fetching = true; this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => { (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, limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined, untilDate: this.date ? this.date.getTime() : undefined
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query)).then(notes => {
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
query: this.tagTl ? this.tagTl.query : undefined
}).then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
notes.pop(); notes.pop();
this.existMore = true; this.existMore = true;
@ -151,14 +183,10 @@ export default Vue.extend({
this.moreFetching = true; this.moreFetching = true;
const promise = (this as any).api(this.endpoint, { const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id, untilId: (this.$refs.timeline as any).tail().id
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query));
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
query: this.tagTl ? this.tagTl.query : undefined
});
promise.then(notes => { promise.then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
@ -173,15 +201,6 @@ export default Vue.extend({
return promise; return promise;
}, },
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
this.fetch();
},
focus() { focus() {
(this.$refs.timeline as any).focus(); (this.$refs.timeline as any).focus();
}, },

View file

@ -7,6 +7,7 @@
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> <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 == '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 == 'messages'">%fa:envelope R%%i18n:@messages%</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 v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
</span> </span>
@ -23,16 +24,21 @@
<main :data-darkmode="$store.state.device.darkmode"> <main :data-darkmode="$store.state.device.darkmode">
<div class="nav" v-if="showNav"> <div class="nav" v-if="showNav">
<div class="bg" @click="showNav = false"></div> <div class="bg" @click="showNav = false"></div>
<div class="pointer"></div>
<div class="body"> <div class="body">
<div> <div>
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <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 == '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 == '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>
<div class="hr"></div>
<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 == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
<template v-if="lists"> <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> <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>
<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> <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>
@ -44,6 +50,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 == 'messages'" ref="tl" key="messages" src="messages"/>
<x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> <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>
@ -150,6 +157,26 @@ export default Vue.extend({
root(isDark) root(isDark)
> .nav > .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 > .bg
position fixed position fixed
z-index 10000 z-index 10000
@ -166,28 +193,22 @@ root(isDark)
left 0 left 0
right 0 right 0
width 300px width 300px
max-height calc(100% - 70px)
margin 0 auto margin 0 auto
overflow auto
-webkit-overflow-scrolling touch
background isDark ? #272f3a : #fff background isDark ? #272f3a : #fff
border-radius 8px border-radius 8px
box-shadow 0 0 16px rgba(#000, 0.1) 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 > div
padding 8px 0 padding 8px 0
> * > .hr
margin 8px 0
border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
> *:not(.hr)
display block display block
padding 8px 16px padding 8px 16px
color isDark ? #cdd0d8 : #666 color isDark ? #cdd0d8 : #666

View file

@ -27,6 +27,9 @@ export const meta = {
untilId: $.type(ID).optional.note({ 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 _id: -1
}; };
if (ps.visibility) {
query.visibility = ps.visibility;
}
if (ps.following) { if (ps.following) {
const followingIds = await getFriendIds(user._id); const followingIds = await getFriendIds(user._id);

View file

@ -142,6 +142,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); 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); const note = await insertNote(user, data, tags, mentionedUsers);
res(note); res(note);
@ -188,7 +196,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
const nm = new NotificationManager(user, note); const nm = new NotificationManager(user, note);
const nmRelatedPromises = []; const nmRelatedPromises = [];
createMentionedEvents(mentionedUsers, noteObj, nm); createMentionedEvents(mentionedUsers, note, nm);
const noteActivity = await renderActivity(data, note); 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)) { 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, { const detailPackedNote = await pack(note, null, {
detail: true, detail: true,
skipHide: 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) => { 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 // Create notification
nm.push(u._id, 'mention'); nm.push(u._id, 'mention');