From 1d5a54ff6f74569fa89c4083301d9b01eb80ad29 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 17 Feb 2019 23:41:47 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=8F=E3=83=83=E3=82=B7=E3=83=A5=E3=82=BF?= =?UTF-8?q?=E3=82=B0=E3=81=A7=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E6=A4=9C?= =?UTF-8?q?=E7=B4=A2=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=20(#4298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ハッシュタグでユーザー検索できるように * :art: * Increase limit * リモートユーザーも表示 * Fix bug * Fix bug * Improve performance --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 + src/client/app/common/views/components/mfm.ts | 8 +- .../app/common/views/components/user-list.vue | 2 +- src/client/app/common/views/pages/explore.vue | 86 ++++++++++++++++--- src/client/app/common/views/pages/follow.vue | 2 +- src/client/app/desktop/script.ts | 2 + .../desktop/views/components/user-card.vue | 2 +- .../desktop/views/deck/deck.user-column.vue | 2 +- .../desktop/views/home/user/user.header.vue | 2 +- src/client/app/mobile/script.ts | 1 + .../mobile/views/components/user-preview.vue | 2 +- .../app/mobile/views/pages/user/index.vue | 2 +- src/models/hashtag.ts | 34 +++++++- src/models/user.ts | 1 + src/remote/activitypub/models/person.ts | 9 ++ src/server/api/endpoints/hashtags/list.ts | 55 ++++++++++++ src/server/api/endpoints/hashtags/users.ts | 83 ++++++++++++++++++ src/server/api/endpoints/i/update.ts | 5 ++ src/services/note/create.ts | 4 +- src/services/register-hashtag.ts | 31 ------- src/services/update-hashtag.ts | 86 +++++++++++++++++++ 22 files changed, 366 insertions(+), 56 deletions(-) create mode 100644 src/server/api/endpoints/hashtags/list.ts create mode 100644 src/server/api/endpoints/hashtags/users.ts delete mode 100644 src/services/register-hashtag.ts create mode 100644 src/services/update-hashtag.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 219d42beeb..60751a61d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ChangeLog unreleased ---------- +* ハッシュタグでユーザー検索できるように * Exploreページに新規ユーザー一覧を追加 10.86.2 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 774bc169ed..d77a99ad43 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -225,6 +225,8 @@ common/views/pages/explore.vue: popular-users: "人気のユーザー" recently-updated-users: "最近投稿したユーザー" recently-registered-users: "新規ユーザー" + popular-tags: "人気のタグ" + federated: "連合" common/views/components/games/reversi/reversi.vue: matching: 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 @@

@{{ user | acct }}

- +
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 @@ 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 @@ @{{ user | acct }}
- +
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 @@ @{{ user | acct }}
- +
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 @@
- +
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 @@
- +
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 @@
- +
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 @@ {{ $t('follows-you') }}
- +
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('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('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 !tags.includes(x))) updateHashtag(user, tag, true, false); + //#region アイコンとヘッダー画像をフェッチ const [avatar, banner] = (await Promise.all([ 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); + } +}