Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ef1205f8b | ||
|
|
e8db63e788 | ||
|
|
0bcef2453c | ||
|
|
b9f549135c | ||
|
|
87b0017386 | ||
|
|
cc8ff556d4 | ||
|
|
021f74da54 | ||
|
|
f9389802d7 | ||
|
|
18dd172c97 | ||
|
|
1d5a54ff6f | ||
|
|
03e2c7eec6 | ||
|
|
0902727d1c | ||
|
|
496895634d | ||
|
|
9414e9e258 | ||
|
|
357528d139 | ||
|
|
c4efbdf4c7 | ||
|
|
fb4a921cd9 | ||
|
|
683b242215 | ||
|
|
a5660d6c82 | ||
|
|
f632ec50c1 | ||
|
|
a55d15214b | ||
|
|
f1709a2cc2 | ||
|
|
effa542958 | ||
|
|
e8bf742c87 | ||
|
|
2e6652edce | ||
|
|
230c204b48 | ||
|
|
3755c600b1 | ||
|
|
24513fc0a3 | ||
|
|
0a79a6564a | ||
|
|
562bb5842b | ||
|
|
ec3ca3032e | ||
|
|
890770c275 | ||
|
|
9ed58a1b4e | ||
|
|
08984be2fe | ||
|
|
e3ade148ca | ||
|
|
34c0eff89f | ||
|
|
40aba47a47 | ||
|
|
6736f51134 | ||
|
|
9d826d6e52 | ||
|
|
902d9bc7a5 | ||
|
|
b6c86e2845 | ||
|
|
34dffdfc8f |
@@ -141,39 +141,28 @@ workflows:
|
|||||||
- l10n_develop
|
- l10n_develop
|
||||||
- imgbot
|
- imgbot
|
||||||
- patch-autogen
|
- patch-autogen
|
||||||
- build:
|
- hold:
|
||||||
|
type: approval
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore:
|
ignore: master
|
||||||
- l10n_develop
|
- build:
|
||||||
- imgbot
|
requires:
|
||||||
- patch-autogen
|
- hold
|
||||||
- test:
|
- test:
|
||||||
executor: with-redis
|
executor: with-redis
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore:
|
|
||||||
# - master
|
|
||||||
- l10n_develop
|
|
||||||
- imgbot
|
|
||||||
- patch-autogen
|
|
||||||
- test:
|
- test:
|
||||||
without_redis: true
|
without_redis: true
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
filters:
|
|
||||||
# branches:
|
|
||||||
# only: master
|
|
||||||
branches:
|
|
||||||
ignore:
|
|
||||||
# - master
|
|
||||||
- l10n_develop
|
|
||||||
- imgbot
|
|
||||||
- patch-autogen
|
|
||||||
docker:
|
docker:
|
||||||
jobs:
|
jobs:
|
||||||
|
- ok:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: master
|
||||||
- hold:
|
- hold:
|
||||||
type: approval
|
type: approval
|
||||||
filters:
|
filters:
|
||||||
|
|||||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,6 +1,34 @@
|
|||||||
ChangeLog
|
ChangeLog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
10.87.1
|
||||||
|
----------
|
||||||
|
* ハッシュタグ検索で大文字小文字が区別されてしまう問題を修正
|
||||||
|
|
||||||
|
10.87.0
|
||||||
|
----------
|
||||||
|
* ハッシュタグでユーザー検索できるように
|
||||||
|
* Exploreページに新規ユーザー一覧を追加
|
||||||
|
* デッキ使用中にホーム扱いで開かれた時にタイムラインボタン等がない問題を修正
|
||||||
|
* デッキ使用中に / 以外でリロードした際にホームモードになる問題を修正
|
||||||
|
|
||||||
|
10.86.2
|
||||||
|
----------
|
||||||
|
* 別タブでルートより下を開いたときにはデッキにしないように
|
||||||
|
* 横のナビゲーションバーの改善
|
||||||
|
* MIDIファイルがオーディオ扱いになる問題を修正
|
||||||
|
* ミュートワードで正規表現を使えるように
|
||||||
|
* デッキで無効になったタイムラインに警告を表示するように
|
||||||
|
* デザインの調整
|
||||||
|
* その他細かな修正
|
||||||
|
|
||||||
|
10.86.1
|
||||||
|
----------
|
||||||
|
* ナビゲーションバーの「ホーム」を「タイムライン」に改称
|
||||||
|
* モバイル版でユーザーページが二重に描画される問題を修正
|
||||||
|
* ユーザー一覧の「もっと読み込む」の動作がおかしい問題を修正
|
||||||
|
* デザインの調整
|
||||||
|
|
||||||
10.86.0
|
10.86.0
|
||||||
----------
|
----------
|
||||||
* Exploreページを実装
|
* Exploreページを実装
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ FROM base AS runner
|
|||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
tini
|
tini
|
||||||
|
RUN npm i -g web-push
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
|
||||||
COPY --from=builder /misskey/node_modules ./node_modules
|
COPY --from=builder /misskey/node_modules ./node_modules
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ common:
|
|||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
messaging: "トーク"
|
messaging: "トーク"
|
||||||
deck: "デッキ"
|
deck: "デッキ"
|
||||||
|
timeline: "タイムライン"
|
||||||
explore: "みつける"
|
explore: "みつける"
|
||||||
following: "フォロー中"
|
following: "フォロー中"
|
||||||
followers: "フォロワー"
|
followers: "フォロワー"
|
||||||
@@ -219,6 +220,14 @@ auth/views/index.vue:
|
|||||||
error: "セッションが存在しません。"
|
error: "セッションが存在しません。"
|
||||||
sign-in: "サインインしてください"
|
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:
|
common/views/components/games/reversi/reversi.vue:
|
||||||
matching:
|
matching:
|
||||||
waiting-for: "{}を待っています"
|
waiting-for: "{}を待っています"
|
||||||
@@ -1087,7 +1096,6 @@ desktop/views/components/ui.header.account.vue:
|
|||||||
dark: "闇に飲まれる"
|
dark: "闇に飲まれる"
|
||||||
|
|
||||||
desktop/views/components/ui.header.nav.vue:
|
desktop/views/components/ui.header.nav.vue:
|
||||||
home: "ホーム"
|
|
||||||
game: "ゲーム"
|
game: "ゲーム"
|
||||||
|
|
||||||
desktop/views/components/ui.header.notifications.vue:
|
desktop/views/components/ui.header.notifications.vue:
|
||||||
@@ -1823,6 +1831,9 @@ deck:
|
|||||||
rename: "名前を変更"
|
rename: "名前を変更"
|
||||||
stack-left: "左に重ねる"
|
stack-left: "左に重ねる"
|
||||||
pop-right: "右に出す"
|
pop-right: "右に出す"
|
||||||
|
disabled-timeline:
|
||||||
|
title: "無効化されたタイムライン"
|
||||||
|
description: "サーバーの運営者により、このタイムラインは使用できない状態に設定されています。"
|
||||||
|
|
||||||
deck/deck.tl-column.vue:
|
deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "10.86.0",
|
"version": "10.87.1",
|
||||||
"clientVersion": "2.0.14319",
|
"clientVersion": "2.0.14358",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"@types/mkdirp": "0.5.2",
|
"@types/mkdirp": "0.5.2",
|
||||||
"@types/mocha": "5.2.5",
|
"@types/mocha": "5.2.5",
|
||||||
"@types/mongodb": "3.1.19",
|
"@types/mongodb": "3.1.19",
|
||||||
"@types/node": "10.12.21",
|
"@types/node": "10.12.24",
|
||||||
"@types/nodemailer": "4.6.5",
|
"@types/nodemailer": "4.6.5",
|
||||||
"@types/nprogress": "0.0.29",
|
"@types/nprogress": "0.0.29",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"@types/request-stats": "3.0.0",
|
"@types/request-stats": "3.0.0",
|
||||||
"@types/rimraf": "2.0.2",
|
"@types/rimraf": "2.0.2",
|
||||||
"@types/seedrandom": "2.4.27",
|
"@types/seedrandom": "2.4.27",
|
||||||
"@types/sharp": "0.21.1",
|
"@types/sharp": "0.21.2",
|
||||||
"@types/showdown": "1.9.2",
|
"@types/showdown": "1.9.2",
|
||||||
"@types/speakeasy": "2.0.3",
|
"@types/speakeasy": "2.0.3",
|
||||||
"@types/systeminformation": "3.23.1",
|
"@types/systeminformation": "3.23.1",
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
"commander": "2.19.0",
|
"commander": "2.19.0",
|
||||||
"crc-32": "1.2.0",
|
"crc-32": "1.2.0",
|
||||||
"css-loader": "2.1.0",
|
"css-loader": "2.1.0",
|
||||||
"cssnano": "4.1.8",
|
"cssnano": "4.1.10",
|
||||||
"dateformat": "3.0.3",
|
"dateformat": "3.0.3",
|
||||||
"deep-equal": "1.0.1",
|
"deep-equal": "1.0.1",
|
||||||
"deepcopy": "0.6.3",
|
"deepcopy": "0.6.3",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ export default function(me, settings, note) {
|
|||||||
|
|
||||||
const includesMutedWords = (text: string) =>
|
const includesMutedWords = (text: string) =>
|
||||||
text
|
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;
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<b>{{ $t('sensitive') }}</b>
|
<b>{{ $t('sensitive') }}</b>
|
||||||
<span>{{ $t('click-to-show') }}</span>
|
<span>{{ $t('click-to-show') }}</span>
|
||||||
</div>
|
</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"
|
<audio class="audio"
|
||||||
:src="media.url"
|
:src="media.url"
|
||||||
:title="media.name"
|
:title="media.name"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -72,7 +72,7 @@ export default Vue.extend({
|
|||||||
fetchMoreUsers() {
|
fetchMoreUsers() {
|
||||||
this.fetchingMoreUsers = true;
|
this.fetchingMoreUsers = true;
|
||||||
this.makePromise(this.cursor).then(x => {
|
this.makePromise(this.cursor).then(x => {
|
||||||
this.us = x.users;
|
this.us = this.us.concat(x.users);
|
||||||
this.cursor = x.cursor;
|
this.cursor = x.cursor;
|
||||||
this.fetchingMoreUsers = false;
|
this.fetchingMoreUsers = false;
|
||||||
}, e => {
|
}, e => {
|
||||||
@@ -139,4 +139,23 @@ export default Vue.extend({
|
|||||||
opacity 0.7
|
opacity 0.7
|
||||||
font-size 14px
|
font-size 14px
|
||||||
|
|
||||||
|
> .more
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
padding 16px
|
||||||
|
color var(--text)
|
||||||
|
border-top solid var(--lineWidth) rgba(#000, 0.05)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background rgba(#000, 0.025)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
background rgba(#000, 0.05)
|
||||||
|
|
||||||
|
&.fetching
|
||||||
|
cursor wait
|
||||||
|
|
||||||
|
> [data-icon]
|
||||||
|
margin-right 4px
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,26 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<mk-user-list :make-promise="verifiedUsers">
|
<mk-user-list v-if="tag != null" :make-promise="tagUsers" :key="`${tag}-local`">
|
||||||
<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}-remote`">
|
||||||
<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>
|
||||||
|
|
||||||
|
<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 } 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', {
|
||||||
@@ -40,11 +67,55 @@ export default Vue.extend({
|
|||||||
sort: '+updatedAt',
|
sort: '+updatedAt',
|
||||||
limit: 10
|
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>
|
</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>
|
||||||
|
|||||||
@@ -124,11 +124,17 @@ init(async (launch, os) => {
|
|||||||
require('./views/components');
|
require('./views/components');
|
||||||
require('./views/widgets');
|
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
|
// Init router
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
routes: [
|
routes: [
|
||||||
os.store.getters.isSignedIn && os.store.state.device.deckMode
|
os.store.state.device.inDeckMode
|
||||||
? { path: '/', name: 'index', component: MkDeck, children: [
|
? { path: '/', name: 'index', component: MkDeck, children: [
|
||||||
{ path: '/@:user', name: 'user', component: () => import('./views/deck/deck.user-column.vue').then(m => m.default), 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) },
|
{ 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: '/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: '/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: '/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', name: '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', 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: '/i/favorites', component: () => import('./views/deck/deck.favorites-column.vue').then(m => m.default) }
|
||||||
]}
|
]}
|
||||||
: { path: '/', component: MkHome, children: [
|
: { 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: '/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: '/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: '/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: '/featured', name: '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: '/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 },
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<li @click="toggleDeckMode">
|
<li @click="toggleDeckMode">
|
||||||
<p>
|
<p>
|
||||||
<span>{{ $t('@.deck') }}</span>
|
<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>
|
<template v-else><i><fa :icon="faColumns"/></i></template>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@@ -165,8 +165,8 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleDeckMode() {
|
toggleDeckMode() {
|
||||||
this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.deckMode });
|
this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode });
|
||||||
location.reload();
|
location.replace('/');
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="home active" @click="goToTop">
|
<li class="timeline" :class="{ active: $route.name == 'index' }" @click="goToTop">
|
||||||
<router-link to="/"><fa icon="home"/><p>{{ $t('home') }}</p></router-link>
|
<router-link to="/"><fa icon="home"/><p>{{ $t('@.timeline') }}</p></router-link>
|
||||||
</li>
|
</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>
|
<router-link to="/featured"><fa :icon="faNewspaper"/><p>{{ $t('@.featured-notes') }}</p></router-link>
|
||||||
</li>
|
</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>
|
<router-link to="/explore"><fa :icon="faHashtag"/><p>{{ $t('@.explore') }}</p></router-link>
|
||||||
</li>
|
</li>
|
||||||
<li class="game">
|
<li class="game">
|
||||||
|
|||||||
@@ -6,24 +6,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav" v-if="$store.getters.isSignedIn">
|
<div class="nav" v-if="$store.getters.isSignedIn">
|
||||||
<template v-if="$store.state.device.deckMode">
|
<div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop">
|
||||||
<div class="deck active" @click="goToTop">
|
<router-link to="/"><fa icon="home"/></router-link>
|
||||||
<router-link to="/"><fa icon="columns"/></router-link>
|
</div>
|
||||||
</div>
|
<div class="featured" :class="{ active: $route.name == 'featured' }">
|
||||||
<div class="home">
|
<router-link to="/featured"><fa :icon="faNewspaper"/></router-link>
|
||||||
<a @click="toggleDeckMode(false)"><fa icon="home"/></a>
|
</div>
|
||||||
</div>
|
<div class="explore" :class="{ active: $route.name == 'explore' }">
|
||||||
</template>
|
<router-link to="/explore"><fa :icon="faHashtag"/></router-link>
|
||||||
<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>
|
</div>
|
||||||
<div class="game">
|
<div class="game">
|
||||||
<a @click="game"><fa icon="gamepad"/><template v-if="hasGameInvitations"><fa icon="circle"/></template></a>
|
<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 }">
|
<div ref="notificationsButton" :class="{ active: showNotifications }">
|
||||||
<a @click="notifications"><fa :icon="['far', 'bell']"/></a>
|
<a @click="notifications"><fa :icon="['far', 'bell']"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="messaging">
|
||||||
|
<a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a @click="settings"><fa icon="cog"/></a>
|
<a @click="settings"><fa icon="cog"/></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="signout">
|
||||||
|
<a @click="signout"><fa icon="power-off"/></a>
|
||||||
<div class="account">
|
</div>
|
||||||
<router-link :to="`/@${ $store.state.i.username }`">
|
<div>
|
||||||
<mk-avatar class="avatar" :user="$store.state.i"/>
|
<router-link to="/i/favorites"><fa icon="star"/></router-link>
|
||||||
</router-link>
|
</div>
|
||||||
|
<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
|
||||||
<div class="nav menu">
|
<a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a>
|
||||||
<div class="signout">
|
</div>
|
||||||
<a @click="signout"><fa icon="power-off"/></a>
|
<div class="account">
|
||||||
</div>
|
<router-link :to="`/@${ $store.state.i.username }`">
|
||||||
<div>
|
<mk-avatar class="avatar" :user="$store.state.i"/>
|
||||||
<router-link to="/i/favorites"><fa icon="star"/></router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
|
<div>
|
||||||
<a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a>
|
<template v-if="$store.state.device.inDeckMode">
|
||||||
</div>
|
<a @click="toggleDeckMode(false)"><fa icon="home"/></a>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a @click="toggleDeckMode(true)"><fa icon="columns"/></a>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav dark">
|
|
||||||
<div>
|
<div>
|
||||||
<a @click="dark"><template v-if="$store.state.device.darkmode"><fa icon="moon"/></template><template v-else><fa :icon="['far', 'moon']"/></template></a>
|
<a @click="dark"><template v-if="$store.state.device.darkmode"><fa icon="moon"/></template><template v-else><fa :icon="['far', 'moon']"/></template></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,6 +79,7 @@ import MkDriveWindow from './drive-window.vue';
|
|||||||
import MkMessagingWindow from './messaging-window.vue';
|
import MkMessagingWindow from './messaging-window.vue';
|
||||||
import MkGameWindow from './game-window.vue';
|
import MkGameWindow from './game-window.vue';
|
||||||
import contains from '../../../common/scripts/contains';
|
import contains from '../../../common/scripts/contains';
|
||||||
|
import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('desktop/views/components/ui.sidebar.vue'),
|
i18n: i18n('desktop/views/components/ui.sidebar.vue'),
|
||||||
@@ -92,7 +87,8 @@ export default Vue.extend({
|
|||||||
return {
|
return {
|
||||||
hasGameInvitations: false,
|
hasGameInvitations: false,
|
||||||
connection: null,
|
connection: null,
|
||||||
showNotifications: false
|
showNotifications: false,
|
||||||
|
faNewspaper, faHashtag
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -124,7 +120,7 @@ export default Vue.extend({
|
|||||||
methods: {
|
methods: {
|
||||||
toggleDeckMode(deck) {
|
toggleDeckMode(deck) {
|
||||||
this.$store.commit('device/set', { key: 'deckMode', value: deck });
|
this.$store.commit('device/set', { key: 'deckMode', value: deck });
|
||||||
location.reload();
|
location.replace('/');
|
||||||
},
|
},
|
||||||
|
|
||||||
onReversiInvited() {
|
onReversiInvited() {
|
||||||
@@ -278,44 +274,23 @@ export default Vue.extend({
|
|||||||
|
|
||||||
> .nav.bottom
|
> .nav.bottom
|
||||||
position absolute
|
position absolute
|
||||||
bottom 128px
|
bottom 0
|
||||||
left 0
|
left 0
|
||||||
|
|
||||||
> .account
|
> .account
|
||||||
position absolute
|
width $width
|
||||||
bottom 64px
|
height $width
|
||||||
left 0
|
padding 14px
|
||||||
width $width
|
|
||||||
height $width
|
|
||||||
padding 14px
|
|
||||||
|
|
||||||
> .menu
|
> *
|
||||||
display none
|
|
||||||
position absolute
|
|
||||||
bottom 64px
|
|
||||||
left 0
|
|
||||||
background var(--desktopHeaderBg)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
> .menu
|
|
||||||
display block
|
display block
|
||||||
|
|
||||||
> *:not(.menu)
|
|
||||||
display block
|
|
||||||
width 100%
|
|
||||||
height 100%
|
|
||||||
|
|
||||||
> .avatar
|
|
||||||
pointer-events none
|
|
||||||
width 100%
|
width 100%
|
||||||
height 100%
|
height 100%
|
||||||
|
|
||||||
> .dark
|
> .avatar
|
||||||
position absolute
|
pointer-events none
|
||||||
bottom 0
|
width 100%
|
||||||
left 0
|
height 100%
|
||||||
width $width
|
|
||||||
height $width
|
|
||||||
|
|
||||||
> .notifications
|
> .notifications
|
||||||
position fixed
|
position fixed
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-explore/>
|
<x-explore v-bind="$attrs"/>
|
||||||
</div>
|
</div>
|
||||||
</x-column>
|
</x-column>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default Vue.extend({
|
|||||||
this.$root.api('notes/featured', {
|
this.$root.api('notes/featured', {
|
||||||
limit: 20,
|
limit: 20,
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
|
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||||
res(notes);
|
res(notes);
|
||||||
this.fetching = false;
|
this.fetching = false;
|
||||||
this.$emit('loaded');
|
this.$emit('loaded');
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import XNotes from './deck.notes.vue';
|
import XNotes from './deck.notes.vue';
|
||||||
|
import { faMinusCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import i18n from '../../../i18n';
|
||||||
|
|
||||||
const fetchLimit = 10;
|
const fetchLimit = 10;
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
i18n: i18n('deck'),
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
XNotes
|
XNotes
|
||||||
},
|
},
|
||||||
@@ -36,7 +47,9 @@ export default Vue.extend({
|
|||||||
fetching: true,
|
fetching: true,
|
||||||
moreFetching: false,
|
moreFetching: false,
|
||||||
existMore: 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.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();
|
this.fetch();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -149,3 +168,16 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.iwaalbte
|
||||||
|
color var(--text)
|
||||||
|
text-align center
|
||||||
|
|
||||||
|
> p
|
||||||
|
margin 16px
|
||||||
|
|
||||||
|
&.desc
|
||||||
|
font-size 14px
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default Vue.extend({
|
|||||||
this.$root.api('notes/featured', {
|
this.$root.api('notes/featured', {
|
||||||
limit: 20
|
limit: 20
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
|
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||||
this.notes = notes;
|
this.notes = notes;
|
||||||
this.fetching = false;
|
this.fetching = false;
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
</ui-container>
|
</ui-container>
|
||||||
</div>
|
</div>
|
||||||
<x-photos :user="user"/>
|
<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"/>
|
<x-timeline class="timeline" ref="tl" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,8 +21,6 @@ import parseAcct from '../../../../../../misc/acct/parse';
|
|||||||
import Progress from '../../../../common/scripts/loading';
|
import Progress from '../../../../common/scripts/loading';
|
||||||
import XTimeline from './user.timeline.vue';
|
import XTimeline from './user.timeline.vue';
|
||||||
import XPhotos from './user.photos.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 XIntegrations from './user.integrations.vue';
|
||||||
import XActivity from '../../../../common/views/components/activity.vue';
|
import XActivity from '../../../../common/views/components/activity.vue';
|
||||||
|
|
||||||
@@ -33,8 +29,6 @@ export default Vue.extend({
|
|||||||
components: {
|
components: {
|
||||||
XTimeline,
|
XTimeline,
|
||||||
XPhotos,
|
XPhotos,
|
||||||
XFollowersYouKnow,
|
|
||||||
XFriends,
|
|
||||||
XIntegrations,
|
XIntegrations,
|
||||||
XActivity
|
XActivity
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ 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', 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: [
|
||||||
{ path: '', name: 'user', component: () => import('./views/pages/user/home.vue').then(m => m.default) },
|
|
||||||
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
|
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
|
||||||
{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
|
{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
|
||||||
]},
|
]},
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export default Vue.extend({
|
|||||||
& + .ukygtjoj
|
& + .ukygtjoj
|
||||||
margin-top 16px
|
margin-top 16px
|
||||||
|
|
||||||
|
@media (max-width 500px)
|
||||||
|
margin-top 8px
|
||||||
|
|
||||||
&.naked
|
&.naked
|
||||||
background transparent !important
|
background transparent !important
|
||||||
box-shadow none !important
|
box-shadow none !important
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<span slot="header"><span style="margin-right:4px;"><fa :icon="faHashtag"/></span>{{ $t('@.explore') }}</span>
|
<span slot="header"><span style="margin-right:4px;"><fa :icon="faHashtag"/></span>{{ $t('@.explore') }}</span>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<x-explore/>
|
<x-explore v-bind="$attrs"/>
|
||||||
</main>
|
</main>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
@@ -34,4 +34,10 @@ main
|
|||||||
margin 0 auto
|
margin 0 auto
|
||||||
padding 8px
|
padding 8px
|
||||||
|
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
@media (min-width 600px)
|
||||||
|
padding 32px
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default Vue.extend({
|
|||||||
this.$root.api('notes/featured', {
|
this.$root.api('notes/featured', {
|
||||||
limit: 20
|
limit: 20
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
|
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||||
this.notes = notes;
|
this.notes = notes;
|
||||||
this.fetching = false;
|
this.fetching = false;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type INote = {
|
|||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
renoteCount: number;
|
renoteCount: number;
|
||||||
repliesCount: number;
|
repliesCount: number;
|
||||||
reactionCounts: any;
|
reactionCounts: Record<string, number>;
|
||||||
mentions: mongo.ObjectID[];
|
mentions: mongo.ObjectID[];
|
||||||
mentionedRemoteUsers: {
|
mentionedRemoteUsers: {
|
||||||
uri: string;
|
uri: string;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
1
src/prelude/symbol.ts
Normal file
1
src/prelude/symbol.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const fallback = Symbol('fallback');
|
||||||
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,7 +143,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
|||||||
|
|
||||||
const { fields, services } = analyzeAttachments(person.attachment);
|
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';
|
const isBot = object.type == 'Service';
|
||||||
|
|
||||||
@@ -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,
|
||||||
@@ -338,7 +343,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
|||||||
|
|
||||||
const { fields, services } = analyzeAttachments(person.attachment);
|
const { fields, services } = analyzeAttachments(person.attachment);
|
||||||
|
|
||||||
const tags = extractHashtags(person.tag);
|
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import File, { packMany } from '../../../../../models/drive-file';
|
import File, { packMany } from '../../../../../models/drive-file';
|
||||||
import define from '../../../define';
|
import define from '../../../define';
|
||||||
|
import { fallback } from '../../../../../prelude/symbol';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
@@ -37,32 +38,15 @@ export const meta = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
|
||||||
let _sort;
|
'+createdAt': { uploadDate: -1 },
|
||||||
if (ps.sort) {
|
'-createdAt': { uploadDate: 1 },
|
||||||
if (ps.sort == '+createdAt') {
|
'+size': { length: -1 },
|
||||||
_sort = {
|
'-size': { length: 1 },
|
||||||
uploadDate: -1
|
[fallback]: { _id: -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
const q = {
|
const q = {
|
||||||
'metadata.deletedAt': { $exists: false },
|
'metadata.deletedAt': { $exists: false },
|
||||||
} as any;
|
} as any;
|
||||||
@@ -73,7 +57,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
const files = await File
|
const files = await File
|
||||||
.find(q, {
|
.find(q, {
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
sort: _sort,
|
sort: sort[ps.sort] || sort[fallback],
|
||||||
skip: ps.offset
|
skip: ps.offset
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import User, { pack } from '../../../../models/user';
|
import User, { pack } from '../../../../models/user';
|
||||||
import define from '../../define';
|
import define from '../../define';
|
||||||
|
import { fallback } from '../../../../prelude/symbol';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
@@ -52,40 +53,17 @@ export const meta = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
|
||||||
let _sort;
|
'+follower': { followersCount: -1 },
|
||||||
if (ps.sort) {
|
'-follower': { followersCount: 1 },
|
||||||
if (ps.sort == '+follower') {
|
'+createdAt': { createdAt: -1 },
|
||||||
_sort = {
|
'-createdAt': { createdAt: 1 },
|
||||||
followersCount: -1
|
'+updatedAt': { updatedAt: -1 },
|
||||||
};
|
'-updatedAt': { updatedAt: 1 },
|
||||||
} else if (ps.sort == '-follower') {
|
[fallback]: { _id: -1 }
|
||||||
_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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
const q = {
|
const q = {
|
||||||
$and: []
|
$and: []
|
||||||
} as any;
|
} as any;
|
||||||
@@ -117,7 +95,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
const users = await User
|
const users = await User
|
||||||
.find(q, {
|
.find(q, {
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
sort: _sort,
|
sort: sort[ps.sort] || sort[fallback],
|
||||||
skip: ps.offset
|
skip: ps.offset
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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: {
|
||||||
@@ -216,11 +217,15 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
|
|||||||
if (updates.description != null) {
|
if (updates.description != null) {
|
||||||
const tokens = parse(updates.description);
|
const tokens = parse(updates.description);
|
||||||
emojis = emojis.concat(extractEmojis(tokens));
|
emojis = emojis.concat(extractEmojis(tokens));
|
||||||
tags = extractHashtags(tokens);
|
tags = extractHashtags(tokens).map(tag => tag.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import User, { pack } from '../../../models/user';
|
import User, { pack } from '../../../models/user';
|
||||||
import define from '../define';
|
import define from '../define';
|
||||||
|
import { fallback } from '../../../prelude/symbol';
|
||||||
|
|
||||||
|
const nonnull = { $ne: null as any };
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: false,
|
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) => {
|
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
|
const users = await User
|
||||||
.find(q, {
|
.find({
|
||||||
|
$and: [
|
||||||
|
state[ps.state] || state[fallback],
|
||||||
|
origin[ps.origin] || origin[fallback]
|
||||||
|
]
|
||||||
|
}, {
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
sort: _sort,
|
sort: sort[ps.sort] || sort[fallback],
|
||||||
skip: ps.offset
|
skip: ps.offset
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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) {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user