ハッシュタグでユーザー検索できるように (#4298)
* ハッシュタグでユーザー検索できるように
* 🎨
* Increase limit
* リモートユーザーも表示
* Fix bug
* Fix bug
* Improve performance
This commit is contained in:
parent
03e2c7eec6
commit
1d5a54ff6f
22 changed files with 366 additions and 56 deletions
|
@ -3,6 +3,7 @@ ChangeLog
|
||||||
|
|
||||||
unreleased
|
unreleased
|
||||||
----------
|
----------
|
||||||
|
* ハッシュタグでユーザー検索できるように
|
||||||
* Exploreページに新規ユーザー一覧を追加
|
* Exploreページに新規ユーザー一覧を追加
|
||||||
|
|
||||||
10.86.2
|
10.86.2
|
||||||
|
|
|
@ -225,6 +225,8 @@ common/views/pages/explore.vue:
|
||||||
popular-users: "人気のユーザー"
|
popular-users: "人気のユーザー"
|
||||||
recently-updated-users: "最近投稿したユーザー"
|
recently-updated-users: "最近投稿したユーザー"
|
||||||
recently-registered-users: "新規ユーザー"
|
recently-registered-users: "新規ユーザー"
|
||||||
|
popular-tags: "人気のタグ"
|
||||||
|
federated: "連合"
|
||||||
|
|
||||||
common/views/components/games/reversi/reversi.vue:
|
common/views/components/games/reversi/reversi.vue:
|
||||||
matching:
|
matching:
|
||||||
|
|
|
@ -40,7 +40,11 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||||
},
|
},
|
||||||
customEmojis: {
|
customEmojis: {
|
||||||
required: false,
|
required: false,
|
||||||
}
|
},
|
||||||
|
isNote: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
|
@ -204,7 +208,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||||
return [createElement('router-link', {
|
return [createElement('router-link', {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
attrs: {
|
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);'
|
style: 'color:var(--mfmHashtag);'
|
||||||
}
|
}
|
||||||
}, `#${token.node.props.hashtag}`)];
|
}, `#${token.node.props.hashtag}`)];
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<p class="username">@{{ user | acct }}</p>
|
<p class="username">@{{ user | acct }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="description" v-if="user.description" :title="user.description">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,29 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<mk-user-list :make-promise="verifiedUsers">
|
<mk-user-list v-if="tag != null" :make-promise="tagUsers" :key="tag">
|
||||||
<span><fa :icon="faBookmark"/> {{ $t('verified-users') }}</span>
|
<fa :icon="faHashtag" fixed-width/>{{ tag }}
|
||||||
</mk-user-list>
|
</mk-user-list>
|
||||||
<mk-user-list :make-promise="popularUsers">
|
<mk-user-list v-if="tag != null" :make-promise="tagRemoteUsers" :key="tag">
|
||||||
<span><fa :icon="faChartLine"/> {{ $t('popular-users') }}</span>
|
<fa :icon="faHashtag" fixed-width/>{{ tag }} ({{ $t('federated') }})
|
||||||
</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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
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';
|
import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/pages/explore.vue'),
|
i18n: i18n('common/views/pages/explore.vue'),
|
||||||
|
|
||||||
|
props: {
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
verifiedUsers: () => this.$root.api('users', {
|
verifiedUsers: () => this.$root.api('users', {
|
||||||
|
@ -49,11 +73,49 @@ export default Vue.extend({
|
||||||
sort: '+createdAt',
|
sort: '+createdAt',
|
||||||
limit: 10
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
.vxjfqztj
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
> *
|
||||||
|
margin-right 16px
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
<span class="username">@{{ user | acct }}</span>
|
<span class="username">@{{ user | acct }}</span>
|
||||||
<div class="description">
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -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: '/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: '/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', 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: '/i/favorites', component: () => import('./views/deck/deck.favorites-column.vue').then(m => m.default) }
|
||||||
]}
|
]}
|
||||||
: { path: '/', component: MkHome, children: [
|
: { 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: '/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: '/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', 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/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
|
||||||
]},
|
]},
|
||||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<span class="username">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span>
|
<span class="username">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span>
|
||||||
|
|
||||||
<div class="description">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="description">
|
<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 class="fields" v-if="user.fields">
|
<div class="fields" v-if="user.fields">
|
||||||
<dl class="field" v-for="(field, i) in user.fields" :key="i">
|
<dl class="field" v-for="(field, i) in user.fields" :key="i">
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<ui-button @click="menu" ref="menu" :inline="true"><fa icon="ellipsis-h"/></ui-button>
|
<ui-button @click="menu" ref="menu" :inline="true"><fa icon="ellipsis-h"/></ui-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<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 class="fields" v-if="user.fields">
|
<div class="fields" v-if="user.fields">
|
||||||
<dl class="field" v-for="(field, i) in user.fields" :key="i">
|
<dl class="field" v-for="(field, i) in user.fields" :key="i">
|
||||||
|
|
|
@ -133,6 +133,7 @@ init((launch) => {
|
||||||
{ path: '/tags/:tag', component: MkTag },
|
{ path: '/tags/:tag', component: MkTag },
|
||||||
{ path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) },
|
{ 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', 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: '/share', component: MkShare },
|
||||||
{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi },
|
{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi },
|
||||||
{ path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [
|
{ path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="description">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
|
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<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 class="fields" v-if="user.fields">
|
<div class="fields" v-if="user.fields">
|
||||||
<dl class="field" v-for="(field, i) in user.fields" :key="i">
|
<dl class="field" v-for="(field, i) in user.fields" :key="i">
|
||||||
|
|
|
@ -3,11 +3,41 @@ import db from '../db/mongodb';
|
||||||
|
|
||||||
const Hashtag = db.get<IHashtags>('hashtags');
|
const Hashtag = db.get<IHashtags>('hashtags');
|
||||||
Hashtag.createIndex('tag', { unique: true });
|
Hashtag.createIndex('tag', { unique: true });
|
||||||
Hashtag.createIndex('mentionedUserIdsCount');
|
Hashtag.createIndex('mentionedUsersCount');
|
||||||
|
Hashtag.createIndex('mentionedLocalUsersCount');
|
||||||
|
Hashtag.createIndex('attachedUsersCount');
|
||||||
|
Hashtag.createIndex('attachedLocalUsersCount');
|
||||||
export default Hashtag;
|
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 {
|
export interface IHashtags {
|
||||||
tag: string;
|
tag: string;
|
||||||
mentionedUserIds: mongo.ObjectID[];
|
mentionedUserIds: mongo.ObjectID[];
|
||||||
mentionedUserIdsCount: number;
|
mentionedUsersCount: number;
|
||||||
|
mentionedLocalUserIds: mongo.ObjectID[];
|
||||||
|
mentionedLocalUsersCount: number;
|
||||||
|
attachedUserIds: mongo.ObjectID[];
|
||||||
|
attachedUsersCount: number;
|
||||||
|
attachedLocalUserIds: mongo.ObjectID[];
|
||||||
|
attachedLocalUsersCount: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ const User = db.get<IUser>('users');
|
||||||
User.createIndex('createdAt');
|
User.createIndex('createdAt');
|
||||||
User.createIndex('updatedAt');
|
User.createIndex('updatedAt');
|
||||||
User.createIndex('followersCount');
|
User.createIndex('followersCount');
|
||||||
|
User.createIndex('tags');
|
||||||
User.createIndex('username');
|
User.createIndex('username');
|
||||||
User.createIndex('usernameLower');
|
User.createIndex('usernameLower');
|
||||||
User.createIndex('host');
|
User.createIndex('host');
|
||||||
|
|
|
@ -23,6 +23,7 @@ import Following from '../../../models/following';
|
||||||
import { IIdentifier } from './identifier';
|
import { IIdentifier } from './identifier';
|
||||||
import { apLogger } from '../logger';
|
import { apLogger } from '../logger';
|
||||||
import { INote } from '../../../models/note';
|
import { INote } from '../../../models/note';
|
||||||
|
import { updateHashtag } from '../../../services/update-hashtag';
|
||||||
const logger = apLogger;
|
const logger = apLogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,6 +211,10 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
||||||
usersChart.update(user, true);
|
usersChart.update(user, true);
|
||||||
//#endregion
|
//#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 アイコンとヘッダー画像をフェッチ
|
//#region アイコンとヘッダー画像をフェッチ
|
||||||
const [avatar, banner] = (await Promise.all<IDriveFile>([
|
const [avatar, banner] = (await Promise.all<IDriveFile>([
|
||||||
person.icon,
|
person.icon,
|
||||||
|
@ -383,6 +388,10 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
||||||
$set: updates
|
$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もアップデートする
|
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
||||||
await Following.update({
|
await Following.update({
|
||||||
followerId: exist._id
|
followerId: exist._id
|
||||||
|
|
55
src/server/api/endpoints/hashtags/list.ts
Normal file
55
src/server/api/endpoints/hashtags/list.ts
Normal file
|
@ -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);
|
||||||
|
}));
|
83
src/server/api/endpoints/hashtags/users.ts
Normal file
83
src/server/api/endpoints/hashtags/users.ts
Normal file
|
@ -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 }))));
|
||||||
|
}));
|
|
@ -11,6 +11,7 @@ import { parse, parsePlain } from '../../../../mfm/parse';
|
||||||
import extractEmojis from '../../../../misc/extract-emojis';
|
import extractEmojis from '../../../../misc/extract-emojis';
|
||||||
import extractHashtags from '../../../../misc/extract-hashtags';
|
import extractHashtags from '../../../../misc/extract-hashtags';
|
||||||
import * as langmap from 'langmap';
|
import * as langmap from 'langmap';
|
||||||
|
import { updateHashtag } from '../../../../services/update-hashtag';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
|
@ -221,6 +222,10 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
updates.emojis = emojis;
|
updates.emojis = emojis;
|
||||||
updates.tags = tags;
|
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
|
//#endregion
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import UserList from '../../models/user-list';
|
||||||
import resolveUser from '../../remote/resolve-user';
|
import resolveUser from '../../remote/resolve-user';
|
||||||
import Meta from '../../models/meta';
|
import Meta from '../../models/meta';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import registerHashtag from '../register-hashtag';
|
import { updateHashtag } from '../update-hashtag';
|
||||||
import isQuote from '../../misc/is-quote';
|
import isQuote from '../../misc/is-quote';
|
||||||
import notesChart from '../../services/chart/notes';
|
import notesChart from '../../services/chart/notes';
|
||||||
import perUserNotesChart from '../../services/chart/per-user-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) {
|
if (data.files) {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
86
src/services/update-hashtag.ts
Normal file
86
src/services/update-hashtag.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue