Merge branch 'develop'
This commit is contained in:
		
						commit
						7b44727b23
					
				
					 39 changed files with 556 additions and 260 deletions
				
			
		
							
								
								
									
										16
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -5,6 +5,22 @@ If you encounter any problems with updating, please try the following:
 | 
			
		|||
1. `npm run clean` or `npm run cleanall`
 | 
			
		||||
2. Retry update (Don't forget `npm i`)
 | 
			
		||||
 | 
			
		||||
11.2.0 (2019/04/18)
 | 
			
		||||
-------------------
 | 
			
		||||
### Improvements
 | 
			
		||||
* 検索で日付(日時)を入力するとタイムラインをその時点まで遡るように
 | 
			
		||||
* APIコンソールでエンドポイントをサジェストするように
 | 
			
		||||
* モバイル版でドライブのメニューを使いやすく
 | 
			
		||||
* サイレンス時に確認を表示するように
 | 
			
		||||
* ユーザーメニューでブロックなどの操作を行う時に確認するように
 | 
			
		||||
 | 
			
		||||
### Fixes
 | 
			
		||||
* アプリケーション連携画面でパーミッションが表示されない問題を修正
 | 
			
		||||
* アンケートウィジットでもMFMを使用するように
 | 
			
		||||
* フォローしてないユーザーのホーム投稿がSTLに流れてくる問題を修正
 | 
			
		||||
* モバイル版でウィジェットを設定できない問題を修正
 | 
			
		||||
* スプラッシュがクリックに反応するように
 | 
			
		||||
 | 
			
		||||
11.1.6 (2019/04/18)
 | 
			
		||||
-------------------
 | 
			
		||||
### Fixes
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,7 @@ common:
 | 
			
		|||
  signup: "新規登録"
 | 
			
		||||
  signout: "ログアウト"
 | 
			
		||||
  reload-to-apply-the-setting: "この設定を反映するにはページをリロードする必要があります。今すぐリロードしますか?"
 | 
			
		||||
  fetching-as-ap-object: "連合に照会中"
 | 
			
		||||
 | 
			
		||||
  got-it: "わかった"
 | 
			
		||||
  customization-tips:
 | 
			
		||||
| 
						 | 
				
			
			@ -527,8 +528,12 @@ common/views/components/user-menu.vue:
 | 
			
		|||
  mention: "メンション"
 | 
			
		||||
  mute: "ミュート"
 | 
			
		||||
  unmute: "ミュート解除"
 | 
			
		||||
  mute-confirm: "このユーザーをミュートしますか?"
 | 
			
		||||
  unmute-confirm: "このユーザーをミュート解除しますか?"
 | 
			
		||||
  block: "ブロック"
 | 
			
		||||
  unblock: "ブロック解除"
 | 
			
		||||
  block-confirm: "このユーザーをブロックしますか?"
 | 
			
		||||
  unblock-confirm: "このユーザーをブロック解除しますか?"
 | 
			
		||||
  push-to-list: "リストに追加"
 | 
			
		||||
  select-list: "リストを選択してください"
 | 
			
		||||
  report-abuse: "スパムを報告"
 | 
			
		||||
| 
						 | 
				
			
			@ -536,8 +541,12 @@ common/views/components/user-menu.vue:
 | 
			
		|||
  report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
 | 
			
		||||
  silence: "サイレンス"
 | 
			
		||||
  unsilence: "サイレンス解除"
 | 
			
		||||
  silence-confirm: "このユーザーをサイレンスしますか?"
 | 
			
		||||
  unsilence-confirm: "このユーザーをサイレンス解除しますか?"
 | 
			
		||||
  suspend: "凍結"
 | 
			
		||||
  unsuspend: "凍結解除"
 | 
			
		||||
  suspend-confirm: "このユーザーを凍結しますか?"
 | 
			
		||||
  unsuspend-confirm: "このユーザーを凍結解除しますか?"
 | 
			
		||||
 | 
			
		||||
common/views/components/poll.vue:
 | 
			
		||||
  vote-to: "「{}」に投票する"
 | 
			
		||||
| 
						 | 
				
			
			@ -739,6 +748,10 @@ common/views/components/user-list-editor.vue:
 | 
			
		|||
  delete-are-you-sure: "リスト「$1」を削除しますか?"
 | 
			
		||||
  deleted: "削除しました"
 | 
			
		||||
 | 
			
		||||
common/views/components/user-lists.vue:
 | 
			
		||||
  create-list: "リストを作成"
 | 
			
		||||
  list-name: "リスト名"
 | 
			
		||||
 | 
			
		||||
common/views/widgets/broadcast.vue:
 | 
			
		||||
  fetching: "確認中"
 | 
			
		||||
  no-broadcasts: "お知らせはありません"
 | 
			
		||||
| 
						 | 
				
			
			@ -1145,8 +1158,6 @@ desktop/views/components/received-follow-requests-window.vue:
 | 
			
		|||
 | 
			
		||||
desktop/views/components/user-lists-window.vue:
 | 
			
		||||
  title: "リスト"
 | 
			
		||||
  create-list: "リストを作成"
 | 
			
		||||
  list-name: "リスト名"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/user-preview.vue:
 | 
			
		||||
  notes: "投稿"
 | 
			
		||||
| 
						 | 
				
			
			@ -1336,7 +1347,9 @@ admin/views/users.vue:
 | 
			
		|||
  unsuspend-confirm: "凍結を解除しますか?"
 | 
			
		||||
  unsuspended: "凍結を解除しました"
 | 
			
		||||
  make-silence: "サイレンス"
 | 
			
		||||
  silence-confirm: "サイレンスしますか?"
 | 
			
		||||
  unmake-silence: "サイレンスの解除"
 | 
			
		||||
  unsilence-confirm: "サイレンスを解除しますか?"
 | 
			
		||||
  verify: "公式アカウントにする"
 | 
			
		||||
  verify-confirm: "公式アカウントにしますか?"
 | 
			
		||||
  verified: "公式アカウントにしました"
 | 
			
		||||
| 
						 | 
				
			
			@ -1573,12 +1586,11 @@ mobile/views/components/drive.vue:
 | 
			
		|||
  file-count: "ファイル"
 | 
			
		||||
  nothing-in-drive: "ドライブには何もありません"
 | 
			
		||||
  folder-is-empty: "このフォルダは空です"
 | 
			
		||||
  prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
 | 
			
		||||
  deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
 | 
			
		||||
  folder-name: "フォルダー名"
 | 
			
		||||
  here-is-root: "現在いる場所はルートで、フォルダではありません。"
 | 
			
		||||
  url-prompt: "アップロードしたいファイルのURL"
 | 
			
		||||
  uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
 | 
			
		||||
  folder-name-cannot-empty: "フォルダ名を空白にすることはできません。"
 | 
			
		||||
 | 
			
		||||
mobile/views/components/drive-file-chooser.vue:
 | 
			
		||||
  select-file: "ファイルを選択"
 | 
			
		||||
| 
						 | 
				
			
			@ -1668,9 +1680,17 @@ mobile/views/components/ui.nav.vue:
 | 
			
		|||
  admin: "管理"
 | 
			
		||||
  about: "Misskeyについて"
 | 
			
		||||
 | 
			
		||||
mobile/views/pages/drive.vue:
 | 
			
		||||
  contextmenu:
 | 
			
		||||
    upload: "ファイルをアップロード"
 | 
			
		||||
    url-upload: "ファイルをURLでアップロード"
 | 
			
		||||
    create-folder: "フォルダーを作成"
 | 
			
		||||
    rename-folder: "フォルダー名を変更"
 | 
			
		||||
    move-folder: "このフォルダを移動"
 | 
			
		||||
    delete-folder: "このフォルダを削除"
 | 
			
		||||
 | 
			
		||||
mobile/views/pages/user-lists.vue:
 | 
			
		||||
  title: "リスト"
 | 
			
		||||
  enter-list-name: "リスト名を入力してください"
 | 
			
		||||
 | 
			
		||||
mobile/views/pages/signup.vue:
 | 
			
		||||
  lets-start: "📦 始めましょう"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "11.1.6",
 | 
			
		||||
	"version": "11.2.0",
 | 
			
		||||
	"codename": "daybreak",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -232,6 +232,8 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		async silenceUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('silence-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				await this.$root.api('admin/silence-user', { userId: this.user.id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
| 
						 | 
				
			
			@ -251,6 +253,8 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		async unsilenceUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('unsilence-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				await this.$root.api('admin/unsilence-user', { userId: this.user.id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										64
									
								
								src/client/app/common/scripts/search.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/client/app/common/scripts/search.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
import { faHistory } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
 | 
			
		||||
export async function search(v: any, q: string) {
 | 
			
		||||
	q = q.trim();
 | 
			
		||||
 | 
			
		||||
	if (q.startsWith('@')) {
 | 
			
		||||
		v.$router.push(`/${q}`);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (q.startsWith('#')) {
 | 
			
		||||
		v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// like 2018/03/12
 | 
			
		||||
	if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) {
 | 
			
		||||
		const date = new Date(q.replace(/-/g, '/'));
 | 
			
		||||
 | 
			
		||||
		// 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは
 | 
			
		||||
		// 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので
 | 
			
		||||
		// 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の
 | 
			
		||||
		// 結果になってしまい、2018/03/12 のコンテンツは含まれない)
 | 
			
		||||
		if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) {
 | 
			
		||||
			date.setHours(23, 59, 59, 999);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		v.$root.$emit('warp', date);
 | 
			
		||||
		v.$root.dialog({
 | 
			
		||||
			icon: faHistory,
 | 
			
		||||
			splash: true,
 | 
			
		||||
		});
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (q.startsWith('https://')) {
 | 
			
		||||
		const dialog = v.$root.dialog({
 | 
			
		||||
			type: 'waiting',
 | 
			
		||||
			text: v.$t('@.fetching-as-ap-object'),
 | 
			
		||||
			showOkButton: false,
 | 
			
		||||
			showCancelButton: false,
 | 
			
		||||
			cancelableByBgClick: false
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			const res = await v.$root.api('ap/show', {
 | 
			
		||||
				uri: q
 | 
			
		||||
			});
 | 
			
		||||
			dialog.close();
 | 
			
		||||
			if (res.type == 'User') {
 | 
			
		||||
				v.$router.push(`/@${res.object.username}@${res.object.host}`);
 | 
			
		||||
			} else if (res.type == 'Note') {
 | 
			
		||||
				v.$router.push(`/notes/${res.object.id}`);
 | 
			
		||||
			}
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			dialog.close();
 | 
			
		||||
			// TODO: Show error
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v.$router.push(`/search?q=${encodeURIComponent(q)}`);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,17 @@
 | 
			
		|||
			<mk-signin/>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else>
 | 
			
		||||
			<div class="icon" v-if="!input && !select && !user" :class="type"><fa :icon="icon"/></div>
 | 
			
		||||
			<div class="icon" v-if="icon">
 | 
			
		||||
				<fa :icon="icon"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="icon" v-else-if="!input && !select && !user" :class="type">
 | 
			
		||||
				<fa icon="check" v-if="type === 'success'"/>
 | 
			
		||||
				<fa :icon="faTimesCircle" v-if="type === 'error'"/>
 | 
			
		||||
				<fa icon="exclamation-triangle" v-if="type === 'warning'"/>
 | 
			
		||||
				<fa icon="info-circle" v-if="type === 'info'"/>
 | 
			
		||||
				<fa :icon="faQuestionCircle" v-if="type === 'question'"/>
 | 
			
		||||
				<fa icon="spinner" pulse v-if="type === 'waiting'"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<header v-if="title" v-html="title"></header>
 | 
			
		||||
			<div class="body" v-if="text" v-html="text"></div>
 | 
			
		||||
			<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
 | 
			
		||||
| 
						 | 
				
			
			@ -14,8 +24,8 @@
 | 
			
		|||
			<ui-select v-if="select" v-model="selectedValue" autofocus>
 | 
			
		||||
				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
 | 
			
		||||
			</ui-select>
 | 
			
		||||
			<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
 | 
			
		||||
				<ui-button @click="ok" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
 | 
			
		||||
			<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
 | 
			
		||||
				<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
 | 
			
		||||
				<ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
		</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -55,10 +65,21 @@ export default Vue.extend({
 | 
			
		|||
		user: {
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		icon: {
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		showOkButton: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			default: true
 | 
			
		||||
		},
 | 
			
		||||
		showCancelButton: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
		cancelableByBgClick: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			default: true
 | 
			
		||||
		},
 | 
			
		||||
		splash: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			default: false
 | 
			
		||||
| 
						 | 
				
			
			@ -69,22 +90,11 @@ export default Vue.extend({
 | 
			
		|||
		return {
 | 
			
		||||
			inputValue: this.input && this.input.default ? this.input.default : null,
 | 
			
		||||
			userInputValue: null,
 | 
			
		||||
			selectedValue: null
 | 
			
		||||
			selectedValue: null,
 | 
			
		||||
			faTimesCircle, faQuestionCircle
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		icon(): any {
 | 
			
		||||
			switch (this.type) {
 | 
			
		||||
				case 'success': return 'check';
 | 
			
		||||
				case 'error': return faTimesCircle;
 | 
			
		||||
				case 'warning': return 'exclamation-triangle';
 | 
			
		||||
				case 'info': return 'info-circle';
 | 
			
		||||
				case 'question': return faQuestionCircle;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$nextTick(() => {
 | 
			
		||||
			(this.$refs.bg as any).style.pointerEvents = 'auto';
 | 
			
		||||
| 
						 | 
				
			
			@ -113,6 +123,8 @@ export default Vue.extend({
 | 
			
		|||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async ok() {
 | 
			
		||||
			if (!this.showOkButton) return;
 | 
			
		||||
 | 
			
		||||
			if (this.user) {
 | 
			
		||||
				const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
 | 
			
		||||
				if (user) {
 | 
			
		||||
| 
						 | 
				
			
			@ -156,7 +168,9 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		onBgClick() {
 | 
			
		||||
			if (this.cancelableByBgClick) {
 | 
			
		||||
				this.cancel();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onInputKeydown(e) {
 | 
			
		||||
| 
						 | 
				
			
			@ -183,9 +197,6 @@ export default Vue.extend({
 | 
			
		|||
	height 100%
 | 
			
		||||
 | 
			
		||||
	&.splash
 | 
			
		||||
		&, *
 | 
			
		||||
			pointer-events none !important
 | 
			
		||||
 | 
			
		||||
		> .main
 | 
			
		||||
			min-width 0
 | 
			
		||||
			width initial
 | 
			
		||||
| 
						 | 
				
			
			@ -243,7 +254,7 @@ export default Vue.extend({
 | 
			
		|||
				margin-top 8px
 | 
			
		||||
 | 
			
		||||
		> .body
 | 
			
		||||
			margin 16px 0
 | 
			
		||||
			margin 16px 0 0 0
 | 
			
		||||
 | 
			
		||||
		> .buttons
 | 
			
		||||
			margin-top 16px
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
	<div class="body">
 | 
			
		||||
		<p class="init" v-if="init"><fa icon="spinner .spin"/>{{ $t('@.loading') }}</p>
 | 
			
		||||
		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ $t('empty') }}</p>
 | 
			
		||||
		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa icon="flag"/>{{ $t('no-history') }}</p>
 | 
			
		||||
		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p>
 | 
			
		||||
		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
 | 
			
		||||
			<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }}
 | 
			
		||||
		</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +35,7 @@ import XMessage from './messaging-room.message.vue';
 | 
			
		|||
import XForm from './messaging-room.form.vue';
 | 
			
		||||
import { url } from '../../../config';
 | 
			
		||||
import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faFlag } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('common/views/components/messaging-room.vue'),
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +55,7 @@ export default Vue.extend({
 | 
			
		|||
			connection: null,
 | 
			
		||||
			showIndicator: false,
 | 
			
		||||
			timer: null,
 | 
			
		||||
			faArrowCircleDown
 | 
			
		||||
			faArrowCircleDown, faFlag
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
 | 
			
		||||
	<section>
 | 
			
		||||
		<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
 | 
			
		||||
		<ui-input v-model="endpoint">
 | 
			
		||||
		<ui-input v-model="endpoint" :datalist="endpoints">
 | 
			
		||||
			<span>{{ $t('console.endpoint') }}</span>
 | 
			
		||||
		</ui-input>
 | 
			
		||||
		<ui-textarea v-model="body">
 | 
			
		||||
| 
						 | 
				
			
			@ -39,15 +39,23 @@ import * as JSON5 from 'json5';
 | 
			
		|||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('common/views/components/api-settings.vue'),
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			endpoint: '',
 | 
			
		||||
			body: '{}',
 | 
			
		||||
			res: null,
 | 
			
		||||
			sending: false
 | 
			
		||||
			sending: false,
 | 
			
		||||
			endpoints: []
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$root.api('endpoints').then(endpoints => {
 | 
			
		||||
			this.endpoints = endpoints;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		regenerateToken() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@
 | 
			
		|||
				@focus="focused = true"
 | 
			
		||||
				@blur="focused = false"
 | 
			
		||||
				@keydown="$emit('keydown', $event)"
 | 
			
		||||
				:list="id"
 | 
			
		||||
			>
 | 
			
		||||
			<input v-else ref="input"
 | 
			
		||||
				:type="type"
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +38,11 @@
 | 
			
		|||
				@focus="focused = true"
 | 
			
		||||
				@blur="focused = false"
 | 
			
		||||
				@keydown="$emit('keydown', $event)"
 | 
			
		||||
				:list="id"
 | 
			
		||||
			>
 | 
			
		||||
			<datalist :id="id" v-if="datalist">
 | 
			
		||||
				<option v-for="data in datalist" :value="data"/>
 | 
			
		||||
			</datalist>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else>
 | 
			
		||||
			<input ref="input"
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +135,10 @@ export default Vue.extend({
 | 
			
		|||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
		datalist: {
 | 
			
		||||
			type: Array,
 | 
			
		||||
			required: false,
 | 
			
		||||
		},
 | 
			
		||||
		inline: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +156,8 @@ export default Vue.extend({
 | 
			
		|||
		return {
 | 
			
		||||
			v: this.value,
 | 
			
		||||
			focused: false,
 | 
			
		||||
			passwordStrength: ''
 | 
			
		||||
			passwordStrength: '',
 | 
			
		||||
			id: Math.random().toString()
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										95
									
								
								src/client/app/common/views/components/user-lists.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/client/app/common/views/components/user-lists.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
 | 
			
		||||
	<button class="ui" @click="add">{{ $t('create-list') }}</button>
 | 
			
		||||
	<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('common/views/components/user-lists.vue'),
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			lists: []
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$root.api('users/lists/list').then(lists => {
 | 
			
		||||
			this.fetching = false;
 | 
			
		||||
			this.lists = lists;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		add() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				title: this.$t('list-name'),
 | 
			
		||||
				input: true
 | 
			
		||||
			}).then(async ({ canceled, result: title }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				const list = await this.$root.api('users/lists/create', {
 | 
			
		||||
					title
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				this.lists.push(list)
 | 
			
		||||
				this.$emit('choosen', list);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		choice(list) {
 | 
			
		||||
			this.$emit('choosen', list);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.xkxvokkjlptzyewouewmceqcxhpgzprp
 | 
			
		||||
	padding 16px
 | 
			
		||||
	background: var(--bg)
 | 
			
		||||
 | 
			
		||||
	> button
 | 
			
		||||
		display block
 | 
			
		||||
		margin-bottom 16px
 | 
			
		||||
		color var(--primaryForeground)
 | 
			
		||||
		background var(--primary)
 | 
			
		||||
		width 100%
 | 
			
		||||
		border-radius 38px
 | 
			
		||||
		user-select none
 | 
			
		||||
		cursor pointer
 | 
			
		||||
		padding 0 16px
 | 
			
		||||
		min-width 100px
 | 
			
		||||
		line-height 38px
 | 
			
		||||
		font-size 14px
 | 
			
		||||
		font-weight 700
 | 
			
		||||
 | 
			
		||||
		&:hover
 | 
			
		||||
			background var(--primaryLighten10)
 | 
			
		||||
 | 
			
		||||
		&:active
 | 
			
		||||
			background var(--primaryDarken10)
 | 
			
		||||
 | 
			
		||||
	a
 | 
			
		||||
		display block
 | 
			
		||||
		margin 8px 0
 | 
			
		||||
		padding 8px
 | 
			
		||||
		color var(--text)
 | 
			
		||||
		background var(--face)
 | 
			
		||||
		box-shadow 0 2px 16px var(--reversiListItemShadow)
 | 
			
		||||
		border-radius 6px
 | 
			
		||||
		cursor pointer
 | 
			
		||||
		line-height 32px
 | 
			
		||||
 | 
			
		||||
		*
 | 
			
		||||
			pointer-events none
 | 
			
		||||
			user-select none
 | 
			
		||||
 | 
			
		||||
		&:hover
 | 
			
		||||
			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
 | 
			
		||||
 | 
			
		||||
		&:active
 | 
			
		||||
			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -89,8 +89,10 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleMute() {
 | 
			
		||||
		async toggleMute() {
 | 
			
		||||
			if (this.user.isMuted) {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('unmute-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('mute/delete', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +104,8 @@ export default Vue.extend({
 | 
			
		|||
					});
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('mute-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('mute/create', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,8 +119,10 @@ export default Vue.extend({
 | 
			
		|||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleBlock() {
 | 
			
		||||
		async toggleBlock() {
 | 
			
		||||
			if (this.user.isBlocking) {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('unblock-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('blocking/delete', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +134,8 @@ export default Vue.extend({
 | 
			
		|||
					});
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('block-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('blocking/create', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -164,7 +172,9 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleSilence() {
 | 
			
		||||
		async toggleSilence() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilence-confirm' : 'silence-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
 | 
			
		||||
				userId: this.user.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +191,9 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleSuspend() {
 | 
			
		||||
		async toggleSuspend() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspend-confirm' : 'suspend-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
 | 
			
		||||
				userId: this.user.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -196,7 +208,18 @@ export default Vue.extend({
 | 
			
		|||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async getConfirmed(text: string): Promise<Boolean> {
 | 
			
		||||
			const confirm = await this.$root.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				title: 'confirm',
 | 
			
		||||
				text,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return !confirm.canceled;
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		fetchMore() {
 | 
			
		||||
			if (!this.more || this.moreFetching) return;
 | 
			
		||||
			if (!this.more || this.moreFetching || this.notes.length === 0) return;
 | 
			
		||||
			this.moreFetching = true;
 | 
			
		||||
			this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
 | 
			
		||||
				this.notes = this.notes.concat(x.notes);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,9 +90,8 @@
 | 
			
		|||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import MkUserListsWindow from './user-lists-window.vue';
 | 
			
		||||
import MkUserListWindow from './user-list-window.vue';
 | 
			
		||||
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
 | 
			
		||||
import MkSettingsWindow from './settings-window.vue';
 | 
			
		||||
// import MkSettingsWindow from './settings-window.vue';
 | 
			
		||||
import MkDriveWindow from './drive-window.vue';
 | 
			
		||||
import contains from '../../../common/scripts/contains';
 | 
			
		||||
import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
| 
						 | 
				
			
			@ -143,12 +142,7 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
		list() {
 | 
			
		||||
			this.close();
 | 
			
		||||
			const w = this.$root.new(MkUserListsWindow);
 | 
			
		||||
			w.$once('choosen', list => {
 | 
			
		||||
				this.$root.new(MkUserListWindow, {
 | 
			
		||||
					list
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			this.$root.new(MkUserListsWindow);
 | 
			
		||||
		},
 | 
			
		||||
		followRequests() {
 | 
			
		||||
			this.close();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import { search } from '../../../common/scripts/search';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/ui.header.search.vue'),
 | 
			
		||||
| 
						 | 
				
			
			@ -22,29 +23,11 @@ export default Vue.extend({
 | 
			
		|||
		async onSubmit() {
 | 
			
		||||
			if (this.wait) return;
 | 
			
		||||
 | 
			
		||||
			const q = this.q.trim();
 | 
			
		||||
			if (q.startsWith('@')) {
 | 
			
		||||
				this.$router.push(`/${q}`);
 | 
			
		||||
			} else if (q.startsWith('#')) {
 | 
			
		||||
				this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
 | 
			
		||||
			} else if (q.startsWith('https://')) {
 | 
			
		||||
			this.wait = true;
 | 
			
		||||
				try {
 | 
			
		||||
					const res = await this.$root.api('ap/show', {
 | 
			
		||||
						uri: q
 | 
			
		||||
					});
 | 
			
		||||
					if (res.type == 'User') {
 | 
			
		||||
						this.$router.push(`/@${res.object.username}@${res.object.host}`);
 | 
			
		||||
					} else if (res.type == 'Note') {
 | 
			
		||||
						this.$router.push(`/notes/${res.object.id}`);
 | 
			
		||||
					}
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					// TODO
 | 
			
		||||
				}
 | 
			
		||||
			search(this, this.q).finally(() => {
 | 
			
		||||
				this.wait = false;
 | 
			
		||||
			} else {
 | 
			
		||||
				this.$router.push(`/search?q=${encodeURIComponent(q)}`);
 | 
			
		||||
			}
 | 
			
		||||
				this.q = '';
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -148,10 +148,7 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		list() {
 | 
			
		||||
			const w = this.$root.new(MkUserListsWindow);
 | 
			
		||||
			w.$once('choosen', list => {
 | 
			
		||||
				this.$router.push(`i/lists/${ list.id }`);
 | 
			
		||||
			});
 | 
			
		||||
			this.$root.new(MkUserListsWindow);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		followRequests() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,10 +18,12 @@ export default Vue.extend({
 | 
			
		|||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			connection: null,
 | 
			
		||||
			date: null,
 | 
			
		||||
			makePromise: cursor => this.$root.api('notes/user-list-timeline', {
 | 
			
		||||
				listId: this.list.id,
 | 
			
		||||
				limit: fetchLimit + 1,
 | 
			
		||||
				untilId: cursor ? cursor : undefined,
 | 
			
		||||
				untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
 | 
			
		||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
			
		||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
			
		||||
				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +48,10 @@ export default Vue.extend({
 | 
			
		|||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.init();
 | 
			
		||||
		this.$root.$on('warp', this.warp);
 | 
			
		||||
		this.$once('hook:beforeDestroy', () => {
 | 
			
		||||
			this.$root.$off('warp', this.warp);
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
| 
						 | 
				
			
			@ -68,6 +74,10 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
		onUserRemoved() {
 | 
			
		||||
			(this.$refs.timeline as any).reload();
 | 
			
		||||
		},
 | 
			
		||||
		warp(date) {
 | 
			
		||||
			this.date = date;
 | 
			
		||||
			(this.$refs.timeline as any).reload();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,85 +1,36 @@
 | 
			
		|||
<template>
 | 
			
		||||
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
 | 
			
		||||
	<template #header><fa icon="list"/> {{ $t('title') }}</template>
 | 
			
		||||
 | 
			
		||||
	<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
 | 
			
		||||
		<button class="ui" @click="add">{{ $t('create-list') }}</button>
 | 
			
		||||
		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a>
 | 
			
		||||
	</div>
 | 
			
		||||
	<x-lists :class="$style.content" @choosen="choosen"/>
 | 
			
		||||
</mk-window>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import MkUserListWindow from './user-list-window.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/user-lists-window.vue'),
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			lists: []
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$root.api('users/lists/list').then(lists => {
 | 
			
		||||
			this.fetching = false;
 | 
			
		||||
			this.lists = lists;
 | 
			
		||||
		});
 | 
			
		||||
	components: {
 | 
			
		||||
		XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		add() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				title: this.$t('list-name'),
 | 
			
		||||
				input: true
 | 
			
		||||
			}).then(async ({ canceled, result: title }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				const list = await this.$root.api('users/lists/create', {
 | 
			
		||||
					title
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				this.$emit('choosen', list);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		choice(list) {
 | 
			
		||||
			this.$emit('choosen', list);
 | 
			
		||||
		},
 | 
			
		||||
		close() {
 | 
			
		||||
			(this as any).$refs.window.close();
 | 
			
		||||
		},
 | 
			
		||||
		choosen(list) {
 | 
			
		||||
			this.$root.new(MkUserListWindow, {
 | 
			
		||||
				list
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.xkxvokkjlptzyewouewmceqcxhpgzprp
 | 
			
		||||
	padding 16px
 | 
			
		||||
 | 
			
		||||
	> button
 | 
			
		||||
		display block
 | 
			
		||||
		margin-bottom 16px
 | 
			
		||||
		color var(--primaryForeground)
 | 
			
		||||
		background var(--primary)
 | 
			
		||||
		width 100%
 | 
			
		||||
		border-radius 38px
 | 
			
		||||
		user-select none
 | 
			
		||||
		cursor pointer
 | 
			
		||||
		padding 0 16px
 | 
			
		||||
		min-width 100px
 | 
			
		||||
		line-height 38px
 | 
			
		||||
		font-size 14px
 | 
			
		||||
		font-weight 700
 | 
			
		||||
 | 
			
		||||
		&:hover
 | 
			
		||||
			background var(--primaryLighten10)
 | 
			
		||||
 | 
			
		||||
		&:active
 | 
			
		||||
			background var(--primaryDarken10)
 | 
			
		||||
 | 
			
		||||
	> a
 | 
			
		||||
		display block
 | 
			
		||||
		padding 16px
 | 
			
		||||
		border solid 1px var(--faceDivider)
 | 
			
		||||
		border-radius 4px
 | 
			
		||||
<style lang="stylus" module>
 | 
			
		||||
.content
 | 
			
		||||
	height 100%
 | 
			
		||||
	overflow auto
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,6 +53,12 @@ export default Vue.extend({
 | 
			
		|||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$root.$on('warp', this.warp);
 | 
			
		||||
		this.$once('hook:beforeDestroy', () => {
 | 
			
		||||
			this.$root.$off('warp', this.warp);
 | 
			
		||||
			this.connection.dispose();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const prepend = note => {
 | 
			
		||||
			(this.$refs.timeline as any).prepend(note);
 | 
			
		||||
		};
 | 
			
		||||
| 
						 | 
				
			
			@ -124,13 +130,14 @@ export default Vue.extend({
 | 
			
		|||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		focus() {
 | 
			
		||||
			(this.$refs.timeline as any).focus();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		warp(date) {
 | 
			
		||||
			this.date = date;
 | 
			
		||||
			(this.$refs.timeline as any).reload();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,7 +36,8 @@ export default Vue.extend({
 | 
			
		|||
				includeReplies: this.mode == 'with-replies',
 | 
			
		||||
				includeMyRenotes: this.mode != 'my-posts',
 | 
			
		||||
				withFiles: this.mode == 'with-media',
 | 
			
		||||
				untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
 | 
			
		||||
				untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
 | 
			
		||||
				untilId: cursor ? cursor : undefined
 | 
			
		||||
			}).then(notes => {
 | 
			
		||||
				if (notes.length == fetchLimit + 1) {
 | 
			
		||||
					notes.pop();
 | 
			
		||||
| 
						 | 
				
			
			@ -62,10 +63,11 @@ export default Vue.extend({
 | 
			
		|||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		document.addEventListener('keydown', this.onDocumentKeydown);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.$root.$on('warp', this.warp);
 | 
			
		||||
		this.$once('hook:beforeDestroy', () => {
 | 
			
		||||
			this.$root.$off('warp', this.warp);
 | 
			
		||||
			document.removeEventListener('keydown', this.onDocumentKeydown);
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,9 @@
 | 
			
		|||
 | 
			
		||||
		<div class="mkw-polls--body">
 | 
			
		||||
			<div class="poll" v-if="!fetching && poll != null">
 | 
			
		||||
				<p v-if="poll.text"><router-link :to="poll | notePage">{{ poll.text }}</router-link></p>
 | 
			
		||||
				<p v-if="poll.text"><router-link :to="poll | notePage">
 | 
			
		||||
					<mfm :text="poll.text" :author="poll.user" :custom-emojis="poll.emojis"/>
 | 
			
		||||
				</router-link></p>
 | 
			
		||||
				<p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p>
 | 
			
		||||
				<mk-poll :note="poll"/>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ export default define({
 | 
			
		|||
}).extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		chosen(date) {
 | 
			
		||||
			this.$emit('chosen', date);
 | 
			
		||||
			this.$root.$emit('warp', date);
 | 
			
		||||
		},
 | 
			
		||||
		func() {
 | 
			
		||||
			if (this.props.design == 5) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -458,10 +458,14 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
 | 
			
		|||
					},
 | 
			
		||||
					dialog(opts) {
 | 
			
		||||
						const vm = this.new(Dialog, opts);
 | 
			
		||||
						return new Promise((res) => {
 | 
			
		||||
						const p: any = new Promise((res) => {
 | 
			
		||||
							vm.$once('ok', result => res({ canceled: false, result }));
 | 
			
		||||
							vm.$once('cancel', () => res({ canceled: true }));
 | 
			
		||||
						});
 | 
			
		||||
						p.close = () => {
 | 
			
		||||
							vm.close();
 | 
			
		||||
						};
 | 
			
		||||
						return p;
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				router,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -379,44 +379,31 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		openContextMenu() {
 | 
			
		||||
			const fn = window.prompt(this.$t('prompt'));
 | 
			
		||||
			if (fn == null || fn == '') return;
 | 
			
		||||
			switch (fn) {
 | 
			
		||||
				case '1':
 | 
			
		||||
					this.selectLocalFile();
 | 
			
		||||
					break;
 | 
			
		||||
				case '2':
 | 
			
		||||
					this.urlUpload();
 | 
			
		||||
					break;
 | 
			
		||||
				case '3':
 | 
			
		||||
					this.createFolder();
 | 
			
		||||
					break;
 | 
			
		||||
				case '4':
 | 
			
		||||
					this.renameFolder();
 | 
			
		||||
					break;
 | 
			
		||||
				case '5':
 | 
			
		||||
					this.moveFolder();
 | 
			
		||||
					break;
 | 
			
		||||
				case '6':
 | 
			
		||||
					this.deleteFolder();
 | 
			
		||||
					break;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		selectLocalFile() {
 | 
			
		||||
			(this.$refs.file as any).click();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		createFolder() {
 | 
			
		||||
			const name = window.prompt(this.$t('folder-name'));
 | 
			
		||||
			if (name == null || name == '') return;
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				title: this.$t('folder-name')
 | 
			
		||||
				input: {
 | 
			
		||||
					default: this.folder.name
 | 
			
		||||
				}
 | 
			
		||||
			}).then(({ result: name }) => {
 | 
			
		||||
				if (!name) {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: this.$t('folder-name-cannot-empty')
 | 
			
		||||
					});
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				this.$root.api('drive/folders/create', {
 | 
			
		||||
					name: name,
 | 
			
		||||
					parentId: this.folder ? this.folder.id : undefined
 | 
			
		||||
				}).then(folder => {
 | 
			
		||||
					this.addFolder(folder, true);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		renameFolder() {
 | 
			
		||||
| 
						 | 
				
			
			@ -427,14 +414,26 @@ export default Vue.extend({
 | 
			
		|||
				});
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			const name = window.prompt(this.$t('folder-name'), this.folder.name);
 | 
			
		||||
			if (name == null || name == '') return;
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				title: this.$t('folder-name')
 | 
			
		||||
				input: {
 | 
			
		||||
					default: this.folder.name
 | 
			
		||||
				}
 | 
			
		||||
			}).then(({ result: name }) => {
 | 
			
		||||
				if (!name) {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: this.$t('cannot-empty')
 | 
			
		||||
					});
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				this.$root.api('drive/folders/update', {
 | 
			
		||||
					name: name,
 | 
			
		||||
					folderId: this.folder.id
 | 
			
		||||
				}).then(folder => {
 | 
			
		||||
					this.cd(folder);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		moveFolder() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,7 +124,7 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		fetchMore() {
 | 
			
		||||
			if (!this.more || this.moreFetching) return;
 | 
			
		||||
			if (!this.more || this.moreFetching || this.notes.length === 0) return;
 | 
			
		||||
			this.moreFetching = true;
 | 
			
		||||
			this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
 | 
			
		||||
				this.notes = this.notes.concat(x.notes);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,6 +66,7 @@ import i18n from '../../../i18n';
 | 
			
		|||
import { lang } from '../../../config';
 | 
			
		||||
import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { search } from '../../../common/scripts/search';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('mobile/views/components/ui.nav.vue'),
 | 
			
		||||
| 
						 | 
				
			
			@ -133,29 +134,10 @@ export default Vue.extend({
 | 
			
		|||
			}).then(async ({ canceled, result: query }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
 | 
			
		||||
				const q = query.trim();
 | 
			
		||||
				if (q.startsWith('@')) {
 | 
			
		||||
					this.$router.push(`/${q}`);
 | 
			
		||||
				} else if (q.startsWith('#')) {
 | 
			
		||||
					this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
 | 
			
		||||
				} else if (q.startsWith('https://')) {
 | 
			
		||||
				this.searching = true;
 | 
			
		||||
					try {
 | 
			
		||||
						const res = await this.$root.api('ap/show', {
 | 
			
		||||
							uri: q
 | 
			
		||||
						});
 | 
			
		||||
						if (res.type == 'User') {
 | 
			
		||||
							this.$router.push(`/@${res.object.username}@${res.object.host}`);
 | 
			
		||||
						} else if (res.type == 'Note') {
 | 
			
		||||
							this.$router.push(`/notes/${res.object.id}`);
 | 
			
		||||
						}
 | 
			
		||||
					} catch (e) {
 | 
			
		||||
						// TODO
 | 
			
		||||
					}
 | 
			
		||||
				search(this, query).finally(() => {
 | 
			
		||||
					this.searching = false;
 | 
			
		||||
				} else {
 | 
			
		||||
					this.$router.push(`/search?q=${encodeURIComponent(q)}`);
 | 
			
		||||
				}
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,10 +15,12 @@ export default Vue.extend({
 | 
			
		|||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			connection: null,
 | 
			
		||||
			date: null,
 | 
			
		||||
			makePromise: cursor => this.$root.api('notes/user-list-timeline', {
 | 
			
		||||
				listId: this.list.id,
 | 
			
		||||
				limit: fetchLimit + 1,
 | 
			
		||||
				untilId: cursor ? cursor : undefined,
 | 
			
		||||
				untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
 | 
			
		||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
			
		||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
			
		||||
				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +47,11 @@ export default Vue.extend({
 | 
			
		|||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.init();
 | 
			
		||||
 | 
			
		||||
		this.$root.$on('warp', this.warp);
 | 
			
		||||
		this.$once('hook:beforeDestroy', () => {
 | 
			
		||||
			this.$root.$off('warp', this.warp);
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +80,11 @@ export default Vue.extend({
 | 
			
		|||
 | 
			
		||||
		onUserRemoved() {
 | 
			
		||||
			(this.$refs.timeline as any).reload();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		warp(date) {
 | 
			
		||||
			this.date = date;
 | 
			
		||||
			(this.$refs.timeline as any).reload();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,11 +17,13 @@ export default Vue.extend({
 | 
			
		|||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			date: null,
 | 
			
		||||
			makePromise: cursor => this.$root.api('users/notes', {
 | 
			
		||||
				userId: this.user.id,
 | 
			
		||||
				limit: fetchLimit + 1,
 | 
			
		||||
				withFiles: this.withMedia,
 | 
			
		||||
				untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
 | 
			
		||||
				untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
 | 
			
		||||
				untilId: cursor ? cursor : undefined
 | 
			
		||||
			}).then(notes => {
 | 
			
		||||
				if (notes.length == fetchLimit + 1) {
 | 
			
		||||
					notes.pop();
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +39,20 @@ export default Vue.extend({
 | 
			
		|||
				}
 | 
			
		||||
			})
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$root.$on('warp', this.warp);
 | 
			
		||||
		this.$once('hook:beforeDestroy', () => {
 | 
			
		||||
			this.$root.$off('warp', this.warp);
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		warp(date) {
 | 
			
		||||
			this.date = date;
 | 
			
		||||
			(this.$refs.timeline as any).reload();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
		<template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
 | 
			
		||||
		<template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template>
 | 
			
		||||
	</template>
 | 
			
		||||
	<template #func><button @click="fn"><fa icon="ellipsis-h"/></button></template>
 | 
			
		||||
	<template #func v-if="folder || (!folder && !file)"><button @click="openContextMenu" ref="contextSource"><fa icon="ellipsis-h"/></button></template>
 | 
			
		||||
	<x-drive
 | 
			
		||||
		ref="browser"
 | 
			
		||||
		:init-folder="initFolder"
 | 
			
		||||
| 
						 | 
				
			
			@ -26,9 +26,12 @@
 | 
			
		|||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import Progress from '../../../common/scripts/loading';
 | 
			
		||||
import XMenu from '../../../common/views/components/menu.vue';
 | 
			
		||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n(),
 | 
			
		||||
	i18n: i18n('mobile/views/pages/drive.vue'),
 | 
			
		||||
	components: {
 | 
			
		||||
		XDrive: () => import('../components/drive.vue').then(m => m.default),
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			@ -63,9 +66,6 @@ export default Vue.extend({
 | 
			
		|||
				(this.$refs as any).browser.goRoot(true);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		fn() {
 | 
			
		||||
			(this.$refs as any).browser.openContextMenu();
 | 
			
		||||
		},
 | 
			
		||||
		onMoveRoot(silent) {
 | 
			
		||||
			const title = `${this.$root.instanceName} Drive`;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -104,6 +104,42 @@ export default Vue.extend({
 | 
			
		|||
 | 
			
		||||
			this.file = file;
 | 
			
		||||
			this.folder = null;
 | 
			
		||||
		},
 | 
			
		||||
		openContextMenu() {
 | 
			
		||||
			this.$root.new(XMenu, {
 | 
			
		||||
				items: [{
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: this.$t('contextmenu.upload'),
 | 
			
		||||
					icon: 'upload',
 | 
			
		||||
					action: this.$refs.browser.selectLocalFile
 | 
			
		||||
				}, {
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: this.$t('contextmenu.url-upload'),
 | 
			
		||||
					icon: faCloudUploadAlt,
 | 
			
		||||
					action: this.$refs.browser.urlUpload
 | 
			
		||||
				}, {
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: this.$t('contextmenu.create-folder'),
 | 
			
		||||
					icon: ['far', 'folder'],
 | 
			
		||||
					action: this.$refs.browser.createFolder
 | 
			
		||||
				}, ...(this.folder ? [{
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: this.$t('contextmenu.rename-folder'),
 | 
			
		||||
					icon: 'i-cursor',
 | 
			
		||||
					action: this.$refs.browser.renameFolder
 | 
			
		||||
				}, {
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: this.$t('contextmenu.move-folder'),
 | 
			
		||||
					icon: ['far', 'folder-open'],
 | 
			
		||||
					action: this.$refs.browser.moveFolder
 | 
			
		||||
				}, {
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: this.$t('contextmenu.delete-folder'),
 | 
			
		||||
					icon: faTrashAlt,
 | 
			
		||||
					action: this.$refs.browser.deleteFolder
 | 
			
		||||
				}] : [])],
 | 
			
		||||
				source: this.$refs.contextSource,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,12 @@ export default Vue.extend({
 | 
			
		|||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$root.$on('warp', this.warp);
 | 
			
		||||
		this.$once('hook:beforeDestroy', () => {
 | 
			
		||||
			this.$root.$off('warp', this.warp);
 | 
			
		||||
			this.connection.dispose();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const prepend = note => {
 | 
			
		||||
			(this.$refs.timeline as any).prepend(note);
 | 
			
		||||
		};
 | 
			
		||||
| 
						 | 
				
			
			@ -125,10 +131,6 @@ export default Vue.extend({
 | 
			
		|||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		focus() {
 | 
			
		||||
			(this.$refs.timeline as any).focus();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,15 @@
 | 
			
		|||
<template>
 | 
			
		||||
<mk-ui>
 | 
			
		||||
	<template #header><fa icon="list"/>{{ $t('title') }}</template>
 | 
			
		||||
	<template #func><button @click="fn"><fa icon="plus"/></button></template>
 | 
			
		||||
	<template #func><button @click="$refs.lists.add()"><fa icon="plus"/></button></template>
 | 
			
		||||
 | 
			
		||||
	<main>
 | 
			
		||||
		<ul>
 | 
			
		||||
			<li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li>
 | 
			
		||||
		</ul>
 | 
			
		||||
	</main>
 | 
			
		||||
	<x-lists ref="lists" @choosen="choosen"/>
 | 
			
		||||
</mk-ui>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import Progress from '../../../common/scripts/loading';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('mobile/views/pages/user-lists.vue'),
 | 
			
		||||
| 
						 | 
				
			
			@ -24,31 +19,16 @@ export default Vue.extend({
 | 
			
		|||
			lists: []
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	components: {
 | 
			
		||||
		XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		document.title = this.$t('title');
 | 
			
		||||
 | 
			
		||||
		Progress.start();
 | 
			
		||||
 | 
			
		||||
		this.$root.api('users/lists/list').then(lists => {
 | 
			
		||||
			this.fetching = false;
 | 
			
		||||
			this.lists = lists;
 | 
			
		||||
 | 
			
		||||
			Progress.done();
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		fn() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				title: this.$t('enter-list-name'),
 | 
			
		||||
				input: true
 | 
			
		||||
			}).then(async ({ canceled, result: title }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				const list = await this.$root.api('users/lists/create', {
 | 
			
		||||
					title
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
		choosen(list) {
 | 
			
		||||
			if (!list) return;
 | 
			
		||||
			this.$router.push(`/i/lists/${list.id}`);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,13 +72,13 @@ export default Vue.extend({
 | 
			
		|||
 | 
			
		||||
	computed: {
 | 
			
		||||
		widgets(): any[] {
 | 
			
		||||
			return this.$store.state.settings.mobileHome;
 | 
			
		||||
			return this.$store.state.device.mobileHome;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.widgets.length == 0) {
 | 
			
		||||
			this.widgets = [{
 | 
			
		||||
			this.$store.commit('device/setMobileHome', [{
 | 
			
		||||
				name: 'calendar',
 | 
			
		||||
				id: 'a', data: {}
 | 
			
		||||
			}, {
 | 
			
		||||
| 
						 | 
				
			
			@ -96,8 +96,7 @@ export default Vue.extend({
 | 
			
		|||
			}, {
 | 
			
		||||
				name: 'version',
 | 
			
		||||
				id: 'g', data: {}
 | 
			
		||||
			}];
 | 
			
		||||
			this.saveHome();
 | 
			
		||||
			}]);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +122,7 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		addWidget() {
 | 
			
		||||
			this.$store.commit('settings/addMobileHomeWidget', {
 | 
			
		||||
			this.$store.commit('device/addMobileHomeWidget', {
 | 
			
		||||
				name: this.widgetAdderSelected,
 | 
			
		||||
				id: uuid(),
 | 
			
		||||
				data: {}
 | 
			
		||||
| 
						 | 
				
			
			@ -131,11 +130,11 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		removeWidget(widget) {
 | 
			
		||||
			this.$store.commit('settings/removeMobileHomeWidget', widget);
 | 
			
		||||
			this.$store.commit('device/removeMobileHomeWidget', widget);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		saveHome() {
 | 
			
		||||
			this.$store.commit('settings/setMobileHome', this.widgets);
 | 
			
		||||
			this.$store.commit('device/setMobileHome', this.widgets);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -358,7 +358,7 @@ export default (os: MiOS) => new Vuex.Store({
 | 
			
		|||
					ctx.commit('set', x);
 | 
			
		||||
 | 
			
		||||
					if (ctx.rootGetters.isSignedIn) {
 | 
			
		||||
						os.api('i/update_client_setting', {
 | 
			
		||||
						os.api('i/update-client-setting', {
 | 
			
		||||
							name: x.key,
 | 
			
		||||
							value: x.value
 | 
			
		||||
						});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ export class AppRepository extends Repository<App> {
 | 
			
		|||
			id: app.id,
 | 
			
		||||
			name: app.name,
 | 
			
		||||
			callbackUrl: app.callbackUrl,
 | 
			
		||||
			permission: app.permission,
 | 
			
		||||
			...(opts.includeSecret ? { secret: app.secret } : {}),
 | 
			
		||||
			...(me ? {
 | 
			
		||||
				isAuthorized: await AccessTokens.count({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										15
									
								
								src/server/api/endpoints/endpoints.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/server/api/endpoints/endpoints.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
import define from '../define';
 | 
			
		||||
import endpoints from '../endpoints';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	requireCredential: false,
 | 
			
		||||
 | 
			
		||||
	tags: ['meta'],
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default define(meta, async () => {
 | 
			
		||||
	return endpoints.map(x => x.name);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +17,8 @@ export const meta = {
 | 
			
		|||
 | 
			
		||||
	tags: ['notes'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		limit: {
 | 
			
		||||
			validator: $.optional.num.range(1, 100),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -142,7 +142,7 @@ export default define(meta, async (ps, me) => {
 | 
			
		|||
	});
 | 
			
		||||
 | 
			
		||||
	//#region Construct query
 | 
			
		||||
	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 | 
			
		||||
	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
 | 
			
		||||
		.andWhere('note.userId = :userId', { userId: user.id })
 | 
			
		||||
		.leftJoinAndSelect('note.user', 'user');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,11 +20,11 @@ export default class extends Channel {
 | 
			
		|||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async onNote(note: any) {
 | 
			
		||||
		// 自分自身の投稿 または その投稿のユーザーをフォローしている または ローカルの投稿 の場合だけ
 | 
			
		||||
		// 自分自身の投稿 または その投稿のユーザーをフォローしている または 全体公開のローカルの投稿 の場合だけ
 | 
			
		||||
		if (!(
 | 
			
		||||
			this.user!.id === note.userId ||
 | 
			
		||||
			this.following.includes(note.userId) ||
 | 
			
		||||
			note.user.host == null
 | 
			
		||||
			(note.user.host == null && note.visibility === 'public')
 | 
			
		||||
		)) return;
 | 
			
		||||
 | 
			
		||||
		if (['followers', 'specified'].includes(note.visibility)) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ describe('Streaming', () => {
 | 
			
		|||
		p.on('message', message => {
 | 
			
		||||
			if (message === 'ok') {
 | 
			
		||||
				(p.channel as any).onread = () => {};
 | 
			
		||||
				initDb(true).then(async connection => {
 | 
			
		||||
				initDb(true).then(async (connection: any) => {
 | 
			
		||||
					Followings = connection.getRepository(Following);
 | 
			
		||||
					done();
 | 
			
		||||
				});
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ describe('Streaming', () => {
 | 
			
		|||
		p.kill();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const follow = async (follower, followee) => {
 | 
			
		||||
	const follow = async (follower: any, followee: any) => {
 | 
			
		||||
		await Followings.save({
 | 
			
		||||
			id: 'a',
 | 
			
		||||
			createdAt: new Date(),
 | 
			
		||||
| 
						 | 
				
			
			@ -484,6 +484,56 @@ describe('Streaming', () => {
 | 
			
		|||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		it('フォローしているユーザーのホーム投稿が流れる', () => new Promise(async done => {
 | 
			
		||||
			const alice = await signup({ username: 'alice' });
 | 
			
		||||
			const bob = await signup({ username: 'bob' });
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
 | 
			
		||||
				if (type == 'note') {
 | 
			
		||||
					assert.deepStrictEqual(body.userId, bob.id);
 | 
			
		||||
					assert.deepStrictEqual(body.text, 'foo');
 | 
			
		||||
					ws.close();
 | 
			
		||||
					done();
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// ホーム投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'home'
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		it('フォローしていないローカルユーザーのホーム投稿は流れない', () => new Promise(async done => {
 | 
			
		||||
			const alice = await signup({ username: 'alice' });
 | 
			
		||||
			const bob = await signup({ username: 'bob' });
 | 
			
		||||
 | 
			
		||||
			let fired = false;
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
 | 
			
		||||
				if (type == 'note') {
 | 
			
		||||
					fired = true;
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// ホーム投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'home'
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
				assert.strictEqual(fired, false);
 | 
			
		||||
				ws.close();
 | 
			
		||||
				done();
 | 
			
		||||
			}, 3000);
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
 | 
			
		||||
			const alice = await signup({ username: 'alice' });
 | 
			
		||||
			const bob = await signup({ username: 'bob' });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,7 +76,7 @@ export const uploadFile = (user: any, path?: string): Promise<any> => new Promis
 | 
			
		|||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function connectStream(user: any, channel: string, listener: any, params?: any): Promise<WebSocket> {
 | 
			
		||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
 | 
			
		||||
	return new Promise((res, rej) => {
 | 
			
		||||
		const ws = new WebSocket(`ws://localhost/streaming?i=${user.token}`);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue