From ea8e6d88aba5b163cf843736fa77dd398418e031 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Wed, 20 Oct 2021 03:10:36 +0900
Subject: [PATCH 1/2] =?UTF-8?q?enhance:=20share=E3=83=9A=E3=83=BC=E3=82=B8?=
 =?UTF-8?q?=E3=81=A7=E3=82=88=E3=82=8A=E5=A4=9A=E3=81=8F=E3=81=AE=E6=83=85?=
 =?UTF-8?q?=E5=A0=B1=E3=82=92=E6=B8=A1=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB=20(#7606)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* shareでより多くの情報を渡せるように

* from chat ui post-form, remove instant and add share

* fix await eating array, make document

* add changelog

* https://github.com/misskey-dev/misskey/pull/7606/files/3581bf9a060742dc59bf7fb8ea7316809cc60522#r692265037

* reply, renoteにも型定義

* :art:

* 閉じなければ100ms後タイムラインに
---
 CHANGELOG.md                          |   1 +
 src/client/components/post-form.vue   |  38 ++++++-
 src/client/pages/share.vue            | 158 ++++++++++++++++++++++----
 src/client/ui/chat/post-form.vue      |   6 +-
 src/docs/ja-JP/advanced/share-page.md |  56 +++++++++
 src/misc/acct.ts                      |   9 +-
 6 files changed, 233 insertions(+), 35 deletions(-)
 create mode 100644 src/docs/ja-JP/advanced/share-page.md

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a3d9a1caa..10b1cdb37d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -131,6 +131,7 @@
 - ワードミュートのドキュメントを追加
 - クライアントのデザインの調整
 - 依存関係の更新
+- /share のクエリでリプライやファイル等の情報を渡せるように
 
 ### Bugfixes
 - チャンネルを作成しているとアカウントを削除できないのを修正
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index a1d89d2a2e..816a69e731 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -117,11 +117,28 @@ export default defineComponent({
 			type: String,
 			required: false
 		},
+		initialVisibility: {
+			type: String,
+			required: false
+		},
+		initialFiles: {
+			type: Array,
+			required: false
+		},
+		initialLocalOnly: {
+			type: Boolean,
+			required: false
+		},
+		visibleUsers: {
+			type: Array,
+			required: false,
+			default: () => []
+		},
 		initialNote: {
 			type: Object,
 			required: false
 		},
-		instant: {
+		share: {
 			type: Boolean,
 			required: false,
 			default: false
@@ -150,8 +167,7 @@ export default defineComponent({
 			showPreview: false,
 			cw: null,
 			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
-			visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
-			visibleUsers: [],
+			visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
 			autocomplete: null,
 			draghover: false,
 			quoteId: null,
@@ -246,6 +262,18 @@ export default defineComponent({
 			this.text = this.initialText;
 		}
 
+		if (this.initialVisibility) {
+			this.visibility = this.initialVisibility;
+		}
+
+		if (this.initialFiles) {
+			this.files = this.initialFiles;
+		}
+
+		if (typeof this.initialLocalOnly === 'boolean') {
+			this.localOnly = this.initialLocalOnly;
+		}
+
 		if (this.mention) {
 			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
 			this.text += ' ';
@@ -321,7 +349,7 @@ export default defineComponent({
 
 		this.$nextTick(() => {
 			// 書きかけの投稿を復元
-			if (!this.instant && !this.mention && !this.specified) {
+			if (!this.share && !this.mention && !this.specified) {
 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
 				if (draft) {
 					this.text = draft.data.text;
@@ -582,8 +610,6 @@ export default defineComponent({
 		},
 
 		saveDraft() {
-			if (this.instant) return;
-
 			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
 
 			data[this.draftKey] = {
diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue
index 67e598fa8f..70a9661dd0 100644
--- a/src/client/pages/share.vue
+++ b/src/client/pages/share.vue
@@ -1,22 +1,38 @@
 <template>
 <div class="">
 	<section class="_section">
-		<div class="_title" v-if="title">{{ title }}</div>
 		<div class="_content">
-			<XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/>
-			<MkButton v-else primary @click="close()">{{ $ts.close }}</MkButton>
+			<XPostForm
+				v-if="state === 'writing'"
+				fixed
+				:share="true"
+				:initial-text="initialText"
+				:initial-visibility="visibility"
+				:initial-files="files"
+				:initial-local-only="localOnly"
+				:reply="reply"
+				:renote="renote"
+				:visible-users="visibleUsers"
+				@posted="state = 'posted'"
+				class="_panel"
+			/>
+			<MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton>
 		</div>
-		<div class="_footer" v-if="url">{{ url }}</div>
 	</section>
 </div>
 </template>
 
 <script lang="ts">
+// SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md
+
 import { defineComponent } from 'vue';
 import MkButton from '@client/components/ui/button.vue';
 import XPostForm from '@client/components/post-form.vue';
 import * as os from '@client/os';
+import { noteVisibilities } from '@/types';
+import { parseAcct } from '@/misc/acct';
 import * as symbols from '@client/symbols';
+import * as Misskey from 'misskey-js';
 
 export default defineComponent({
 	components: {
@@ -30,35 +46,139 @@ export default defineComponent({
 				title: this.$ts.share,
 				icon: 'fas fa-share-alt'
 			},
-			title: null,
-			text: null,
-			url: null,
-			initialText: null,
-			posted: false,
+			state: 'fetching' as 'fetching' | 'writing' | 'posted',
 
+			title: null as string | null,
+			initialText: null as string | null,
+			reply: null as Misskey.entities.Note | null,
+			renote: null as Misskey.entities.Note | null,
+			visibility: null as string | null,
+			localOnly: null as boolean | null,
+			files: [] as Misskey.entities.DriveFile[],
+			visibleUsers: [] as Misskey.entities.User[],
 		}
 	},
 
-	created() {
+	async created() {
 		const urlParams = new URLSearchParams(window.location.search);
+
 		this.title = urlParams.get('title');
-		this.text = urlParams.get('text');
-		this.url = urlParams.get('url');
-		
-		let text = '';
-		if (this.title) text += `【${this.title}】\n`;
-		if (this.text) text += `${this.text}\n`;
-		if (this.url) text += `${this.url}`;
-		this.initialText = text.trim();
+		const text = urlParams.get('text');
+		const url = urlParams.get('url');
+
+		let noteText = '';
+		if (this.title) noteText += `[ ${this.title} ]\n`;
+		// Googleニュース対策
+		if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, '');
+		else if (text && this.title !== text) noteText += `${text}\n`;
+		if (url) noteText += `${url}`;
+		this.initialText = noteText.trim();
+
+		const visibility = urlParams.get('visibility');
+		if (noteVisibilities.includes(visibility)) {
+			this.visibility = visibility;
+		}
+
+		if (this.visibility === 'specified') {
+			const visibleUserIds = urlParams.get('visibleUserIds');
+			const visibleAccts = urlParams.get('visibleAccts');
+			await Promise.all(
+				[
+					...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
+					...(visibleAccts ? visibleAccts.split(',').map(parseAcct) : [])
+				]
+				// TypeScriptの指示通りに変換する
+				.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
+				.map(q => os.api('users/show', q)
+					.then(user => {
+						this.visibleUsers.push(user);
+					}, () => {
+						console.error(`Invalid user query: ${JSON.stringify(q)}`);
+					})
+				)
+			);
+		}
+
+		const localOnly = urlParams.get('localOnly');
+		if (localOnly === '0') this.localOnly = false;
+		else if (localOnly === '1') this.localOnly = true;
+
+		try {
+			//#region Reply
+			const replyId = urlParams.get('replyId');
+			const replyUri = urlParams.get('replyUri');
+			if (replyId) {
+				this.reply = await os.api('notes/show', {
+					noteId: replyId
+				});
+			} else if (replyUri) {
+				const obj = await os.api('ap/show', {
+					uri: replyUri
+				});
+				if (obj.type === 'Note') {
+					this.reply = obj.object;
+				}
+			}
+			//#endregion
+
+			//#region Renote
+			const renoteId = urlParams.get('renoteId');
+			const renoteUri = urlParams.get('renoteUri');
+			if (renoteId) {
+				this.renote = await os.api('notes/show', {
+					noteId: renoteId
+				});
+			} else if (renoteUri) {
+				const obj = await os.api('ap/show', {
+					uri: renoteUri
+				});
+				if (obj.type === 'Note') {
+					this.renote = obj.object;
+				}
+			}
+			//#endregion
+
+			//#region Drive files
+			const fileIds = urlParams.get('fileIds');
+			if (fileIds) {
+				await Promise.all(
+					fileIds.split(',')
+					.map(fileId => os.api('drive/files/show', { fileId })
+						.then(file => {
+							this.files.push(file);
+						}, () => {
+							console.error(`Failed to fetch a file ${fileId}`);
+						})
+					)
+				);
+			}
+			//#endregion
+		} catch (e) {
+			os.dialog({
+				type: 'error',
+				title: e.message,
+				text: e.name
+			});
+		}
+
+		this.state = 'writing';
 	},
 
 	methods: {
 		close() {
-			window.close()
+			window.close();
+
+			// 閉じなければ100ms後タイムラインに
+			setTimeout(() => {
+				this.$router.push('/');
+			}, 100);
 		}
 	}
 });
 </script>
 
 <style lang="scss" scoped>
+.close {
+	margin: 16px auto;
+}
 </style>
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
index 0cacaf77e7..64b8d08cbc 100644
--- a/src/client/ui/chat/post-form.vue
+++ b/src/client/ui/chat/post-form.vue
@@ -100,7 +100,7 @@ export default defineComponent({
 			type: Object,
 			required: false
 		},
-		instant: {
+		share: {
 			type: Boolean,
 			required: false,
 			default: false
@@ -277,7 +277,7 @@ export default defineComponent({
 
 		this.$nextTick(() => {
 			// 書きかけの投稿を復元
-			if (!this.instant && !this.mention && !this.specified) {
+			if (!this.share && !this.mention && !this.specified) {
 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
 				if (draft) {
 					this.text = draft.data.text;
@@ -507,8 +507,6 @@ export default defineComponent({
 		},
 
 		saveDraft() {
-			if (this.instant) return;
-
 			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
 
 			data[this.draftKey] = {
diff --git a/src/docs/ja-JP/advanced/share-page.md b/src/docs/ja-JP/advanced/share-page.md
new file mode 100644
index 0000000000..75a9d14d29
--- /dev/null
+++ b/src/docs/ja-JP/advanced/share-page.md
@@ -0,0 +1,56 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。
+ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/misc/acct.ts b/src/misc/acct.ts
index 16876c4429..5106b1a09e 100644
--- a/src/misc/acct.ts
+++ b/src/misc/acct.ts
@@ -1,13 +1,10 @@
-export type Acct = {
-	username: string;
-	host: string | null;
-};
+import * as Misskey from 'misskey-js';
 
-export const getAcct = (user: Acct) => {
+export const getAcct = (user: Misskey.Acct) => {
 	return user.host == null ? user.username : `${user.username}@${user.host}`;
 };
 
-export const parseAcct = (acct: string): Acct => {
+export const parseAcct = (acct: string): Misskey.Acct => {
 	if (acct.startsWith('@')) acct = acct.substr(1);
 	const split = acct.split('@', 2);
 	return { username: split[0], host: split[1] || null };

From a4e31366119d2d115ab951f74e508ccf73197306 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 20 Oct 2021 03:12:20 +0900
Subject: [PATCH 2/2] Update CHANGELOG.md

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10b1cdb37d..7a58015f4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 - API: ユーザーのリアクション一覧を取得する users/reactions を追加
 - API: users/search および users/search-by-username-and-host を強化
 - ミュート及びブロックのインポートを行えるように
+- クライアント: /share のクエリでリプライやファイル等の情報を渡せるように
 
 ### Bugfixes
 - クライアント: テーマの管理が行えない問題を修正
@@ -131,7 +132,6 @@
 - ワードミュートのドキュメントを追加
 - クライアントのデザインの調整
 - 依存関係の更新
-- /share のクエリでリプライやファイル等の情報を渡せるように
 
 ### Bugfixes
 - チャンネルを作成しているとアカウントを削除できないのを修正