Compare commits

..

53 Commits

Author SHA1 Message Date
f968d05ea0 2.37.7 2018-06-12 09:10:52 +09:00
d6e5dc2167 Fix bugs 2018-06-12 09:10:34 +09:00
460147fea2 2.37.6 2018-06-12 08:59:36 +09:00
cea44834bb Improve usability 2018-06-12 08:58:50 +09:00
1af50fd7b8 冗長なハッシュタグの表示を無くした 2018-06-12 08:43:48 +09:00
b18013025f 🎨 2018-06-12 08:09:27 +09:00
399eb60809 2.37.5 2018-06-12 02:47:17 +09:00
ed67e3506b ✌️ 2018-06-12 02:46:54 +09:00
d8ff37fc45 ✌️ 2018-06-12 02:28:28 +09:00
2fcc3bb1ea 2.37.4 2018-06-12 02:19:00 +09:00
2e680c3d1e Fix bug 2018-06-12 02:18:29 +09:00
af0a0ef41b Merge branch 'master' of https://github.com/syuilo/misskey 2018-06-12 02:03:26 +09:00
bbfccb0bbf 🎨 2018-06-12 02:03:18 +09:00
c89eb5d69f Merge pull request #1705 from syuilo/l10n_master
New Crowdin translations
2018-06-12 02:03:06 +09:00
ebde84214e Improve hashtag trend detection 2018-06-12 02:00:05 +09:00
03fbae7b6d 変数調整 2018-06-12 01:51:51 +09:00
f90e9596d4 Fix bug 2018-06-12 01:48:29 +09:00
944f9524e2 Fix bug 2018-06-12 01:45:58 +09:00
c61050244e 変数調整 2018-06-12 01:43:56 +09:00
90337adbbc Improve hashtag trend detection 2018-06-12 01:41:17 +09:00
7b67e41c5b New translations ja.yml (Polish) 2018-06-11 19:22:16 +09:00
91db24fcfc 2.37.3 2018-06-11 14:16:46 +09:00
bb53db905f ✌️ 2018-06-11 14:16:21 +09:00
0e9a1efe46 Merge pull request #1704 from syuilo/l10n_master
New Crowdin translations
2018-06-11 14:11:33 +09:00
289cd3e200 New translations ja.yml (English) 2018-06-11 14:10:47 +09:00
e0f847e539 ✌️ 2018-06-11 14:06:23 +09:00
c2842b486e New translations ja.yml (Portuguese) 2018-06-11 13:51:15 +09:00
7235ade42f New translations ja.yml (Korean) 2018-06-11 13:51:13 +09:00
850be2df1d New translations ja.yml (Polish) 2018-06-11 13:51:11 +09:00
d504501440 New translations ja.yml (Chinese Simplified) 2018-06-11 13:51:09 +09:00
208392f12c New translations ja.yml (Italian) 2018-06-11 13:51:07 +09:00
0fe036c640 New translations ja.yml (Russian) 2018-06-11 13:51:05 +09:00
a40c41f0b0 New translations ja.yml (English) 2018-06-11 13:51:03 +09:00
4affa5b710 New translations ja.yml (Spanish) 2018-06-11 13:51:01 +09:00
4eb574d991 New translations ja.yml (German) 2018-06-11 13:50:59 +09:00
2c1577ea24 New translations ja.yml (French) 2018-06-11 13:50:56 +09:00
b87e7e50b6 Clean up 2018-06-11 13:49:53 +09:00
36215d50bd 2.37.2 2018-06-11 13:48:07 +09:00
5ff1245d0c Merge pull request #1703 from syuilo/l10n_master
New Crowdin translations
2018-06-11 13:47:42 +09:00
ebd189fb27 🍕 2018-06-11 13:46:23 +09:00
6f724827bd ✌️ 2018-06-11 13:45:32 +09:00
b6a0982012 New translations ja.yml (English) 2018-06-11 13:00:50 +09:00
c3e375e8a5 ✌️ 2018-06-11 12:52:47 +09:00
302409fd83 New translations ja.yml (Portuguese) 2018-06-11 11:51:04 +09:00
a2046461c1 New translations ja.yml (Korean) 2018-06-11 11:51:03 +09:00
6660c34120 New translations ja.yml (Polish) 2018-06-11 11:51:01 +09:00
b88ccf0ddd New translations ja.yml (Chinese Simplified) 2018-06-11 11:50:58 +09:00
b898bbf94c New translations ja.yml (Italian) 2018-06-11 11:50:57 +09:00
787e89eb95 New translations ja.yml (Russian) 2018-06-11 11:50:55 +09:00
1022c2c438 New translations ja.yml (English) 2018-06-11 11:50:53 +09:00
ba21c62ed4 New translations ja.yml (Spanish) 2018-06-11 11:50:51 +09:00
bfe66c919b New translations ja.yml (German) 2018-06-11 11:50:49 +09:00
3dacf7f661 New translations ja.yml (French) 2018-06-11 11:50:47 +09:00
17 changed files with 204 additions and 65 deletions

View File

@ -70,6 +70,7 @@ common:
donation: "Spenden" donation: "Spenden"
nav: "Navigation" nav: "Navigation"
tips: "Tipps" tips: "Tipps"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "Widget hinzufügen:" widgets: "Widget hinzufügen:"
home: "Startseite" home: "Startseite"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "Serverinformationen" title: "Serverinformationen"
toggle: "Sicht umschalten" toggle: "Sicht umschalten"

View File

@ -70,6 +70,7 @@ common:
donation: "Donation" donation: "Donation"
nav: "Navigation" nav: "Navigation"
tips: "Tips" tips: "Tips"
hashtags: "Hashtags"
deck: deck:
widgets: "Widgets" widgets: "Widgets"
home: "Home" home: "Home"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "Toggle views" toggle: "Toggle views"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "Hashtags" title: "Hashtags"
count: "{} users mentioned"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "Server info" title: "Server info"
toggle: "Toggle views" toggle: "Toggle views"

View File

@ -70,6 +70,7 @@ common:
donation: "寄付のお願い" donation: "寄付のお願い"
nav: "ナビゲーション" nav: "ナビゲーション"
tips: "ヒント" tips: "ヒント"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "ウィジェット" widgets: "ウィジェット"
home: "ホーム" home: "ホーム"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"

View File

@ -70,6 +70,7 @@ common:
donation: "Dons" donation: "Dons"
nav: "Navigation" nav: "Navigation"
tips: "Conseils" tips: "Conseils"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "Widgets" widgets: "Widgets"
home: "Accueil" home: "Accueil"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "Info sur le serveur" title: "Info sur le serveur"
toggle: "Afficher les vues" toggle: "Afficher les vues"

View File

@ -70,6 +70,7 @@ common:
donation: "寄付のお願い" donation: "寄付のお願い"
nav: "ナビゲーション" nav: "ナビゲーション"
tips: "ヒント" tips: "ヒント"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "ウィジェット" widgets: "ウィジェット"
home: "ホーム" home: "ホーム"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"

View File

@ -257,6 +257,8 @@ common/views/widgets/posts-monitor.vue:
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
empty: "トレンドなし"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"

View File

@ -70,6 +70,7 @@ common:
donation: "寄付のお願い" donation: "寄付のお願い"
nav: "ナビゲーション" nav: "ナビゲーション"
tips: "ヒント" tips: "ヒント"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "ウィジェット" widgets: "ウィジェット"
home: "ホーム" home: "ホーム"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"

View File

@ -70,6 +70,7 @@ common:
donation: "Dotacje" donation: "Dotacje"
nav: "Nawigacja" nav: "Nawigacja"
tips: "Wskazówki" tips: "Wskazówki"
hashtags: "Hashtagi"
deck: deck:
widgets: "Widżety" widgets: "Widżety"
home: "Strona główna" home: "Strona główna"
@ -225,7 +226,8 @@ common/views/widgets/posts-monitor.vue:
title: "Wykres wpisów" title: "Wykres wpisów"
toggle: "Przełącz widok" toggle: "Przełącz widok"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "Hashtagi"
count: "Wspomniany przez {} użytkowników"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "Informacje o serwerze" title: "Informacje o serwerze"
toggle: "Przełącz widok" toggle: "Przełącz widok"

View File

@ -70,6 +70,7 @@ common:
donation: "寄付のお願い" donation: "寄付のお願い"
nav: "ナビゲーション" nav: "ナビゲーション"
tips: "ヒント" tips: "ヒント"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "ウィジェット" widgets: "ウィジェット"
home: "ホーム" home: "ホーム"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"

View File

@ -70,6 +70,7 @@ common:
donation: "寄付のお願い" donation: "寄付のお願い"
nav: "ナビゲーション" nav: "ナビゲーション"
tips: "ヒント" tips: "ヒント"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "ウィジェット" widgets: "ウィジェット"
home: "ホーム" home: "ホーム"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"

View File

@ -70,6 +70,7 @@ common:
donation: "寄付のお願い" donation: "寄付のお願い"
nav: "ナビゲーション" nav: "ナビゲーション"
tips: "ヒント" tips: "ヒント"
hashtags: "ハッシュタグ"
deck: deck:
widgets: "ウィジェット" widgets: "ウィジェット"
home: "ホーム" home: "ホーム"
@ -226,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/hashtags.vue: common/views/widgets/hashtags.vue:
title: "ハッシュタグ" title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "2.37.1", "version": "2.37.7",
"clientVersion": "1.0.6419", "clientVersion": "1.0.6474",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,

View File

