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
 | 
			
		||||
                - imgbot
 | 
			
		||||
                - patch-autogen
 | 
			
		||||
      - build:
 | 
			
		||||
      - hold:
 | 
			
		||||
          type: approval
 | 
			
		||||
          filters:
 | 
			
		||||
            branches:
 | 
			
		||||
              ignore:
 | 
			
		||||
                - l10n_develop
 | 
			
		||||
                - imgbot
 | 
			
		||||
                - patch-autogen
 | 
			
		||||
              ignore: master
 | 
			
		||||
      - build:
 | 
			
		||||
          requires:
 | 
			
		||||
            - hold
 | 
			
		||||
      - 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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,6 +1,34 @@
 | 
			
		||||
ChangeLog
 | 
			
		||||
=========
 | 
			
		||||
 | 
			
		||||
10.87.1
 | 
			
		||||
----------
 | 
			
		||||
* ハッシュタグ検索で大文字小文字が区別されてしまう問題を修正
 | 
			
		||||
 | 
			
		||||
10.87.0
 | 
			
		||||
----------
 | 
			
		||||
* ハッシュタグでユーザー検索できるように
 | 
			
		||||
* Exploreページに新規ユーザー一覧を追加
 | 
			
		||||
* デッキ使用中にホーム扱いで開かれた時にタイムラインボタン等がない問題を修正
 | 
			
		||||
* デッキ使用中に / 以外でリロードした際にホームモードになる問題を修正
 | 
			
		||||
 | 
			
		||||
10.86.2
 | 
			
		||||
----------
 | 
			
		||||
* 別タブでルートより下を開いたときにはデッキにしないように
 | 
			
		||||
* 横のナビゲーションバーの改善
 | 
			
		||||
* MIDIファイルがオーディオ扱いになる問題を修正
 | 
			
		||||
* ミュートワードで正規表現を使えるように
 | 
			
		||||
* デッキで無効になったタイムラインに警告を表示するように
 | 
			
		||||
* デザインの調整
 | 
			
		||||
* その他細かな修正
 | 
			
		||||
 | 
			
		||||
10.86.1
 | 
			
		||||
----------
 | 
			
		||||
* ナビゲーションバーの「ホーム」を「タイムライン」に改称
 | 
			
		||||
* モバイル版でユーザーページが二重に描画される問題を修正
 | 
			
		||||
* ユーザー一覧の「もっと読み込む」の動作がおかしい問題を修正
 | 
			
		||||
* デザインの調整
 | 
			
		||||
 | 
			
		||||
10.86.0
 | 
			
		||||
----------
 | 
			
		||||
* Exploreページを実装
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,7 @@ common:
 | 
			
		||||
  drive: "ドライブ"
 | 
			
		||||
  messaging: "トーク"
 | 
			
		||||
  deck: "デッキ"
 | 
			
		||||
  timeline: "タイムライン"
 | 
			
		||||
  explore: "みつける"
 | 
			
		||||
  following: "フォロー中"
 | 
			
		||||
  followers: "フォロワー"
 | 
			
		||||
@@ -219,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: "{}を待っています"
 | 
			
		||||
@@ -1087,7 +1096,6 @@ desktop/views/components/ui.header.account.vue:
 | 
			
		||||
  dark: "闇に飲まれる"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/ui.header.nav.vue:
 | 
			
		||||
  home: "ホーム"
 | 
			
		||||
  game: "ゲーム"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/ui.header.notifications.vue:
 | 
			
		||||
@@ -1823,6 +1831,9 @@ deck:
 | 
			
		||||
  rename: "名前を変更"
 | 
			
		||||
  stack-left: "左に重ねる"
 | 
			
		||||
  pop-right: "右に出す"
 | 
			
		||||
  disabled-timeline:
 | 
			
		||||
    title: "無効化されたタイムライン"
 | 
			
		||||
    description: "サーバーの運営者により、このタイムラインは使用できない状態に設定されています。"
 | 
			
		||||
 | 
			
		||||
deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "10.86.0",
 | 
			
		||||
	"clientVersion": "2.0.14319",
 | 
			
		||||
	"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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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 (
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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}`)];
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -72,7 +72,7 @@ export default Vue.extend({
 | 
			
		||||
		fetchMoreUsers() {
 | 
			
		||||
			this.fetchingMoreUsers = true;
 | 
			
		||||
			this.makePromise(this.cursor).then(x => {
 | 
			
		||||
				this.us = x.users;
 | 
			
		||||
				this.us = this.us.concat(x.users);
 | 
			
		||||
				this.cursor = x.cursor;
 | 
			
		||||
				this.fetchingMoreUsers = false;
 | 
			
		||||
			}, e => {
 | 
			
		||||
@@ -139,4 +139,23 @@ export default Vue.extend({
 | 
			
		||||
				opacity 0.7
 | 
			
		||||
				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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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('/');
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="nav">
 | 
			
		||||
	<ul>
 | 
			
		||||
		<li class="home active" @click="goToTop">
 | 
			
		||||
			<router-link to="/"><fa icon="home"/><p>{{ $t('home') }}</p></router-link>
 | 
			
		||||
		<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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
	</span>
 | 
			
		||||
 | 
			
		||||
	<div>
 | 
			
		||||
		<x-explore/>
 | 
			
		||||
		<x-explore v-bind="$attrs"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</x-column>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
@@ -133,10 +133,10 @@ 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', 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: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [
 | 
			
		||||
				{ 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) },
 | 
			
		||||
			]},
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,9 @@ export default Vue.extend({
 | 
			
		||||
	& + .ukygtjoj
 | 
			
		||||
		margin-top 16px
 | 
			
		||||
 | 
			
		||||
		@media (max-width 500px)
 | 
			
		||||
			margin-top 8px
 | 
			
		||||
 | 
			
		||||
	&.naked
 | 
			
		||||
		background transparent !important
 | 
			
		||||
		box-shadow none !important
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -34,4 +34,10 @@ main
 | 
			
		||||
	margin 0 auto
 | 
			
		||||
	padding 8px
 | 
			
		||||
 | 
			
		||||
	@media (min-width 500px)
 | 
			
		||||
		padding 16px
 | 
			
		||||
 | 
			
		||||
	@media (min-width 600px)
 | 
			
		||||
		padding 32px
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										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 { 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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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