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/search および users/search-by-username-and-host を強化 | ||||
| - ミュート及びブロックのインポートを行えるように | ||||
| - クライアント: /share のクエリでリプライやファイル等の情報を渡せるように | ||||
| 
 | ||||
| ### Bugfixes | ||||
| - クライアント: テーマの管理が行えない問題を修正 | ||||
|  |  | |||
|  | @ -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] = { | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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] = { | ||||
|  |  | |||
							
								
								
									
										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 = { | ||||
| 	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 }; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue