diff --git a/CHANGELOG.md b/CHANGELOG.md index ec38ad9bdfe577d5c7a114340aa5afe1c54e4a93..3dcf57caab756a1344f4e1f93c75519dd702fb4a 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 - クライアント: テーマã®ç®¡ç†ãŒè¡Œãˆãªã„å•é¡Œã‚’ä¿®æ£ diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index a1d89d2a2ed4c9b17ee84341461a15d1f3260332..816a69e731eddf0777bd62ffb96e391a1c1ac461 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 67e598fa8f7edc70df6b964882e2e9ecc6914e86..70a9661dd0d6f6be09ab28cd2b6c4c0a850e629a 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 0cacaf77e723abd215486e9359c2071a878b08a1..64b8d08cbc8ea5567b602734ff42aeca9e4c8145 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 0000000000000000000000000000000000000000..75a9d14d29e7bdcadcbe449a066f181ba972d19f --- /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 16876c4429df9051221d7fd7629af403960126a8..5106b1a09e5cd5dcc1abc218c2f19e31af383b0b 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 };