summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2019-02-17 23:41:47 +0900
committerGitHub <noreply@github.com>2019-02-17 23:41:47 +0900
commit1d5a54ff6f74569fa89c4083301d9b01eb80ad29 (patch)
tree0695f8ee6942b49236b880bb228be45b26282e90 /src
parentFix #4300 (#4304) (diff)
downloadsharkey-1d5a54ff6f74569fa89c4083301d9b01eb80ad29.tar.gz
sharkey-1d5a54ff6f74569fa89c4083301d9b01eb80ad29.tar.bz2
sharkey-1d5a54ff6f74569fa89c4083301d9b01eb80ad29.zip
ハッシュタグでユーザー検索できるように (#4298)
* ハッシュタグでユーザー検索できるように * :art: * Increase limit * リモートユーザーも表示 * Fix bug * Fix bug * Improve performance
Diffstat (limited to 'src')
-rw-r--r--src/client/app/common/views/components/mfm.ts8
-rw-r--r--src/client/app/common/views/components/user-list.vue2
-rw-r--r--src/client/app/common/views/pages/explore.vue86
-rw-r--r--src/client/app/common/views/pages/follow.vue2
-rw-r--r--src/client/app/desktop/script.ts2
-rw-r--r--src/client/app/desktop/views/components/user-card.vue2
-rw-r--r--src/client/app/desktop/views/deck/deck.user-column.vue2
-rw-r--r--src/client/app/desktop/views/home/user/user.header.vue2
-rw-r--r--src/client/app/mobile/script.ts1
-rw-r--r--src/client/app/mobile/views/components/user-preview.vue2
-rw-r--r--src/client/app/mobile/views/pages/user/index.vue2
-rw-r--r--src/models/hashtag.ts34
-rw-r--r--src/models/user.ts1
-rw-r--r--src/remote/activitypub/models/person.ts9
-rw-r--r--src/server/api/endpoints/hashtags/list.ts55
-rw-r--r--src/server/api/endpoints/hashtags/users.ts83
-rw-r--r--src/server/api/endpoints/i/update.ts5
-rw-r--r--src/services/note/create.ts4
-rw-r--r--src/services/register-hashtag.ts31
-rw-r--r--src/services/update-hashtag.ts86
20 files changed, 363 insertions, 56 deletions
diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/app/common/views/components/mfm.ts
index e322c53a38..78734200a7 100644
--- a/src/client/app/common/views/components/mfm.ts
+++ b/src/client/app/common/views/components/mfm.ts
@@ -40,7 +40,11 @@ export default Vue.component('misskey-flavored-markdown', {
},
customEmojis: {
required: false,
- }
+ },
+ isNote: {
+ type: Boolean,
+ default: true
+ },
},
render(createElement) {
@@ -204,7 +208,7 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement('router-link', {
key: Math.random(),
attrs: {
- to: `/tags/${encodeURIComponent(token.node.props.hashtag)}`,
+ to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
style: 'color:var(--mfmHashtag);'
}
}, `#${token.node.props.hashtag}`)];
diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue
index 8541e85433..ee44eac860 100644
--- a/src/client/app/common/views/components/user-list.vue
+++ b/src/client/app/common/views/components/user-list.vue
@@ -13,7 +13,7 @@
<p class="username">@{{ user | acct }}</p>
</div>
<div class="description" v-if="user.description" :title="user.description">
- <mfm :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false"/>
+ <mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false"/>
</div>
</div>
</div>
diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue
index 79fa26b70f..2d273d3fd2 100644
--- a/src/client/app/common/views/pages/explore.vue
+++ b/src/client/app/common/views/pages/explore.vue
@@ -1,29 +1,53 @@
<template>
<div>
- <mk-user-list :make-promise="verifiedUsers">
- <span><fa :icon="faBookmark"/> {{ $t('verified-users') }}</span>
+ <mk-user-list v-if="tag != null" :make-promise="tagUsers" :key="tag">
+ <fa :icon="faHashtag" fixed-width/>{{ tag }}
</mk-user-list>
- <mk-user-list :make-promise="popularUsers">
- <span><fa :icon="faChartLine"/> {{ $t('popular-users') }}</span>
- </mk-user-list>
- <mk-user-list :make-promise="recentlyUpdatedUsers">
- <span><fa :icon="faCommentAlt"/> {{ $t('recently-updated-users') }}</span>
- </mk-user-list>
- <mk-user-list :make-promise="recentlyRegisteredUsers">
- <span><fa :icon="faPlus"/> {{ $t('recently-registered-users') }}</span>
+ <mk-user-list v-if="tag != null" :make-promise="tagRemoteUsers" :key="tag">
+ <fa :icon="faHashtag" fixed-width/>{{ tag }} ({{ $t('federated') }})
</mk-user-list>
+
+ <ui-container :body-togglable="true">
+ <template slot="header"><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template>
+
+ <div class="vxjfqztj">
+ <router-link v-for="tag in tags" :to="`/explore/tags/${tag.tag}`" :key="tag.tag">{{ tag.tag }}</router-link>
+ </div>
+ </ui-container>
+
+ <template v-if="tag == null">
+ <mk-user-list :make-promise="verifiedUsers">
+ <fa :icon="faBookmark" fixed-width/>{{ $t('verified-users') }}
+ </mk-user-list>
+ <mk-user-list :make-promise="popularUsers">
+ <fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }}
+ </mk-user-list>
+ <mk-user-list :make-promise="recentlyUpdatedUsers">
+ <fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }}
+ </mk-user-list>
+ <mk-user-list :make-promise="recentlyRegisteredUsers">
+ <fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }}
+ </mk-user-list>
+ </template>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
-import { faChartLine, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { faChartLine, faPlus, faHashtag } from '@fortawesome/free-solid-svg-icons';
import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/pages/explore.vue'),
+ props: {
+ tag: {
+ type: String,
+ required: false
+ }
+ },
+
data() {
return {
verifiedUsers: () => this.$root.api('users', {
@@ -49,11 +73,49 @@ export default Vue.extend({
sort: '+createdAt',
limit: 10
}),
- faBookmark, faChartLine, faCommentAlt, faPlus
+ tags: [],
+ faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag
};
},
+
+ computed: {
+ tagUsers(): () => Promise<any> {
+ return () => this.$root.api('hashtags/users', {
+ tag: this.tag,
+ state: 'alive',
+ origin: 'local',
+ sort: '+follower',
+ limit: 30
+ });
+ },
+
+ tagRemoteUsers(): () => Promise<any> {
+ return () => this.$root.api('hashtags/users', {
+ tag: this.tag,
+ state: 'alive',
+ origin: 'remote',
+ sort: '+follower',
+ limit: 30
+ });
+ },
+ },
+
+ created() {
+ this.$root.api('hashtags/list', {
+ sort: '+attachedLocalUsers',
+ limit: 30
+ }).then(tags => {
+ this.tags = tags;
+ });
+ }
});
</script>
<style lang="stylus" scoped>
+.vxjfqztj
+ padding 16px
+
+ > *
+ margin-right 16px
+
</style>
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
index 4d1febaec0..f8d12a2dca 100644
--- a/src/client/app/common/views/pages/follow.vue
+++ b/src/client/app/common/views/pages/follow.vue
@@ -12,7 +12,7 @@
</router-link>
<span class="username">@{{ user | acct }}</span>
<div class="description">
- <mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
</div>
</main>
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index c66171e3af..8c8e3c3fbf 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -146,6 +146,7 @@ init(async (launch, os) => {
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/deck/deck.hashtag-column.vue').then(m => m.default) },
{ path: '/featured', component: () => import('./views/deck/deck.featured-column.vue').then(m => m.default) },
{ path: '/explore', component: () => import('./views/deck/deck.explore-column.vue').then(m => m.default) },
+ { path: '/explore/tags/:tag', props: true, component: () => import('./views/deck/deck.explore-column.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('./views/deck/deck.favorites-column.vue').then(m => m.default) }
]}
: { path: '/', component: MkHome, children: [
@@ -160,6 +161,7 @@ init(async (launch, os) => {
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) },
{ path: '/featured', name: 'featured', component: () => import('./views/home/featured.vue').then(m => m.default) },
{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
+ { path: '/explore/tags/:tag', name: 'explore', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
]},
{ path: '/i/messaging/:user', component: MkMessagingRoom },
diff --git a/src/client/app/desktop/views/components/user-card.vue b/src/client/app/desktop/views/components/user-card.vue
index 21a4ab9f6c..61b3be9305 100644
--- a/src/client/app/desktop/views/components/user-card.vue
+++ b/src/client/app/desktop/views/components/user-card.vue
@@ -10,7 +10,7 @@
<span class="username">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span>
<div class="description">
- <mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
</div>
</div>
diff --git a/src/client/app/desktop/views/deck/deck.user-column.vue b/src/client/app/desktop/views/deck/deck.user-column.vue
index d6618c5716..813667f6aa 100644
--- a/src/client/app/desktop/views/deck/deck.user-column.vue
+++ b/src/client/app/desktop/views/deck/deck.user-column.vue
@@ -25,7 +25,7 @@
</header>
<div class="info">
<div class="description">
- <mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
<div class="fields" v-if="user.fields">
<dl class="field" v-for="(field, i) in user.fields" :key="i">
diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue
index debfb24393..1219a07916 100644
--- a/src/client/app/desktop/views/home/user/user.header.vue
+++ b/src/client/app/desktop/views/home/user/user.header.vue
@@ -23,7 +23,7 @@
<ui-button @click="menu" ref="menu" :inline="true"><fa icon="ellipsis-h"/></ui-button>
</div>
<div class="description">
- <mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
<div class="fields" v-if="user.fields">
<dl class="field" v-for="(field, i) in user.fields" :key="i">
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index dbdc0f630c..9bec577d7b 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -133,6 +133,7 @@ init((launch) => {
{ path: '/tags/:tag', component: MkTag },
{ path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) },
{ path: '/explore', name: 'explore', component: () => import('./views/pages/explore.vue').then(m => m.default) },
+ { path: '/explore/tags/:tag', name: 'explore', props: true, component: () => import('./views/pages/explore.vue').then(m => m.default) },
{ path: '/share', component: MkShare },
{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi },
{ path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index b40e6f7619..ea8bbe242f 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -10,7 +10,7 @@
</header>
<div class="body">
<div class="description">
- <mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
</div>
</div>
diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue
index 48b65624ef..d7fb3d4d58 100644
--- a/src/client/app/mobile/views/pages/user/index.vue
+++ b/src/client/app/mobile/views/pages/user/index.vue
@@ -22,7 +22,7 @@
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
</div>
<div class="description">
- <mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
<div class="fields" v-if="user.fields">
<dl class="field" v-for="(field, i) in user.fields" :key="i">
diff --git a/src/models/hashtag.ts b/src/models/hashtag.ts
index f5b6156055..742e4a254c 100644
--- a/src/models/hashtag.ts
+++ b/src/models/hashtag.ts
@@ -3,11 +3,41 @@ import db from '../db/mongodb';
const Hashtag = db.get<IHashtags>('hashtags');
Hashtag.createIndex('tag', { unique: true });
-Hashtag.createIndex('mentionedUserIdsCount');
+Hashtag.createIndex('mentionedUsersCount');
+Hashtag.createIndex('mentionedLocalUsersCount');
+Hashtag.createIndex('attachedUsersCount');
+Hashtag.createIndex('attachedLocalUsersCount');
export default Hashtag;
+// 後方互換性のため
+Hashtag.findOne({ attachedUserIds: { $exists: false }}).then(h => {
+ if (h != null) {
+ Hashtag.update({}, {
+ $rename: {
+ mentionedUserIdsCount: 'mentionedUsersCount'
+ },
+ $set: {
+ mentionedLocalUserIds: [],
+ mentionedLocalUsersCount: 0,
+ attachedUserIds: [],
+ attachedUsersCount: 0,
+ attachedLocalUserIds: [],
+ attachedLocalUsersCount: 0,
+ }
+ }, {
+ multi: true
+ });
+ }
+});
+
export interface IHashtags {
tag: string;
mentionedUserIds: mongo.ObjectID[];
- mentionedUserIdsCount: number;
+ mentionedUsersCount: number;
+ mentionedLocalUserIds: mongo.ObjectID[];
+ mentionedLocalUsersCount: number;
+ attachedUserIds: mongo.ObjectID[];
+ attachedUsersCount: number;
+ attachedLocalUserIds: mongo.ObjectID[];
+ attachedLocalUsersCount: number;
}
diff --git a/src/models/user.ts b/src/models/user.ts
index 6cc44f371d..2549b2568a 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -18,6 +18,7 @@ const User = db.get<IUser>('users');
User.createIndex('createdAt');
User.createIndex('updatedAt');
User.createIndex('followersCount');
+User.createIndex('tags');
User.createIndex('username');
User.createIndex('usernameLower');
User.createIndex('host');
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index c90df16906..9a38bbf144 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -23,6 +23,7 @@ import Following from '../../../models/following';
import { IIdentifier } from './identifier';
import { apLogger } from '../logger';
import { INote } from '../../../models/note';
+import { updateHashtag } from '../../../services/update-hashtag';
const logger = apLogger;
/**
@@ -210,6 +211,10 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
usersChart.update(user, true);
//#endregion
+ // ハッシュタグ登録
+ for (const tag of tags) updateHashtag(user, tag, true, true);
+ for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
+
//#region アイコンとヘッダー画像をフェッチ
const [avatar, banner] = (await Promise.all<IDriveFile>([
person.icon,
@@ -383,6 +388,10 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
$set: updates
});
+ // ハッシュタグ更新
+ for (const tag of tags) updateHashtag(exist, tag, true, true);
+ for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false);
+
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await Following.update({
followerId: exist._id
diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts
new file mode 100644
index 0000000000..5c37dbd6b5
--- /dev/null
+++ b/src/server/api/endpoints/hashtags/list.ts
@@ -0,0 +1,55 @@
+import $ from 'cafy';
+import define from '../../define';
+import Hashtag from '../../../../models/hashtag';
+
+export const meta = {
+ requireCredential: false,
+
+ params: {
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10
+ },
+
+ sort: {
+ validator: $.str.or([
+ '+mentionedUsers',
+ '-mentionedUsers',
+ '+mentionedLocalUsers',
+ '-mentionedLocalUsers',
+ '+attachedUsers',
+ '-attachedUsers',
+ '+attachedLocalUsers',
+ '-attachedLocalUsers',
+ ]),
+ },
+ }
+};
+
+const sort: any = {
+ '+mentionedUsers': { mentionedUsersCount: -1 },
+ '-mentionedUsers': { mentionedUsersCount: 1 },
+ '+mentionedLocalUsers': { mentionedLocalUsersCount: -1 },
+ '-mentionedLocalUsers': { mentionedLocalUsersCount: 1 },
+ '+attachedUsers': { attachedUsersCount: -1 },
+ '-attachedUsers': { attachedUsersCount: 1 },
+ '+attachedLocalUsers': { attachedLocalUsersCount: -1 },
+ '-attachedLocalUsers': { attachedLocalUsersCount: 1 },
+};
+
+export default define(meta, (ps, me) => new Promise(async (res, rej) => {
+ const tags = await Hashtag
+ .find({}, {
+ limit: ps.limit,
+ sort: sort[ps.sort],
+ fields: {
+ tag: true,
+ mentionedUsersCount: true,
+ mentionedLocalUsersCount: true,
+ attachedUsersCount: true,
+ attachedLocalUsersCount: true
+ }
+ });
+
+ res(tags);
+}));
diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts
new file mode 100644
index 0000000000..be6b53b889
--- /dev/null
+++ b/src/server/api/endpoints/hashtags/users.ts
@@ -0,0 +1,83 @@
+import $ from 'cafy';
+import User, { pack } from '../../../../models/user';
+import define from '../../define';
+
+export const meta = {
+ requireCredential: false,
+
+ params: {
+ tag: {
+ validator: $.str,
+ },
+
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10
+ },
+
+ sort: {
+ validator: $.str.or([
+ '+follower',
+ '-follower',
+ '+createdAt',
+ '-createdAt',
+ '+updatedAt',
+ '-updatedAt',
+ ]),
+ },
+
+ state: {
+ validator: $.optional.str.or([
+ 'all',
+ 'alive'
+ ]),
+ default: 'all'
+ },
+
+ origin: {
+ validator: $.optional.str.or([
+ 'combined',
+ 'local',
+ 'remote',
+ ]),
+ default: 'local'
+ }
+ }
+};
+
+const sort: any = {
+ '+follower': { followersCount: -1 },
+ '-follower': { followersCount: 1 },
+ '+createdAt': { createdAt: -1 },
+ '-createdAt': { createdAt: 1 },
+ '+updatedAt': { updatedAt: -1 },
+ '-updatedAt': { updatedAt: 1 },
+};
+
+export default define(meta, (ps, me) => new Promise(async (res, rej) => {
+ const q = {
+ tags: ps.tag,
+ $and: []
+ } as any;
+
+ // state
+ q.$and.push(
+ ps.state == 'alive' ? { updatedAt: { $gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)) } } :
+ {}
+ );
+
+ // origin
+ q.$and.push(
+ ps.origin == 'local' ? { host: null } :
+ ps.origin == 'remote' ? { host: { $ne: null } } :
+ {}
+ );
+
+ const users = await User
+ .find(q, {
+ limit: ps.limit,
+ sort: sort[ps.sort],
+ });
+
+ res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
+}));
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 6ae63c52db..b3ec53223f 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -11,6 +11,7 @@ import { parse, parsePlain } from '../../../../mfm/parse';
import extractEmojis from '../../../../misc/extract-emojis';
import extractHashtags from '../../../../misc/extract-hashtags';
import * as langmap from 'langmap';
+import { updateHashtag } from '../../../../services/update-hashtag';
export const meta = {
desc: {
@@ -221,6 +222,10 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
updates.emojis = emojis;
updates.tags = tags;
+
+ // ハッシュタグ更新
+ for (const tag of tags) updateHashtag(user, tag, true, true);
+ for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
}
//#endregion
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 126d698b08..c94686dcc0 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -19,7 +19,7 @@ import UserList from '../../models/user-list';
import resolveUser from '../../remote/resolve-user';
import Meta from '../../models/meta';
import config from '../../config';
-import registerHashtag from '../register-hashtag';
+import { updateHashtag } from '../update-hashtag';
import isQuote from '../../misc/is-quote';
import notesChart from '../../services/chart/notes';
import perUserNotesChart from '../../services/chart/per-user-notes';
@@ -235,7 +235,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
}
// ハッシュタグ登録
- for (const tag of tags) registerHashtag(user, tag);
+ for (const tag of tags) updateHashtag(user, tag);
// ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティにこの投稿を追加
if (data.files) {
diff --git a/src/services/register-hashtag.ts b/src/services/register-hashtag.ts
deleted file mode 100644
index 01b7bc871a..0000000000
--- a/src/services/register-hashtag.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { IUser } from '../models/user';
-import Hashtag from '../models/hashtag';
-import hashtagChart from '../services/chart/hashtag';
-
-export default async function(user: IUser, tag: string) {
- tag = tag.toLowerCase();
-
- const index = await Hashtag.findOne({ tag });
-
- if (index != null) {
- // 自分が初めてこのタグを使ったなら
- if (!index.mentionedUserIds.some(id => id.equals(user._id))) {
- Hashtag.update({ tag }, {
- $push: {
- mentionedUserIds: user._id
- },
- $inc: {
- mentionedUserIdsCount: 1
- }
- });
- }
- } else {
- Hashtag.insert({
- tag,
- mentionedUserIds: [user._id],
- mentionedUserIdsCount: 1
- });
- }
-
- hashtagChart.update(tag, user);
-}
diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts
new file mode 100644
index 0000000000..e5de7c10c6
--- /dev/null
+++ b/src/services/update-hashtag.ts
@@ -0,0 +1,86 @@
+import { IUser, isLocalUser } from '../models/user';
+import Hashtag from '../models/hashtag';
+import hashtagChart from './chart/hashtag';
+
+export async function updateHashtag(user: IUser, tag: string, isUserAttached = false, inc = true) {
+ tag = tag.toLowerCase();
+
+ const index = await Hashtag.findOne({ tag });
+
+ if (index == null && !inc) return;
+
+ if (index != null) {
+ const $push = {} as any;
+ const $pull = {} as any;
+ const $inc = {} as any;
+
+ if (isUserAttached) {
+ if (inc) {
+ // 自分が初めてこのタグを使ったなら
+ if (!index.attachedUserIds.some(id => id.equals(user._id))) {
+ $push.attachedUserIds = user._id;
+ $inc.attachedUsersCount = 1;
+ }
+ // 自分が(ローカル内で)初めてこのタグを使ったなら
+ if (isLocalUser(user) && !index.attachedLocalUserIds.some(id => id.equals(user._id))) {
+ $push.attachedLocalUserIds = user._id;
+ $inc.attachedLocalUsersCount = 1;
+ }
+ } else {
+ $pull.attachedUserIds = user._id;
+ $inc.attachedUsersCount = -1;
+ if (isLocalUser(user)) {
+ $pull.attachedLocalUserIds = user._id;
+ $inc.attachedLocalUsersCount = -1;
+ }
+ }
+ } else {
+ // 自分が初めてこのタグを使ったなら
+ if (!index.mentionedUserIds.some(id => id.equals(user._id))) {
+ $push.mentionedUserIds = user._id;
+ $inc.mentionedUsersCount = 1;
+ }
+ // 自分が(ローカル内で)初めてこのタグを使ったなら
+ if (isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id.equals(user._id))) {
+ $push.mentionedLocalUserIds = user._id;
+ $inc.mentionedLocalUsersCount = 1;
+ }
+ }
+
+ const q = {} as any;
+ if (Object.keys($push).length > 0) q.$push = $push;
+ if (Object.keys($pull).length > 0) q.$pull = $pull;
+ if (Object.keys($inc).length > 0) q.$inc = $inc;
+ if (Object.keys(q).length > 0) Hashtag.update({ tag }, q);
+ } else {
+ if (isUserAttached) {
+ Hashtag.insert({
+ tag,
+ mentionedUserIds: [],
+ mentionedUsersCount: 0,
+ mentionedLocalUserIds: [],
+ mentionedLocalUsersCount: 0,
+ attachedUserIds: [user._id],
+ attachedUsersCount: 1,
+ attachedLocalUserIds: isLocalUser(user) ? [user._id] : [],
+ attachedLocalUsersCount: isLocalUser(user) ? 1 : 0
+ });
+ } else {
+ Hashtag.insert({
+ tag,
+ mentionedUserIds: [user._id],
+ mentionedUsersCount: 1,
+ mentionedLocalUserIds: isLocalUser(user) ? [user._id] : [],
+ mentionedLocalUsersCount: isLocalUser(user) ? 1 : 0,
+ attachedUserIds: [],
+ attachedUsersCount: 0,
+ attachedLocalUserIds: [],
+ attachedLocalUsersCount: 0
+ });
+ }
+ }
+
+ if (!isUserAttached) {
+ hashtagChart.update(tag, user);
+ }
+}