diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts index 6fed267d6e..463f763888 100644 --- a/src/client/app/common/mios.ts +++ b/src/client/app/common/mios.ts @@ -21,7 +21,9 @@ const defaultSettings = { showMaps: true, showPostFormOnTopOfTl: false, gradientWindowHeader: false, - showReplyTarget: true + showReplyTarget: true, + showMyRenotes: true, + showRenotedMyNotes: true }; //#region api requests diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index adfe43bb64..b5111dabc9 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -45,6 +45,8 @@ + + 位置情報が添付された投稿のマップを自動的に展開します。 @@ -319,6 +321,18 @@ export default Vue.extend({ value: v }); }, + onChangeShowMyRenotes(v) { + (this as any).api('i/update_client_setting', { + name: 'showMyRenotes', + value: v + }); + }, + onChangeShowRenotedMyNotes(v) { + (this as any).api('i/update_client_setting', { + name: 'showRenotedMyNotes', + value: v + }); + }, onChangeShowMaps(v) { (this as any).api('i/update_client_setting', { name: 'showMaps', diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index 1e98f087e1..f66ae57885 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -90,7 +90,9 @@ export default Vue.extend({ (this as any).api(this.endpoint, { limit: 11, - untilDate: this.date ? this.date.getTime() : undefined + untilDate: this.date ? this.date.getTime() : undefined, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes }).then(notes => { if (notes.length == 11) { notes.pop(); @@ -108,7 +110,9 @@ export default Vue.extend({ this.moreFetching = true; (this as any).api(this.endpoint, { limit: 11, - untilId: this.notes[this.notes.length - 1].id + untilId: this.notes[this.notes.length - 1].id, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes }).then(notes => { if (notes.length == 11) { notes.pop(); @@ -121,6 +125,21 @@ export default Vue.extend({ }, onNote(note) { + const isMyNote = note.userId == (this as any).os.i.id; + const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + + if ((this as any).os.i.clientSettings.showMyRenotes === false) { + if (isMyNote && isPureRenote) { + return; + } + } + + if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) { + if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) { + return; + } + } + // サウンドを再生する if ((this as any).os.isEnableSounds) { const sound = new Audio(`${url}/assets/post.mp3`); diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue index 11b82aa456..a6227996b8 100644 --- a/src/client/app/mobile/views/components/timeline.vue +++ b/src/client/app/mobile/views/components/timeline.vue @@ -30,6 +30,7 @@ export default Vue.extend({ default: null } }, + data() { return { fetching: true, @@ -40,11 +41,13 @@ export default Vue.extend({ connectionId: null }; }, + computed: { alone(): boolean { return (this as any).os.i.followingCount == 0; } }, + mounted() { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); @@ -53,20 +56,24 @@ export default Vue.extend({ this.connection.on('follow', this.onChangeFollowing); this.connection.on('unfollow', this.onChangeFollowing); -this.fetch(); + this.fetch(); }, + beforeDestroy() { this.connection.off('note', this.onNote); this.connection.off('follow', this.onChangeFollowing); this.connection.off('unfollow', this.onChangeFollowing); (this as any).os.stream.dispose(this.connectionId); }, + methods: { fetch(cb?) { this.fetching = true; (this as any).api('notes/timeline', { limit: limit + 1, - untilDate: this.date ? (this.date as any).getTime() : undefined + untilDate: this.date ? (this.date as any).getTime() : undefined, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes }).then(notes => { if (notes.length == limit + 1) { notes.pop(); @@ -78,11 +85,14 @@ this.fetch(); if (cb) cb(); }); }, + more() { this.moreFetching = true; (this as any).api('notes/timeline', { limit: limit + 1, - untilId: this.notes[this.notes.length - 1].id + untilId: this.notes[this.notes.length - 1].id, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes }).then(notes => { if (notes.length == limit + 1) { notes.pop(); @@ -94,12 +104,29 @@ this.fetch(); this.moreFetching = false; }); }, + onNote(note) { + const isMyNote = note.userId == (this as any).os.i.id; + const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + + if ((this as any).os.i.clientSettings.showMyRenotes === false) { + if (isMyNote && isPureRenote) { + return; + } + } + + if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) { + if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) { + return; + } + } + this.notes.unshift(note); const isTop = window.scrollY > 8; if (isTop) this.notes.pop(); }, + onChangeFollowing() { this.fetch(); } diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index cb14fa6eb0..de30afea57 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -37,6 +37,14 @@ module.exports = async (params, user, app) => { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } + // Get 'includeMyRenotes' parameter + const [includeMyRenotes = true, includeMyRenotesErr] = $(params.includeMyRenotes).optional.boolean().$; + if (includeMyRenotesErr) throw 'invalid includeMyRenotes param'; + + // Get 'includeRenotedMyNotes' parameter + const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $(params.includeRenotedMyNotes).optional.boolean().$; + if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param'; + const [followings, watchingChannelIds, mutedUserIds] = await Promise.all([ // フォローを取得 // Fetch following @@ -84,38 +92,76 @@ module.exports = async (params, user, app) => { }); const query = { - $or: [{ - $and: [{ - // フォローしている人のタイムラインへの投稿 - $or: followQuery - }, { - // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る - $or: [{ - channelId: { - $exists: false - } + $and: [{ + $or: [{ + $and: [{ + // フォローしている人のタイムラインへの投稿 + $or: followQuery }, { - channelId: null + // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る + $or: [{ + channelId: { + $exists: false + } + }, { + channelId: null + }] }] - }] - }, { - // Watchしているチャンネルへの投稿 - channelId: { - $in: watchingChannelIds - } - }], - // mute - userId: { - $nin: mutedUserIds - }, - '_reply.userId': { - $nin: mutedUserIds - }, - '_renote.userId': { - $nin: mutedUserIds - }, + }, { + // Watchしているチャンネルへの投稿 + channelId: { + $in: watchingChannelIds + } + }], + // mute + userId: { + $nin: mutedUserIds + }, + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + }, + }] } as any; + // MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。 + // つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。 + // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws + + if (includeMyRenotes === false) { + query.$and.push({ + $or: [{ + userId: { $ne: user._id } + }, { + renoteId: null + }, { + text: { $ne: null } + }, { + mediaIds: { $ne: [] } + }, { + poll: { $ne: null } + }] + }); + } + + if (includeRenotedMyNotes === false) { + query.$and.push({ + $or: [{ + '_renote.userId': { $ne: user._id } + }, { + renoteId: null + }, { + text: { $ne: null } + }, { + mediaIds: { $ne: [] } + }, { + poll: { $ne: null } + }] + }); + } + if (sinceId) { sort._id = 1; query._id = {