@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
ast = this.ast; ast = this.ast;
} }
if (ast.filter(x => x.type != 'hashtag').length == 0) {
return;
}
while (
ast[ast.length - 1].type == 'hashtag' ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n')) {
ast.pop();
}
// Parse ast to DOM // Parse ast to DOM
const els = flatten(ast.map(token => { const els = flatten(ast.map(token => {
switch (token.type) { switch (token.type) {
@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
case 'hashtag': case 'hashtag':
return createElement('a', { return createElement('a', {
attrs: { attrs: {
href: `${url}/search?q=${token.content}`, href: `${url}/tags/${token.content}`,
target: '_blank' target: '_blank'
} }
}, token.content); }, token.content);

View File

@ -1,5 +1,5 @@
<template> <template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
<defs> <defs>
<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0"> <linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop> <stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
@ -14,17 +14,17 @@
:points="polylinePoints" :points="polylinePoints"
fill="none" fill="none"
stroke="#fff" stroke="#fff"
stroke-width="0.7"/> stroke-width="2"/>
<circle <circle
:cx="headX" :cx="headX"
:cy="headY" :cy="headY"
r="1.2" r="3"
fill="#fff"/> fill="#fff"/>
</mask> </mask>
</defs> </defs>
<rect <rect
x="-2" y="-2" x="-10" y="-10"
:width="viewBoxX + 4" :height="viewBoxY + 4" :width="viewBoxX + 20" :height="viewBoxY + 20"
:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/> :style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
</svg> </svg>
</template> </template>
@ -49,7 +49,8 @@ export default Vue.extend({
polylinePoints: '', polylinePoints: '',
polygonPoints: '', polygonPoints: '',
headX: null, headX: null,
headY: null headY: null,
clock: null
}; };
}, },
watch: { watch: {
@ -59,16 +60,26 @@ export default Vue.extend({
}, },
created() { created() {
this.draw(); this.draw();
// Vueが何故かWatchを発動させない場合があるので
this.clock = setInterval(this.draw, 1000);
},
beforeDestroy() {
clearInterval(this.clock);
}, },
methods: { methods: {
draw() { draw() {
const stats = this.src.slice().reverse(); const stats = this.src.slice().reverse();
const peak = Math.max.apply(null, stats) || 1; const peak = Math.max.apply(null, stats) || 1;
const polylinePoints = stats.map((x, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (x / peak)) * this.viewBoxY]); const polylinePoints = stats.map((n, i) => [
i * (this.viewBoxX / (stats.length - 1)),
(1 - (n / peak)) * this.viewBoxY
]);
this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.polygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.headX = polylinePoints[polylinePoints.length - 1][0]; this.headX = polylinePoints[polylinePoints.length - 1][0];
this.headY = polylinePoints[polylinePoints.length - 1][1]; this.headY = polylinePoints[polylinePoints.length - 1][1];

View File

@ -5,14 +5,16 @@
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'"> <div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div v-else> <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
<transition-group v-else tag="div" name="chart">
<div v-for="stat in stats" :key="stat.tag"> <div v-for="stat in stats" :key="stat.tag">
<div class="tag"> <div class="tag">
<router-link :to="`/tags/${ stat.tag }`">#{{ stat.tag }}</router-link> <router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
</div> </div>
<x-chart class="chart" :src="stat.chart"/> <x-chart class="chart" :src="stat.chart"/>
</div> </div>
</div> </transition-group>
</div> </div>
</mk-widget-container> </mk-widget-container>
</div> </div>
@ -40,7 +42,7 @@ export default define({
}, },
mounted() { mounted() {
this.fetch(); this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60 * 10); this.clock = setInterval(this.fetch, 1000 * 60);
}, },
beforeDestroy() { beforeDestroy() {
clearInterval(this.clock); clearInterval(this.clock);
@ -64,6 +66,7 @@ export default define({
root(isDark) root(isDark)
.mkw-hashtags--body .mkw-hashtags--body
> .fetching > .fetching
> .empty
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
@ -73,22 +76,42 @@ root(isDark)
margin-right 4px margin-right 4px
> div > div
.chart-enter
.chart-leave-to
opacity 0
transform translateY(-30px)
> *
transition transform .3s ease, opacity .3s ease
> div > div
display flex display flex
align-items center align-items center
padding 16px padding 14px 16px
&:not(:last-child) &:not(:last-child)
border-bottom solid 1px #393f4f border-bottom solid 1px isDark ? #393f4f : #eee
> .tag > .tag
flex 1 flex 1
overflow hidden
font-size 14px
color isDark ? #9baec8 : #65727b
> a > a
color #9baec8 display block
width 100%
white-space nowrap
overflow hidden
text-overflow ellipsis
color inherit
> p
margin 0
font-size 75%
opacity 0.7
> .chart > .chart
width 50px
height 30px height 30px
.mkw-hashtags[data-darkmode] .mkw-hashtags[data-darkmode]

View File

@ -5,7 +5,7 @@
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq"> <div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
<template v-if="edit"> <template v-if="edit">
<header> <header>
<select v-model="widgetAdderSelected"> <select v-model="widgetAdderSelected" @change="addWidget">
<option value="profile">%i18n:common.widgets.profile%</option> <option value="profile">%i18n:common.widgets.profile%</option>
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option> <option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
<option value="calendar">%i18n:common.widgets.calendar%</option> <option value="calendar">%i18n:common.widgets.calendar%</option>
@ -30,20 +30,15 @@
<option value="nav">%i18n:common.widgets.nav%</option> <option value="nav">%i18n:common.widgets.nav%</option>
<option value="tips">%i18n:common.widgets.tips%</option> <option value="tips">%i18n:common.widgets.tips%</option>
</select> </select>
<button @click="addWidget">%i18n:@add%</button>
</header> </header>
<x-draggable <x-draggable
:list="column.widgets" :list="column.widgets"
:options="{ handle: '.handle', animation: 150 }" :options="{ animation: 150 }"
@sort="onWidgetSort" @sort="onWidgetSort"
> >
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id"> <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
<header> <button class="remove" @click="removeWidget(widget)">%fa:times%</button>
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</header>
<div @click="widgetFunc(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
</div> </div>
</x-draggable> </x-draggable>
</template> </template>
@ -142,6 +137,13 @@ export default Vue.extend({
root(isDark) root(isDark)
.gqpwvtwtprsbmnssnbicggtwqhmylhnq .gqpwvtwtprsbmnssnbicggtwqhmylhnq
> header
padding 16px
> *
width 100%
padding 4px
.widget, .customize-container .widget, .customize-container
margin 8px margin 8px
@ -149,7 +151,21 @@ root(isDark)
margin-top 0 margin-top 0
.customize-container .customize-container
background #fff cursor move
> *:not(.remove)
pointer-events none
> .remove
position absolute
z-index 1
top 8px
right 8px
width 32px
height 32px
color #fff
background rgba(#000, 0.7)
border-radius 4px
> header > header
color isDark ? #fff : #000 color isDark ? #fff : #000

View File

@ -1,13 +1,26 @@
import Note from '../../../../models/note'; import Note from '../../../../models/note';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
*/
const rangeA = 1000 * 60 * 30; // 30分
const rangeB = 1000 * 60 * 120; // 2時間
const coefficient = 1.5; // 「n倍」の部分
const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
const max = 5;
/** /**
* Get trends of hashtags * Get trends of hashtags
*/ */
module.exports = (params, user) => new Promise(async (res, rej) => { module.exports = () => new Promise(async (res, rej) => {
//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
const data = await Note.aggregate([{ const data = await Note.aggregate([{
$match: { $match: {
createdAt: { createdAt: {
$gt: new Date(Date.now() - 1000 * 60 * 60) $gt: new Date(Date.now() - rangeA)
}, },
tags: { tags: {
$exists: true, $exists: true,
@ -18,49 +31,83 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
$unwind: '$tags' $unwind: '$tags'
}, { }, {
$group: { $group: {
_id: '$tags', _id: { tags: '$tags', userId: '$userId' }
count: {
$sum: 1
}
}
}, {
$group: {
_id: null,
tags: {
$push: {
tag: '$_id',
count: '$count'
}
}
}
}, {
$project: {
_id: false,
tags: true
} }
}]) as Array<{ }]) as Array<{
tags: Array<{ _id: {
tag: string; tags: string;
count: number; userId: any;
}> }
}>; }>;
//#endregion
if (data.length == 0) { if (data.length == 0) {
return res([]); return res([]);
} }
const hots = data[0].tags const tags = [];
// カウント
data.map(x => x._id).forEach(x => {
const i = tags.findIndex(tag => tag.name == x.tags);
if (i != -1) {
tags[i].count++;
} else {
tags.push({
name: x.tags,
count: 1
});
}
});
// 最低要求投稿者数を下回るならカットする
const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
//#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
const hotsPromises = limitedTags.map(async tag => {
const passedCount = (await Note.distinct('userId', {
tags: tag.name,
createdAt: {
$lt: new Date(Date.now() - rangeA),
$gt: new Date(Date.now() - rangeB)
}
}) as any).length;
if (tag.count >= (passedCount * coefficient)) {
return tag;
} else {
return null;
}
});
//#endregion
// タグを人気順に並べ替え
let hots = (await Promise.all(hotsPromises))
.filter(x => x != null)
.sort((a, b) => b.count - a.count) .sort((a, b) => b.count - a.count)
.map(tag => tag.tag) .map(tag => tag.name)
.slice(0, 10); .slice(0, max);
const countPromises: Array<Promise<number[]>> = []; //#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
if (hots.length < max) {
hots = hots.concat(tags
.filter(tag => hots.indexOf(tag.name) == -1)
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, max - hots.length));
}
//#endregion
for (let i = 0; i < 10; i++) { //#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
// 10分 const countPromises: Array<Promise<any[]>> = [];
const interval = 1000 * 60 * 10;
countPromises.push(Promise.all(hots.map(tag => Note.count({ const range = 20;
// 10分
const interval = 1000 * 60 * 10;
for (let i = 0; i < range; i++) {
countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', {
tags: tag, tags: tag,
createdAt: { createdAt: {
$lt: new Date(Date.now() - (interval * i)), $lt: new Date(Date.now() - (interval * i)),
@ -71,9 +118,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const countsLog = await Promise.all(countPromises); const countsLog = await Promise.all(countPromises);
const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', {
tags: tag,
createdAt: {
$gt: new Date(Date.now() - (interval * range))
}
})));
//#endregion
const stats = hots.map((tag, i) => ({ const stats = hots.map((tag, i) => ({
tag, tag,
chart: countsLog.map(counts => counts[i]) chart: countsLog.map(counts => counts[i].length),
usersCount: totalCounts[i].length
})); }));
res(stats); res(stats);