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 - チャンネルを作成しているとアカウントを削除できないのを修正