Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
		
						commit
						5ca6e6b5df
					
				
					 6 changed files with 233 additions and 35 deletions
				
			
		|  | @ -17,6 +17,7 @@ | ||||||
| - API: ユーザーのリアクション一覧を取得する users/reactions を追加 | - API: ユーザーのリアクション一覧を取得する users/reactions を追加 | ||||||
| - API: users/search および users/search-by-username-and-host を強化 | - API: users/search および users/search-by-username-and-host を強化 | ||||||
| - ミュート及びブロックのインポートを行えるように | - ミュート及びブロックのインポートを行えるように | ||||||
|  | - クライアント: /share のクエリでリプライやファイル等の情報を渡せるように | ||||||
| 
 | 
 | ||||||
| ### Bugfixes | ### Bugfixes | ||||||
| - クライアント: テーマの管理が行えない問題を修正 | - クライアント: テーマの管理が行えない問題を修正 | ||||||
|  |  | ||||||
|  | @ -117,11 +117,28 @@ export default defineComponent({ | ||||||
| 			type: String, | 			type: String, | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
|  | 		initialVisibility: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: false | ||||||
|  | 		}, | ||||||
|  | 		initialFiles: { | ||||||
|  | 			type: Array, | ||||||
|  | 			required: false | ||||||
|  | 		}, | ||||||
|  | 		initialLocalOnly: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false | ||||||
|  | 		}, | ||||||
|  | 		visibleUsers: { | ||||||
|  | 			type: Array, | ||||||
|  | 			required: false, | ||||||
|  | 			default: () => [] | ||||||
|  | 		}, | ||||||
| 		initialNote: { | 		initialNote: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
| 		instant: { | 		share: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  | @ -150,8 +167,7 @@ export default defineComponent({ | ||||||
| 			showPreview: false, | 			showPreview: false, | ||||||
| 			cw: null, | 			cw: null, | ||||||
| 			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly, | 			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, | 			visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number], | ||||||
| 			visibleUsers: [], |  | ||||||
| 			autocomplete: null, | 			autocomplete: null, | ||||||
| 			draghover: false, | 			draghover: false, | ||||||
| 			quoteId: null, | 			quoteId: null, | ||||||
|  | @ -246,6 +262,18 @@ export default defineComponent({ | ||||||
| 			this.text = this.initialText; | 			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) { | 		if (this.mention) { | ||||||
| 			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`; | 			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`; | ||||||
| 			this.text += ' '; | 			this.text += ' '; | ||||||
|  | @ -321,7 +349,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 		this.$nextTick(() => { | 		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]; | 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; | ||||||
| 				if (draft) { | 				if (draft) { | ||||||
| 					this.text = draft.data.text; | 					this.text = draft.data.text; | ||||||
|  | @ -582,8 +610,6 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		saveDraft() { | 		saveDraft() { | ||||||
| 			if (this.instant) return; |  | ||||||
| 
 |  | ||||||
| 			const data = JSON.parse(localStorage.getItem('drafts') || '{}'); | 			const data = JSON.parse(localStorage.getItem('drafts') || '{}'); | ||||||
| 
 | 
 | ||||||
| 			data[this.draftKey] = { | 			data[this.draftKey] = { | ||||||
|  |  | ||||||
|  | @ -1,22 +1,38 @@ | ||||||
| <template> | <template> | ||||||
| <div class=""> | <div class=""> | ||||||
| 	<section class="_section"> | 	<section class="_section"> | ||||||
| 		<div class="_title" v-if="title">{{ title }}</div> |  | ||||||
| 		<div class="_content"> | 		<div class="_content"> | ||||||
| 			<XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/> | 			<XPostForm | ||||||
| 			<MkButton v-else primary @click="close()">{{ $ts.close }}</MkButton> | 				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> | ||||||
| 		<div class="_footer" v-if="url">{{ url }}</div> |  | ||||||
| 	</section> | 	</section> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | // SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md | ||||||
|  | 
 | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkButton from '@client/components/ui/button.vue'; | import MkButton from '@client/components/ui/button.vue'; | ||||||
| import XPostForm from '@client/components/post-form.vue'; | import XPostForm from '@client/components/post-form.vue'; | ||||||
| import * as os from '@client/os'; | import * as os from '@client/os'; | ||||||
|  | import { noteVisibilities } from '@/types'; | ||||||
|  | import { parseAcct } from '@/misc/acct'; | ||||||
| import * as symbols from '@client/symbols'; | import * as symbols from '@client/symbols'; | ||||||
|  | import * as Misskey from 'misskey-js'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -30,35 +46,139 @@ export default defineComponent({ | ||||||
| 				title: this.$ts.share, | 				title: this.$ts.share, | ||||||
| 				icon: 'fas fa-share-alt' | 				icon: 'fas fa-share-alt' | ||||||
| 			}, | 			}, | ||||||
| 			title: null, | 			state: 'fetching' as 'fetching' | 'writing' | 'posted', | ||||||
| 			text: null, |  | ||||||
| 			url: null, |  | ||||||
| 			initialText: null, |  | ||||||
| 			posted: false, |  | ||||||
| 
 | 
 | ||||||
|  | 			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); | 		const urlParams = new URLSearchParams(window.location.search); | ||||||
|  | 
 | ||||||
| 		this.title = urlParams.get('title'); | 		this.title = urlParams.get('title'); | ||||||
| 		this.text = urlParams.get('text'); | 		const text = urlParams.get('text'); | ||||||
| 		this.url = urlParams.get('url'); | 		const url = urlParams.get('url'); | ||||||
| 		 | 
 | ||||||
| 		let text = ''; | 		let noteText = ''; | ||||||
| 		if (this.title) text += `【${this.title}】\n`; | 		if (this.title) noteText += `[ ${this.title} ]\n`; | ||||||
| 		if (this.text) text += `${this.text}\n`; | 		// Googleニュース対策 | ||||||
| 		if (this.url) text += `${this.url}`; | 		if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, ''); | ||||||
| 		this.initialText = text.trim(); | 		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: { | 	methods: { | ||||||
| 		close() { | 		close() { | ||||||
| 			window.close() | 			window.close(); | ||||||
|  | 
 | ||||||
|  | 			// 閉じなければ100ms後タイムラインに | ||||||
|  | 			setTimeout(() => { | ||||||
|  | 				this.$router.push('/'); | ||||||
|  | 			}, 100); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  | .close { | ||||||
|  | 	margin: 16px auto; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ export default defineComponent({ | ||||||
| 			type: Object, | 			type: Object, | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
| 		instant: { | 		share: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  | @ -277,7 +277,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 		this.$nextTick(() => { | 		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]; | 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; | ||||||
| 				if (draft) { | 				if (draft) { | ||||||
| 					this.text = draft.data.text; | 					this.text = draft.data.text; | ||||||
|  | @ -507,8 +507,6 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		saveDraft() { | 		saveDraft() { | ||||||
| 			if (this.instant) return; |  | ||||||
| 
 |  | ||||||
| 			const data = JSON.parse(localStorage.getItem('drafts') || '{}'); | 			const data = JSON.parse(localStorage.getItem('drafts') || '{}'); | ||||||
| 
 | 
 | ||||||
| 			data[this.draftKey] = { | 			data[this.draftKey] = { | ||||||
|  |  | ||||||
							
								
								
									
										56
									
								
								src/docs/ja-JP/advanced/share-page.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/docs/ja-JP/advanced/share-page.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
|  | @ -1,13 +1,10 @@ | ||||||
| export type Acct = { | import * as Misskey from 'misskey-js'; | ||||||
| 	username: string; |  | ||||||
| 	host: string | null; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const getAcct = (user: Acct) => { | export const getAcct = (user: Misskey.Acct) => { | ||||||
| 	return user.host == null ? user.username : `${user.username}@${user.host}`; | 	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); | 	if (acct.startsWith('@')) acct = acct.substr(1); | ||||||
| 	const split = acct.split('@', 2); | 	const split = acct.split('@', 2); | ||||||
| 	return { username: split[0], host: split[1] || null }; | 	return { username: split[0], host: split[1] || null }; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue