Implement #2736
This commit is contained in:
parent
195f676500
commit
d9f0e158a3
25 changed files with 239 additions and 39 deletions
|
@ -50,6 +50,30 @@ export class HomeStream extends Stream {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.on('unreadMention', () => {
|
||||||
|
os.store.dispatch('mergeMe', {
|
||||||
|
hasUnreadMentions: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on('readAllUnreadMentions', () => {
|
||||||
|
os.store.dispatch('mergeMe', {
|
||||||
|
hasUnreadMentions: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on('unreadSpecifiedNote', () => {
|
||||||
|
os.store.dispatch('mergeMe', {
|
||||||
|
hasUnreadSpecifiedNotes: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on('readAllUnreadSpecifiedNotes', () => {
|
||||||
|
os.store.dispatch('mergeMe', {
|
||||||
|
hasUnreadSpecifiedNotes: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.on('clientSettingUpdated', x => {
|
this.on('clientSettingUpdated', x => {
|
||||||
os.store.commit('settings/set', {
|
os.store.commit('settings/set', {
|
||||||
key: x.key,
|
key: x.key,
|
||||||
|
|
|
@ -213,10 +213,14 @@ export default Vue.extend({
|
||||||
methods: {
|
methods: {
|
||||||
capture(withHandler = false) {
|
capture(withHandler = false) {
|
||||||
if (this.$store.getters.isSignedIn) {
|
if (this.$store.getters.isSignedIn) {
|
||||||
this.connection.send({
|
const data = {
|
||||||
type: 'capture',
|
type: 'capture',
|
||||||
id: this.p.id
|
id: this.p.id
|
||||||
});
|
} as any;
|
||||||
|
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
|
||||||
|
data.read = true;
|
||||||
|
}
|
||||||
|
this.connection.send(data);
|
||||||
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<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 == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></button>
|
||||||
<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</button>
|
<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></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>
|
||||||
|
@ -202,6 +202,13 @@ root(isDark)
|
||||||
line-height 42px
|
line-height 42px
|
||||||
color isDark ? #9baec8 : #ccc
|
color isDark ? #9baec8 : #ccc
|
||||||
|
|
||||||
|
> .badge
|
||||||
|
position absolute
|
||||||
|
top -4px
|
||||||
|
right 4px
|
||||||
|
font-size 10px
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color isDark ? #b2c1d5 : #aaa
|
color isDark ? #b2c1d5 : #aaa
|
||||||
|
|
||||||
|
|
|
@ -147,10 +147,14 @@ export default Vue.extend({
|
||||||
methods: {
|
methods: {
|
||||||
capture(withHandler = false) {
|
capture(withHandler = false) {
|
||||||
if (this.$store.getters.isSignedIn) {
|
if (this.$store.getters.isSignedIn) {
|
||||||
this.connection.send({
|
const data = {
|
||||||
type: 'capture',
|
type: 'capture',
|
||||||
id: this.p.id
|
id: this.p.id
|
||||||
});
|
} as any;
|
||||||
|
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
|
||||||
|
data.read = true;
|
||||||
|
}
|
||||||
|
this.connection.send(data);
|
||||||
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -160,10 +160,14 @@ export default Vue.extend({
|
||||||
methods: {
|
methods: {
|
||||||
capture(withHandler = false) {
|
capture(withHandler = false) {
|
||||||
if (this.$store.getters.isSignedIn) {
|
if (this.$store.getters.isSignedIn) {
|
||||||
this.connection.send({
|
const data = {
|
||||||
type: 'capture',
|
type: 'capture',
|
||||||
id: this.p.id
|
id: this.p.id
|
||||||
});
|
} as any;
|
||||||
|
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
|
||||||
|
data.read = true;
|
||||||
|
}
|
||||||
|
this.connection.send(data);
|
||||||
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -188,9 +188,6 @@ root(isDark)
|
||||||
overflow hidden
|
overflow hidden
|
||||||
text-overflow ellipsis
|
text-overflow ellipsis
|
||||||
|
|
||||||
[data-fa], [data-icon]
|
|
||||||
margin-right 4px
|
|
||||||
|
|
||||||
> img
|
> img
|
||||||
display inline-block
|
display inline-block
|
||||||
vertical-align bottom
|
vertical-align bottom
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
|
<template v-if="folder"><span style="margin-right:4px;">%fa:R folder-open%</span>{{ folder.name }}</template>
|
||||||
<template v-if="file"><mk-file-type-icon data-icon :type="file.type"/>{{ file.name }}</template>
|
<template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
|
||||||
<template v-if="!folder && !file">%fa:cloud%%i18n:@drive%</template>
|
<template v-if="!folder && !file"><span style="margin-right:4px;">%fa:cloud%</span>%i18n:@drive%</template>
|
||||||
</span>
|
</span>
|
||||||
<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
|
<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
|
||||||
<mk-drive
|
<mk-drive
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:star%%i18n:@title%</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:star%</span>%i18n:@title%</span>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<template v-for="favorite in favorites">
|
<template v-for="favorite in favorites">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:gamepad%%i18n:@reversi%</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:gamepad%</span>%i18n:@reversi%</span>
|
||||||
<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
|
<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header" @click="showNav = true">
|
<span slot="header" @click="showNav = true">
|
||||||
<span>
|
<span :class="$style.title">
|
||||||
<span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
|
<span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
|
||||||
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
|
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
|
||||||
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
|
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
<template v-if="!showNav">%fa:angle-down%</template>
|
<template v-if="!showNav">%fa:angle-down%</template>
|
||||||
<template v-else>%fa:angle-up%</template>
|
<template v-else>%fa:angle-up%</template>
|
||||||
</span>
|
</span>
|
||||||
|
<i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template slot="func">
|
<template slot="func">
|
||||||
|
@ -32,8 +33,8 @@
|
||||||
<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>
|
<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%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></span>
|
||||||
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
|
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></span>
|
||||||
<template v-if="lists">
|
<template v-if="lists">
|
||||||
<div class="hr" v-if="lists.length > 0"></div>
|
<div class="hr" v-if="lists.length > 0"></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>
|
||||||
|
@ -220,6 +221,11 @@ root(isDark)
|
||||||
&:not([data-active]):hover
|
&:not([data-active]):hover
|
||||||
background isDark ? #353e4a : #eee
|
background isDark ? #353e4a : #eee
|
||||||
|
|
||||||
|
> .badge
|
||||||
|
margin-left 6px
|
||||||
|
font-size 10px
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
> .tl
|
> .tl
|
||||||
max-width 680px
|
max-width 680px
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
|
@ -238,3 +244,18 @@ main:not([data-darkmode])
|
||||||
root(false)
|
root(false)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="stylus" module>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
.title
|
||||||
|
i
|
||||||
|
margin-right 4px
|
||||||
|
|
||||||
|
.badge
|
||||||
|
margin-left 6px
|
||||||
|
font-size 10px
|
||||||
|
color $theme-color
|
||||||
|
vertical-align middle
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
<template v-if="user">%fa:R comments%{{ user | userName }}</template>
|
<template v-if="user"><span style="margin-right:4px;">%fa:R comments%</span>{{ user | userName }}</template>
|
||||||
<template v-else><mk-ellipsis/></template>
|
<template v-else><mk-ellipsis/></template>
|
||||||
</span>
|
</span>
|
||||||
<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
|
<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:R comments%%i18n:@messaging%</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:R comments%</span>%i18n:@messaging%</span>
|
||||||
<mk-messaging @navigate="navigate" :header-top="48"/>
|
<mk-messaging @navigate="navigate" :header-top="48"/>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:R sticky-note%%i18n:@title%</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:R sticky-note%</span>%i18n:@title%</span>
|
||||||
<main v-if="!fetching">
|
<main v-if="!fetching">
|
||||||
<div>
|
<div>
|
||||||
<mk-note-detail :note="note"/>
|
<mk-note-detail :note="note"/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:R bell%%i18n:@notifications%</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:R bell%</span>%i18n:@notifications%</span>
|
||||||
<template slot="func"><button @click="fn">%fa:check%</button></template>
|
<template slot="func"><button @click="fn">%fa:check%</button></template>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:cog%%i18n:@settings%</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:cog%</span>%i18n:@settings%</span>
|
||||||
<main :data-darkmode="$store.state.device.darkmode">
|
<main :data-darkmode="$store.state.device.darkmode">
|
||||||
<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
|
<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:hashtag%</span>{{ $route.params.tag }}</span>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
|
<p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:home%%i18n:@dashboard%</span>
|
<span slot="header"><span style="margin-right:4px;">%fa:home%</span>%i18n:@dashboard%</span>
|
||||||
<template slot="func">
|
<template slot="func">
|
||||||
<button @click="customizing = !customizing">%fa:cog%</button>
|
<button @click="customizing = !customizing">%fa:cog%</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
17
src/models/note-unread.ts
Normal file
17
src/models/note-unread.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import db from '../db/mongodb';
|
||||||
|
|
||||||
|
const NoteUnread = db.get<INoteUnread>('noteUnreads');
|
||||||
|
NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
|
||||||
|
export default NoteUnread;
|
||||||
|
|
||||||
|
export interface INoteUnread {
|
||||||
|
_id: mongo.ObjectID;
|
||||||
|
noteId: mongo.ObjectID;
|
||||||
|
userId: mongo.ObjectID;
|
||||||
|
isSpecified: boolean;
|
||||||
|
|
||||||
|
_note: {
|
||||||
|
userId: mongo.ObjectID;
|
||||||
|
};
|
||||||
|
}
|
|
@ -295,8 +295,8 @@ export const pack = async (
|
||||||
|
|
||||||
delete _note._user;
|
delete _note._user;
|
||||||
delete _note._reply;
|
delete _note._reply;
|
||||||
delete _note.repost;
|
delete _note._renote;
|
||||||
delete _note.mentions;
|
delete _note._files;
|
||||||
if (_note.geo) delete _note.geo.type;
|
if (_note.geo) delete _note.geo.type;
|
||||||
|
|
||||||
// Populate user
|
// Populate user
|
||||||
|
|
|
@ -348,7 +348,8 @@ export const pack = (
|
||||||
me?: string | mongo.ObjectID | IUser,
|
me?: string | mongo.ObjectID | IUser,
|
||||||
options?: {
|
options?: {
|
||||||
detail?: boolean,
|
detail?: boolean,
|
||||||
includeSecrets?: boolean
|
includeSecrets?: boolean,
|
||||||
|
includeHasUnreadNotes?: boolean
|
||||||
}
|
}
|
||||||
) => new Promise<any>(async (resolve, reject) => {
|
) => new Promise<any>(async (resolve, reject) => {
|
||||||
|
|
||||||
|
@ -510,6 +511,11 @@ export const pack = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!opts.includeHasUnreadNotes) {
|
||||||
|
delete _user.hasUnreadSpecifiedNotes;
|
||||||
|
delete _user.hasUnreadMentions;
|
||||||
|
}
|
||||||
|
|
||||||
// resolve promises in _user object
|
// resolve promises in _user object
|
||||||
_user = await rap(_user);
|
_user = await rap(_user);
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
|
||||||
// Serialize
|
// Serialize
|
||||||
res(await pack(user, user, {
|
res(await pack(user, user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
|
includeHasUnreadNotes: true,
|
||||||
includeSecrets: isSecure
|
includeSecrets: isSecure
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import readNotification from '../common/read-notification';
|
||||||
import call from '../call';
|
import call from '../call';
|
||||||
import { IApp } from '../../../models/app';
|
import { IApp } from '../../../models/app';
|
||||||
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||||
|
import readNote from '../../../services/note/read';
|
||||||
|
|
||||||
const log = debug('misskey');
|
const log = debug('misskey');
|
||||||
|
|
||||||
|
@ -94,6 +95,9 @@ export default async function(
|
||||||
if (!msg.id) return;
|
if (!msg.id) return;
|
||||||
log(`CAPTURE: ${msg.id} by @${user.username}`);
|
log(`CAPTURE: ${msg.id} by @${user.username}`);
|
||||||
subscriber.on(`note-stream:${msg.id}`, onNoteStream);
|
subscriber.on(`note-stream:${msg.id}`, onNoteStream);
|
||||||
|
if (msg.read) {
|
||||||
|
readNote(user._id, msg.id);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'decapture':
|
case 'decapture':
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { TextElementMention } from '../../mfm/parse/elements/mention';
|
||||||
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
|
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
|
||||||
import { updateNoteStats } from '../update-chart';
|
import { updateNoteStats } from '../update-chart';
|
||||||
import { erase, unique } from '../../prelude/array';
|
import { erase, unique } from '../../prelude/array';
|
||||||
|
import insertNoteUnread from './unread';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -170,6 +171,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
||||||
// Increment notes count (user)
|
// Increment notes count (user)
|
||||||
incNotesCountOfUser(user);
|
incNotesCountOfUser(user);
|
||||||
|
|
||||||
|
// 未読通知を作成
|
||||||
|
if (data.visibility == 'specified') {
|
||||||
|
data.visibleUsers.forEach(u => {
|
||||||
|
insertNoteUnread(u, note, true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mentionedUsers.forEach(u => {
|
||||||
|
insertNoteUnread(u, note, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
saveReply(data.reply, note);
|
saveReply(data.reply, note);
|
||||||
}
|
}
|
||||||
|
@ -314,16 +326,6 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
|
||||||
publishGlobalTimelineStream(noteObj);
|
publishGlobalTimelineStream(noteObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.visibility == 'specified') {
|
|
||||||
visibleUsers.forEach(async (u) => {
|
|
||||||
const n = await pack(note, u, {
|
|
||||||
detail: true
|
|
||||||
});
|
|
||||||
publishUserStream(u._id, 'note', n);
|
|
||||||
publishHybridTimelineStream(u._id, n);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
||||||
// フォロワーに配信
|
// フォロワーに配信
|
||||||
publishToFollowers(note, user, noteActivity);
|
publishToFollowers(note, user, noteActivity);
|
||||||
|
|
62
src/services/note/read.ts
Normal file
62
src/services/note/read.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import { publishUserStream } from '../../stream';
|
||||||
|
import User from '../../models/user';
|
||||||
|
import NoteUnread from '../../models/note-unread';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a note as read
|
||||||
|
*/
|
||||||
|
export default (
|
||||||
|
user: string | mongo.ObjectID,
|
||||||
|
note: string | mongo.ObjectID
|
||||||
|
) => new Promise<any>(async (resolve, reject) => {
|
||||||
|
|
||||||
|
const userId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(user)
|
||||||
|
? user as mongo.ObjectID
|
||||||
|
: new mongo.ObjectID(user);
|
||||||
|
|
||||||
|
const noteId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(note)
|
||||||
|
? note as mongo.ObjectID
|
||||||
|
: new mongo.ObjectID(note);
|
||||||
|
|
||||||
|
// Remove document
|
||||||
|
await NoteUnread.remove({
|
||||||
|
userId: userId,
|
||||||
|
noteId: noteId
|
||||||
|
});
|
||||||
|
|
||||||
|
const count1 = await NoteUnread
|
||||||
|
.count({
|
||||||
|
userId: userId,
|
||||||
|
isSpecified: false
|
||||||
|
}, {
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const count2 = await NoteUnread
|
||||||
|
.count({
|
||||||
|
userId: userId,
|
||||||
|
isSpecified: true
|
||||||
|
}, {
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count1 == 0 || count2 == 0) {
|
||||||
|
User.update({ _id: userId }, {
|
||||||
|
$set: {
|
||||||
|
hasUnreadMentions: count1 != 0 || count2 != 0,
|
||||||
|
hasUnreadSpecifiedNotes: count2 != 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count1 == 0) {
|
||||||
|
// 全て既読になったイベントを発行
|
||||||
|
publishUserStream(userId, 'readAllUnreadMentions');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count2 == 0) {
|
||||||
|
// 全て既読になったイベントを発行
|
||||||
|
publishUserStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||||
|
}
|
||||||
|
});
|
47
src/services/note/unread.ts
Normal file
47
src/services/note/unread.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import NoteUnread from '../../models/note-unread';
|
||||||
|
import User, { IUser } from '../../models/user';
|
||||||
|
import { INote } from '../../models/note';
|
||||||
|
import Mute from '../../models/mute';
|
||||||
|
import { publishUserStream } from '../../stream';
|
||||||
|
|
||||||
|
export default async function(user: IUser, note: INote, isSpecified = false) {
|
||||||
|
const unread = await NoteUnread.insert({
|
||||||
|
noteId: note._id,
|
||||||
|
userId: user._id,
|
||||||
|
isSpecified,
|
||||||
|
_note: {
|
||||||
|
userId: note.userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
||||||
|
setTimeout(async () => {
|
||||||
|
const exist = await NoteUnread.findOne({ _id: unread._id });
|
||||||
|
if (exist == null) return;
|
||||||
|
|
||||||
|
//#region ただしミュートされているなら発行しない
|
||||||
|
const mute = await Mute.find({
|
||||||
|
muterId: user._id
|
||||||
|
});
|
||||||
|
const mutedUserIds = mute.map(m => m.muteeId.toString());
|
||||||
|
if (mutedUserIds.includes(note.userId.toString())) return;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
User.update({
|
||||||
|
_id: user._id
|
||||||
|
}, {
|
||||||
|
$set: isSpecified ? {
|
||||||
|
hasUnreadSpecifiedNotes: true,
|
||||||
|
hasUnreadMentions: true
|
||||||
|
} : {
|
||||||
|
hasUnreadMentions: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
publishUserStream(user._id, 'unreadMention', note._id);
|
||||||
|
|
||||||
|
if (isSpecified) {
|
||||||
|
publishUserStream(user._id, 'unreadSpecifiedNote', note._id);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
Loading…
Reference in a new issue