Compare commits

...

34 Commits

Author SHA1 Message Date
syuilo
7ef1205f8b 10.87.1 2019-02-18 01:11:28 +09:00
syuilo
e8db63e788 Fix bug 2019-02-18 01:11:14 +09:00
syuilo
0bcef2453c 10.87.0 2019-02-18 00:51:39 +09:00
syuilo
b9f549135c Fix bug 2019-02-18 00:49:34 +09:00
syuilo
87b0017386 Fix bug 2019-02-18 00:47:45 +09:00
syuilo
cc8ff556d4 Fix bug 2019-02-18 00:45:43 +09:00
syuilo
021f74da54 🎨 2019-02-18 00:41:05 +09:00
syuilo
f9389802d7 Update CHANGELOG.md 2019-02-18 00:39:31 +09:00
syuilo
18dd172c97 Chore: Fix type definition 2019-02-18 00:20:14 +09:00
syuilo
1d5a54ff6f ハッシュタグでユーザー検索できるように (#4298)
* ハッシュタグでユーザー検索できるように

* 🎨

* Increase limit

* リモートユーザーも表示

* Fix bug

* Fix bug

* Improve performance
2019-02-17 23:41:47 +09:00
MeiMei
03e2c7eec6 Fix #4300 (#4304)
* Fix #4300

* sidebar
2019-02-17 23:40:00 +09:00
dependabot[bot]
0902727d1c Update @types/node requirement from 10.12.21 to 10.12.24 (#4231)
Updates the requirements on [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-17 21:42:40 +09:00
dependabot[bot]
496895634d Update @types/sharp requirement from 0.21.1 to 0.21.2 (#4263)
Updates the requirements on [@types/sharp](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-17 21:42:03 +09:00
dependabot[bot]
9414e9e258 Update cssnano requirement from 4.1.8 to 4.1.10 (#4265)
Updates the requirements on [cssnano](https://github.com/cssnano/cssnano) to permit the latest version.
- [Release notes](https://github.com/cssnano/cssnano/releases)
- [Commits](https://github.com/cssnano/cssnano/commits/v4.1.10)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-17 21:41:36 +09:00
Acid Chicken (硫酸鶏)
357528d139 Use object instead of if chain (#4212) 2019-02-17 21:40:53 +09:00
MeiMei
c4efbdf4c7 Fix #4292 (#4294)
* Fix #4292

* use commit
2019-02-17 21:28:20 +09:00
かひわし4(バージョン1)
fb4a921cd9 Docker: Add support for service worker (#4296)
Service worker requires web-push package.
2019-02-17 21:26:00 +09:00
Acid Chicken (硫酸鶏)
683b242215 Use Record<string, number> instead of any (#4293) 2019-02-17 19:38:46 +09:00
syuilo
a5660d6c82 Exploreページに新規ユーザー一覧を追加 (#4295) 2019-02-17 17:53:51 +09:00
syuilo
f632ec50c1 10.86.2 2019-02-17 03:29:17 +09:00
syuilo
a55d15214b Update ja-JP.yml 2019-02-17 03:28:36 +09:00
syuilo
f1709a2cc2 Update CHANGELOG.md 2019-02-17 03:23:43 +09:00
syuilo
effa542958 Improve user page 2019-02-17 03:23:34 +09:00
MeiMei
e8bf742c87 別タブでルートより下を開いたときにはデッキにしないように (#4291) 2019-02-17 02:26:17 +09:00
syuilo
2e6652edce Resolve #4272 2019-02-17 01:50:17 +09:00
syuilo
230c204b48 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-17 01:12:08 +09:00
syuilo
3755c600b1 Update CHANGELOG.md 2019-02-17 01:11:59 +09:00
Acid Chicken (硫酸鶏)
24513fc0a3 Update media-banner.vue (#4287) 2019-02-17 01:09:49 +09:00
syuilo
0a79a6564a Add support for disabled timeline to deck
Close #4286
Resolve #4275
2019-02-17 01:04:21 +09:00
syuilo
562bb5842b [Client] Improve featured notes page 2019-02-17 00:28:41 +09:00
syuilo
ec3ca3032e ミュートワードで正規表現を使えるように 2019-02-16 19:37:05 +09:00
syuilo
890770c275 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-16 17:42:01 +09:00
syuilo
9ed58a1b4e 🎨 2019-02-16 17:41:54 +09:00
Acid Chicken (硫酸鶏)
08984be2fe Update config.yml 2019-02-16 17:05:56 +09:00
42 changed files with 586 additions and 301 deletions

View File

@@ -149,38 +149,20 @@ workflows:
- build:
requires:
- hold
filters:
branches:
ignore:
- l10n_develop
- imgbot
- patch-autogen
- test:
executor: with-redis
requires:
- build
filters:
branches:
ignore:
# - master
- l10n_develop
- imgbot
- patch-autogen
- test:
without_redis: true
requires:
- build
filters:
# branches:
# only: master
branches:
ignore:
# - master
- l10n_develop
- imgbot
- patch-autogen
docker:
jobs:
- ok:
filters:
branches:
ignore: master
- hold:
type: approval
filters:

View File

@@ -1,6 +1,27 @@
ChangeLog
=========
10.87.1
----------
* ハッシュタグ検索で大文字小文字が区別されてしまう問題を修正
10.87.0
----------
* ハッシュタグでユーザー検索できるように
* Exploreページに新規ユーザー一覧を追加
* デッキ使用中にホーム扱いで開かれた時にタイムラインボタン等がない問題を修正
* デッキ使用中に / 以外でリロードした際にホームモードになる問題を修正
10.86.2
----------
* 別タブでルートより下を開いたときにはデッキにしないように
* 横のナビゲーションバーの改善
* MIDIファイルがオーディオ扱いになる問題を修正
* ミュートワードで正規表現を使えるように
* デッキで無効になったタイムラインに警告を表示するように
* デザインの調整
* その他細かな修正
10.86.1
----------
* ナビゲーションバーの「ホーム」を「タイムライン」に改称

View File

@@ -32,6 +32,7 @@ FROM base AS runner
RUN apk add --no-cache \
ffmpeg \
tini
RUN npm i -g web-push
ENTRYPOINT ["/sbin/tini", "--"]
COPY --from=builder /misskey/node_modules ./node_modules

View File

@@ -220,6 +220,14 @@ auth/views/index.vue:
error: "セッションが存在しません。"
sign-in: "サインインしてください"
common/views/pages/explore.vue:
verified-users: "公式アカウント"
popular-users: "人気のユーザー"
recently-updated-users: "最近投稿したユーザー"
recently-registered-users: "新規ユーザー"
popular-tags: "人気のタグ"
federated: "連合"
common/views/components/games/reversi/reversi.vue:
matching:
waiting-for: "{}を待っています"
@@ -1823,6 +1831,9 @@ deck:
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
disabled-timeline:
title: "無効化されたタイムライン"
description: "サーバーの運営者により、このタイムラインは使用できない状態に設定されています。"
deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"

View File

@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.86.1",
"clientVersion": "2.0.14327",
"version": "10.87.1",
"clientVersion": "2.0.14358",
"codename": "nighthike",
"repository": {
"type": "git",
@@ -67,7 +67,7 @@
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.5",
"@types/mongodb": "3.1.19",
"@types/node": "10.12.21",
"@types/node": "10.12.24",
"@types/nodemailer": "4.6.5",
"@types/nprogress": "0.0.29",
"@types/oauth": "0.9.1",
@@ -83,7 +83,7 @@
"@types/request-stats": "3.0.0",
"@types/rimraf": "2.0.2",
"@types/seedrandom": "2.4.27",
"@types/sharp": "0.21.1",
"@types/sharp": "0.21.2",
"@types/showdown": "1.9.2",
"@types/speakeasy": "2.0.3",
"@types/systeminformation": "3.23.1",
@@ -110,7 +110,7 @@
"commander": "2.19.0",
"crc-32": "1.2.0",
"css-loader": "2.1.0",
"cssnano": "4.1.8",
"cssnano": "4.1.10",
"dateformat": "3.0.3",
"deep-equal": "1.0.1",
"deepcopy": "0.6.3",

View File

@@ -4,7 +4,8 @@ export default function(me, settings, note) {
const includesMutedWords = (text: string) =>
text
? settings.mutedWords.some(q => q.length > 0 && !q.some(word => !text.includes(word)))
? settings.mutedWords.some(q => q.length > 0 && !q.some(word =>
word.startsWith('/') && word.endsWith('/') ? !(new RegExp(word.substr(1, word.length - 2)).test(text)) : !text.includes(word)))
: false;
return (

View File

@@ -5,7 +5,7 @@
<b>{{ $t('sensitive') }}</b>
<span>{{ $t('click-to-show') }}</span>
</div>
<div class="audio" v-else-if="media.type.startsWith('audio')">
<div class="audio" v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'">
<audio class="audio"
:src="media.url"
:title="media.name"

View File

@@ -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}`)];

View File

@@ -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>

View File

@@ -1,26 +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}-local`">
<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 v-if="tag != null" :make-promise="tagRemoteUsers" :key="`${tag}-remote`">
<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 } 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', {
@@ -40,11 +67,55 @@ export default Vue.extend({
sort: '+updatedAt',
limit: 10
}),
faBookmark, faChartLine, faCommentAlt
recentlyRegisteredUsers: () => this.$root.api('users', {
origin: 'local',
state: 'alive',
sort: '+createdAt',
limit: 10
}),
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>

View File

@@ -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>

View File

@@ -124,11 +124,17 @@ init(async (launch, os) => {
require('./views/components');
require('./views/widgets');
os.store.commit('device/set', {
key: 'inDeckMode',
value: os.store.getters.isSignedIn && os.store.state.device.deckMode
&& (document.location.pathname === '/' || window.performance.navigation.type === 1)
});
// Init router
const router = new VueRouter({
mode: 'history',
routes: [
os.store.getters.isSignedIn && os.store.state.device.deckMode
os.store.state.device.inDeckMode
? { path: '/', name: 'index', component: MkDeck, children: [
{ path: '/@:user', name: 'user', component: () => import('./views/deck/deck.user-column.vue').then(m => m.default), children: [
{ path: '', name: 'user', component: () => import('./views/deck/deck.user-column.home.vue').then(m => m.default) },
@@ -138,8 +144,9 @@ init(async (launch, os) => {
{ path: '/notes/:note', name: 'note', component: () => import('./views/deck/deck.note-column.vue').then(m => m.default) },
{ path: '/search', component: () => import('./views/deck/deck.search-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: '/explore', component: () => import('./views/deck/deck.explore-column.vue').then(m => m.default) },
{ path: '/featured', name: 'featured', component: () => import('./views/deck/deck.featured-column.vue').then(m => m.default) },
{ path: '/explore', name: '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: [
@@ -152,8 +159,9 @@ init(async (launch, os) => {
{ path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) },
{ path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) },
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) },
{ path: '/featured', component: () => import('./views/home/featured.vue').then(m => m.default) },
{ path: '/explore', component: () => import('../common/views/pages/explore.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 },

View File

@@ -63,7 +63,7 @@
<li @click="toggleDeckMode">
<p>
<span>{{ $t('@.deck') }}</span>
<template v-if="$store.state.device.deckMode"><i><fa :icon="faHome"/></i></template>
<template v-if="$store.state.device.inDeckMode"><i><fa :icon="faHome"/></i></template>
<template v-else><i><fa :icon="faColumns"/></i></template>
</p>
</li>
@@ -165,8 +165,8 @@ export default Vue.extend({
});
},
toggleDeckMode() {
this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.deckMode });
location.reload();
this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode });
location.replace('/');
},
}
});

View File

@@ -1,13 +1,13 @@
<template>
<div class="nav">
<ul>
<li v-if="!$store.state.device.deckMode" class="timeline" @click="goToTop">
<li class="timeline" :class="{ active: $route.name == 'index' }" @click="goToTop">
<router-link to="/"><fa icon="home"/><p>{{ $t('@.timeline') }}</p></router-link>
</li>
<li class="featured">
<li class="featured" :class="{ active: $route.name == 'featured' }">
<router-link to="/featured"><fa :icon="faNewspaper"/><p>{{ $t('@.featured-notes') }}</p></router-link>
</li>
<li class="explore">
<li class="explore" :class="{ active: $route.name == 'explore' }">
<router-link to="/explore"><fa :icon="faHashtag"/><p>{{ $t('@.explore') }}</p></router-link>
</li>
<li class="game">

View File

@@ -6,24 +6,14 @@
</div>
<div class="nav" v-if="$store.getters.isSignedIn">
<template v-if="$store.state.device.deckMode">
<div class="deck active" @click="goToTop">
<router-link to="/"><fa icon="columns"/></router-link>
</div>
<div class="home">
<a @click="toggleDeckMode(false)"><fa icon="home"/></a>
</div>
</template>
<template v-else>
<div class="home active" @click="goToTop">
<router-link to="/"><fa icon="home"/></router-link>
</div>
<div class="deck">
<a @click="toggleDeckMode(true)"><fa icon="columns"/></a>
</div>
</template>
<div class="messaging">
<a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a>
<div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop">
<router-link to="/"><fa icon="home"/></router-link>
</div>
<div class="featured" :class="{ active: $route.name == 'featured' }">
<router-link to="/featured"><fa :icon="faNewspaper"/></router-link>
</div>
<div class="explore" :class="{ active: $route.name == 'explore' }">
<router-link to="/explore"><fa :icon="faHashtag"/></router-link>
</div>
<div class="game">
<a @click="game"><fa icon="gamepad"/><template v-if="hasGameInvitations"><fa icon="circle"/></template></a>
@@ -37,30 +27,34 @@
<div ref="notificationsButton" :class="{ active: showNotifications }">
<a @click="notifications"><fa :icon="['far', 'bell']"/></a>
</div>
<div class="messaging">
<a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a>
</div>
<div>
<a @click="settings"><fa icon="cog"/></a>
</div>
</div>
<div class="account">
<router-link :to="`/@${ $store.state.i.username }`">
<mk-avatar class="avatar" :user="$store.state.i"/>
</router-link>
<div class="nav menu">
<div class="signout">
<a @click="signout"><fa icon="power-off"/></a>
</div>
<div>
<router-link to="/i/favorites"><fa icon="star"/></router-link>
</div>
<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
<a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a>
</div>
<div class="signout">
<a @click="signout"><fa icon="power-off"/></a>
</div>
<div>
<router-link to="/i/favorites"><fa icon="star"/></router-link>
</div>
<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
<a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a>
</div>
<div class="account">
<router-link :to="`/@${ $store.state.i.username }`">
<mk-avatar class="avatar" :user="$store.state.i"/>
</router-link>
</div>
<div>
<template v-if="$store.state.device.inDeckMode">
<a @click="toggleDeckMode(false)"><fa icon="home"/></a>
</template>
<template v-else>
<a @click="toggleDeckMode(true)"><fa icon="columns"/></a>
</template>
</div>
</div>
<div class="nav dark">
<div>
<a @click="dark"><template v-if="$store.state.device.darkmode"><fa icon="moon"/></template><template v-else><fa :icon="['far', 'moon']"/></template></a>
</div>
@@ -85,6 +79,7 @@ import MkDriveWindow from './drive-window.vue';
import MkMessagingWindow from './messaging-window.vue';
import MkGameWindow from './game-window.vue';
import contains from '../../../common/scripts/contains';
import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n('desktop/views/components/ui.sidebar.vue'),
@@ -92,7 +87,8 @@ export default Vue.extend({
return {
hasGameInvitations: false,
connection: null,
showNotifications: false
showNotifications: false,
faNewspaper, faHashtag
};
},
@@ -124,7 +120,7 @@ export default Vue.extend({
methods: {
toggleDeckMode(deck) {
this.$store.commit('device/set', { key: 'deckMode', value: deck });
location.reload();
location.replace('/');
},
onReversiInvited() {
@@ -278,44 +274,23 @@ export default Vue.extend({
> .nav.bottom
position absolute
bottom 128px
bottom 0
left 0
> .account
position absolute
bottom 64px
left 0
width $width
height $width
padding 14px
> .account
width $width
height $width
padding 14px
> .menu
display none
position absolute
bottom 64px
left 0
background var(--desktopHeaderBg)
&:hover
> .menu
> *
display block
> *:not(.menu)
display block
width 100%
height 100%
> .avatar
pointer-events none
width 100%
height 100%
> .dark
position absolute
bottom 0
left 0
width $width
height $width
> .avatar
pointer-events none
width 100%
height 100%
> .notifications
position fixed

View File

@@ -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>

View File

@@ -5,7 +5,7 @@
</span>
<div>
<x-explore/>
<x-explore v-bind="$attrs"/>
</div>
</x-column>
</template>

View File

@@ -44,6 +44,7 @@ export default Vue.extend({
this.$root.api('notes/featured', {
limit: 20,
}).then(notes => {
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
res(notes);
this.fetching = false;
this.$emit('loaded');

View File

@@ -1,14 +1,25 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
<div class="iwaalbte" v-if="disabled">
<p>
<fa :icon="faMinusCircle"/>
{{ $t('disabled-timeline.title') }}
</p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
</div>
<x-notes v-else ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
</template>
<script lang="ts">
import Vue from 'vue';
import XNotes from './deck.notes.vue';
import { faMinusCircle } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../i18n';
const fetchLimit = 10;
export default Vue.extend({
i18n: i18n('deck'),
components: {
XNotes
},
@@ -36,7 +47,9 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
connection: null
connection: null,
disabled: false,
faMinusCircle
};
},
@@ -75,6 +88,12 @@ export default Vue.extend({
this.connection.on('unfollow', this.onChangeFollowing);
}
this.$root.getMeta().then(meta => {
this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) ||
meta.disableGlobalTimeline && ['global'].includes(this.src));
});
this.fetch();
},
@@ -149,3 +168,16 @@ export default Vue.extend({
}
});
</script>
<style lang="stylus" scoped>
.iwaalbte
color var(--text)
text-align center
> p
margin 16px
&.desc
font-size 14px
</style>

View File

@@ -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">

View File

@@ -30,6 +30,7 @@ export default Vue.extend({
this.$root.api('notes/featured', {
limit: 20
}).then(notes => {
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
this.notes = notes;
this.fetching = false;

View File

@@ -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">

View File

@@ -10,8 +10,6 @@
</ui-container>
</div>
<x-photos :user="user"/>
<x-friends :user="user"/>
<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
<x-timeline class="timeline" ref="tl" :user="user"/>
</div>
</template>
@@ -23,8 +21,6 @@ import parseAcct from '../../../../../../misc/acct/parse';
import Progress from '../../../../common/scripts/loading';
import XTimeline from './user.timeline.vue';
import XPhotos from './user.photos.vue';
import XFollowersYouKnow from './user.followers-you-know.vue';
import XFriends from './user.friends.vue';
import XIntegrations from './user.integrations.vue';
import XActivity from '../../../../common/views/components/activity.vue';
@@ -33,8 +29,6 @@ export default Vue.extend({
components: {
XTimeline,
XPhotos,
XFollowersYouKnow,
XFriends,
XIntegrations,
XActivity
},

View File

@@ -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: [

View File

@@ -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>

View File

@@ -3,7 +3,7 @@
<span slot="header"><span style="margin-right:4px;"><fa :icon="faHashtag"/></span>{{ $t('@.explore') }}</span>
<main>
<x-explore/>
<x-explore v-bind="$attrs"/>
</main>
</mk-ui>
</template>

View File

@@ -38,6 +38,7 @@ export default Vue.extend({
this.$root.api('notes/featured', {
limit: 20
}).then(notes => {
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
this.notes = notes;
this.fetching = false;

View File

@@ -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">

View File

@@ -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;
}

View File

@@ -49,7 +49,7 @@ export type INote = {
localOnly: boolean;
renoteCount: number;
repliesCount: number;
reactionCounts: any;
reactionCounts: Record<string, number>;
mentions: mongo.ObjectID[];
mentionedRemoteUsers: {
uri: string;

View File

@@ -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');

1
src/prelude/symbol.ts Normal file
View File

@@ -0,0 +1 @@
export const fallback = Symbol('fallback');

View File

@@ -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;
/**
@@ -142,7 +143,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
const { fields, services } = analyzeAttachments(person.attachment);
const tags = extractHashtags(person.tag);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
const isBot = object.type == 'Service';
@@ -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,
@@ -338,7 +343,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
const { fields, services } = analyzeAttachments(person.attachment);
const tags = extractHashtags(person.tag);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
const updates = {
lastFetchedAt: new Date(),
@@ -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

View File

@@ -1,6 +1,7 @@
import $ from 'cafy';
import File, { packMany } from '../../../../../models/drive-file';
import define from '../../../define';
import { fallback } from '../../../../../prelude/symbol';
export const meta = {
requireCredential: false,
@@ -37,32 +38,15 @@ export const meta = {
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
let _sort;
if (ps.sort) {
if (ps.sort == '+createdAt') {
_sort = {
uploadDate: -1
};
} else if (ps.sort == '-createdAt') {
_sort = {
uploadDate: 1
};
} else if (ps.sort == '+size') {
_sort = {
length: -1
};
} else if (ps.sort == '-size') {
_sort = {
length: 1
};
}
} else {
_sort = {
_id: -1
};
}
const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
'+createdAt': { uploadDate: -1 },
'-createdAt': { uploadDate: 1 },
'+size': { length: -1 },
'-size': { length: 1 },
[fallback]: { _id: -1 }
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const q = {
'metadata.deletedAt': { $exists: false },
} as any;
@@ -73,7 +57,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const files = await File
.find(q, {
limit: ps.limit,
sort: _sort,
sort: sort[ps.sort] || sort[fallback],
skip: ps.offset
});

View File

@@ -1,6 +1,7 @@
import $ from 'cafy';
import User, { pack } from '../../../../models/user';
import define from '../../define';
import { fallback } from '../../../../prelude/symbol';
export const meta = {
requireCredential: true,
@@ -52,40 +53,17 @@ export const meta = {
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
let _sort;
if (ps.sort) {
if (ps.sort == '+follower') {
_sort = {
followersCount: -1
};
} else if (ps.sort == '-follower') {
_sort = {
followersCount: 1
};
} else if (ps.sort == '+createdAt') {
_sort = {
createdAt: -1
};
} else if (ps.sort == '+updatedAt') {
_sort = {
updatedAt: -1
};
} else if (ps.sort == '-createdAt') {
_sort = {
createdAt: 1
};
} else if (ps.sort == '-updatedAt') {
_sort = {
updatedAt: 1
};
}
} else {
_sort = {
_id: -1
};
}
const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
'+follower': { followersCount: -1 },
'-follower': { followersCount: 1 },
'+createdAt': { createdAt: -1 },
'-createdAt': { createdAt: 1 },
'+updatedAt': { updatedAt: -1 },
'-updatedAt': { updatedAt: 1 },
[fallback]: { _id: -1 }
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const q = {
$and: []
} as any;
@@ -117,7 +95,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const users = await User
.find(q, {
limit: ps.limit,
sort: _sort,
sort: sort[ps.sort] || sort[fallback],
skip: ps.offset
});

View 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);
}));

View 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 }))));
}));

View File

@@ -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: {
@@ -216,11 +217,15 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
if (updates.description != null) {
const tokens = parse(updates.description);
emojis = emojis.concat(extractEmojis(tokens));
tags = extractHashtags(tokens);
tags = extractHashtags(tokens).map(tag => tag.toLowerCase());
}
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

View File

@@ -1,6 +1,9 @@
import $ from 'cafy';
import User, { pack } from '../../../models/user';
import define from '../define';
import { fallback } from '../../../prelude/symbol';
const nonnull = { $ne: null as any };
export const meta = {
requireCredential: false,
@@ -50,71 +53,48 @@ export const meta = {
}
};
const state: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
'admin': { isAdmin: true },
'moderator': { isModerator: true },
'adminOrModerator': {
$or: [
{ isAdmin: true },
{ isModerator: true }
]
},
'verified': { isVerified: true },
'alive': {
updatedAt: { $gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }
},
[fallback]: {}
};
const origin: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
'local': { host: null },
'remote': { host: nonnull },
[fallback]: {}
};
const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
'+follower': { followersCount: -1 },
'-follower': { followersCount: 1 },
'+createdAt': { createdAt: -1 },
'-createdAt': { createdAt: 1 },
'+updatedAt': { updatedAt: -1 },
'-updatedAt': { updatedAt: 1 },
[fallback]: { _id: -1 }
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
let _sort;
if (ps.sort) {
if (ps.sort == '+follower') {
_sort = {
followersCount: -1
};
} else if (ps.sort == '-follower') {
_sort = {
followersCount: 1
};
} else if (ps.sort == '+createdAt') {
_sort = {
createdAt: -1
};
} else if (ps.sort == '+updatedAt') {
_sort = {
updatedAt: -1
};
} else if (ps.sort == '-createdAt') {
_sort = {
createdAt: 1
};
} else if (ps.sort == '-updatedAt') {
_sort = {
updatedAt: 1
};
}
} else {
_sort = {
_id: -1
};
}
const q = {
$and: []
} as any;
// state
q.$and.push(
ps.state == 'admin' ? { isAdmin: true } :
ps.state == 'moderator' ? { isModerator: true } :
ps.state == 'adminOrModerator' ? {
$or: [{
isAdmin: true
}, {
isModerator: true
}]
} :
ps.state == 'verified' ? { isVerified: true } :
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, {
.find({
$and: [
state[ps.state] || state[fallback],
origin[ps.origin] || origin[fallback]
]
}, {
limit: ps.limit,
sort: _sort,
sort: sort[ps.sort] || sort[fallback],
skip: ps.offset
});

View File

@@ -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';
@@ -234,8 +234,8 @@ 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) {

View File

@@ -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);
}

View 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);
}
}