Skip to content

Commit 5ca6e6b

Browse files
committed
2 parents 98c26df + a4e3136 commit 5ca6e6b

File tree

6 files changed

+233
-35
lines changed

6 files changed

+233
-35
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- API: ユーザーのリアクション一覧を取得する users/reactions を追加
1818
- API: users/search および users/search-by-username-and-host を強化
1919
- ミュート及びブロックのインポートを行えるように
20+
- クライアント: /share のクエリでリプライやファイル等の情報を渡せるように
2021

2122
### Bugfixes
2223
- クライアント: テーマの管理が行えない問題を修正

src/client/components/post-form.vue

+32-6
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,28 @@ export default defineComponent({
117117
type: String,
118118
required: false
119119
},
120+
initialVisibility: {
121+
type: String,
122+
required: false
123+
},
124+
initialFiles: {
125+
type: Array,
126+
required: false
127+
},
128+
initialLocalOnly: {
129+
type: Boolean,
130+
required: false
131+
},
132+
visibleUsers: {
133+
type: Array,
134+
required: false,
135+
default: () => []
136+
},
120137
initialNote: {
121138
type: Object,
122139
required: false
123140
},
124-
instant: {
141+
share: {
125142
type: Boolean,
126143
required: false,
127144
default: false
@@ -150,8 +167,7 @@ export default defineComponent({
150167
showPreview: false,
151168
cw: null,
152169
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
153-
visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
154-
visibleUsers: [],
170+
visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
155171
autocomplete: null,
156172
draghover: false,
157173
quoteId: null,
@@ -246,6 +262,18 @@ export default defineComponent({
246262
this.text = this.initialText;
247263
}
248264
265+
if (this.initialVisibility) {
266+
this.visibility = this.initialVisibility;
267+
}
268+
269+
if (this.initialFiles) {
270+
this.files = this.initialFiles;
271+
}
272+
273+
if (typeof this.initialLocalOnly === 'boolean') {
274+
this.localOnly = this.initialLocalOnly;
275+
}
276+
249277
if (this.mention) {
250278
this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
251279
this.text += ' ';
@@ -321,7 +349,7 @@ export default defineComponent({
321349
322350
this.$nextTick(() => {
323351
// 書きかけの投稿を復元
324-
if (!this.instant && !this.mention && !this.specified) {
352+
if (!this.share && !this.mention && !this.specified) {
325353
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
326354
if (draft) {
327355
this.text = draft.data.text;
@@ -582,8 +610,6 @@ export default defineComponent({
582610
},
583611
584612
saveDraft() {
585-
if (this.instant) return;
586-
587613
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
588614
589615
data[this.draftKey] = {

src/client/pages/share.vue

+139-19
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
<template>
22
<div class="">
33
<section class="_section">
4-
<div class="_title" v-if="title">{{ title }}</div>
54
<div class="_content">
6-
<XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/>
7-
<MkButton v-else primary @click="close()">{{ $ts.close }}</MkButton>
5+
<XPostForm
6+
v-if="state === 'writing'"
7+
fixed
8+
:share="true"
9+
:initial-text="initialText"
10+
:initial-visibility="visibility"
11+
:initial-files="files"
12+
:initial-local-only="localOnly"
13+
:reply="reply"
14+
:renote="renote"
15+
:visible-users="visibleUsers"
16+
@posted="state = 'posted'"
17+
class="_panel"
18+
/>
19+
<MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton>
820
</div>
9-
<div class="_footer" v-if="url">{{ url }}</div>
1021
</section>
1122
</div>
1223
</template>
1324

1425
<script lang="ts">
26+
// SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md
27+
1528
import { defineComponent } from 'vue';
1629
import MkButton from '@client/components/ui/button.vue';
1730
import XPostForm from '@client/components/post-form.vue';
1831
import * as os from '@client/os';
32+
import { noteVisibilities } from '@/types';
33+
import { parseAcct } from '@/misc/acct';
1934
import * as symbols from '@client/symbols';
35+
import * as Misskey from 'misskey-js';
2036
2137
export default defineComponent({
2238
components: {
@@ -30,35 +46,139 @@ export default defineComponent({
3046
title: this.$ts.share,
3147
icon: 'fas fa-share-alt'
3248
},
33-
title: null,
34-
text: null,
35-
url: null,
36-
initialText: null,
37-
posted: false,
49+
state: 'fetching' as 'fetching' | 'writing' | 'posted',
3850
51+
title: null as string | null,
52+
initialText: null as string | null,
53+
reply: null as Misskey.entities.Note | null,
54+
renote: null as Misskey.entities.Note | null,
55+
visibility: null as string | null,
56+
localOnly: null as boolean | null,
57+
files: [] as Misskey.entities.DriveFile[],
58+
visibleUsers: [] as Misskey.entities.User[],
3959
}
4060
},
4161
42-
created() {
62+
async created() {
4363
const urlParams = new URLSearchParams(window.location.search);
64+
4465
this.title = urlParams.get('title');
45-
this.text = urlParams.get('text');
46-
this.url = urlParams.get('url');
47-
48-
let text = '';
49-
if (this.title) text += `【${this.title}】\n`;
50-
if (this.text) text += `${this.text}\n`;
51-
if (this.url) text += `${this.url}`;
52-
this.initialText = text.trim();
66+
const text = urlParams.get('text');
67+
const url = urlParams.get('url');
68+
69+
let noteText = '';
70+
if (this.title) noteText += `[ ${this.title} ]\n`;
71+
// Googleニュース対策
72+
if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, '');
73+
else if (text && this.title !== text) noteText += `${text}\n`;
74+
if (url) noteText += `${url}`;
75+
this.initialText = noteText.trim();
76+
77+
const visibility = urlParams.get('visibility');
78+
if (noteVisibilities.includes(visibility)) {
79+
this.visibility = visibility;
80+
}
81+
82+
if (this.visibility === 'specified') {
83+
const visibleUserIds = urlParams.get('visibleUserIds');
84+
const visibleAccts = urlParams.get('visibleAccts');
85+
await Promise.all(
86+
[
87+
...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
88+
...(visibleAccts ? visibleAccts.split(',').map(parseAcct) : [])
89+
]
90+
// TypeScriptの指示通りに変換する
91+
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
92+
.map(q => os.api('users/show', q)
93+
.then(user => {
94+
this.visibleUsers.push(user);
95+
}, () => {
96+
console.error(`Invalid user query: ${JSON.stringify(q)}`);
97+
})
98+
)
99+
);
100+
}
101+
102+
const localOnly = urlParams.get('localOnly');
103+
if (localOnly === '0') this.localOnly = false;
104+
else if (localOnly === '1') this.localOnly = true;
105+
106+
try {
107+
//#region Reply
108+
const replyId = urlParams.get('replyId');
109+
const replyUri = urlParams.get('replyUri');
110+
if (replyId) {
111+
this.reply = await os.api('notes/show', {
112+
noteId: replyId
113+
});
114+
} else if (replyUri) {
115+
const obj = await os.api('ap/show', {
116+
uri: replyUri
117+
});
118+
if (obj.type === 'Note') {
119+
this.reply = obj.object;
120+
}
121+
}
122+
//#endregion
123+
124+
//#region Renote
125+
const renoteId = urlParams.get('renoteId');
126+
const renoteUri = urlParams.get('renoteUri');
127+
if (renoteId) {
128+
this.renote = await os.api('notes/show', {
129+
noteId: renoteId
130+
});
131+
} else if (renoteUri) {
132+
const obj = await os.api('ap/show', {
133+
uri: renoteUri
134+
});
135+
if (obj.type === 'Note') {
136+
this.renote = obj.object;
137+
}
138+
}
139+
//#endregion
140+
141+
//#region Drive files
142+
const fileIds = urlParams.get('fileIds');
143+
if (fileIds) {
144+
await Promise.all(
145+
fileIds.split(',')
146+
.map(fileId => os.api('drive/files/show', { fileId })
147+
.then(file => {
148+
this.files.push(file);
149+
}, () => {
150+
console.error(`Failed to fetch a file ${fileId}`);
151+
})
152+
)
153+
);
154+
}
155+
//#endregion
156+
} catch (e) {
157+
os.dialog({
158+
type: 'error',
159+
title: e.message,
160+
text: e.name
161+
});
162+
}
163+
164+
this.state = 'writing';
53165
},
54166
55167
methods: {
56168
close() {
57-
window.close()
169+
window.close();
170+
171+
// 閉じなければ100ms後タイムラインに
172+
setTimeout(() => {
173+
this.$router.push('/');
174+
}, 100);
58175
}
59176
}
60177
});
61178
</script>
62179

63180
<style lang="scss" scoped>
181+
.close {
182+
margin: 16px auto;
183+
}
64184
</style>

src/client/ui/chat/post-form.vue

+2-4
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export default defineComponent({
100100
type: Object,
101101
required: false
102102
},
103-
instant: {
103+
share: {
104104
type: Boolean,
105105
required: false,
106106
default: false
@@ -277,7 +277,7 @@ export default defineComponent({
277277
278278
this.$nextTick(() => {
279279
// 書きかけの投稿を復元
280-
if (!this.instant && !this.mention && !this.specified) {
280+
if (!this.share && !this.mention && !this.specified) {
281281
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
282282
if (draft) {
283283
this.text = draft.data.text;
@@ -507,8 +507,6 @@ export default defineComponent({
507507
},
508508
509509
saveDraft() {
510-
if (this.instant) return;
511-
512510
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
513511
514512
data[this.draftKey] = {

src/docs/ja-JP/advanced/share-page.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# シェアページ
2+
`/share`を開くと、共有用の投稿フォームを開くことができます。
3+
ここではシェアページで利用できるクエリ文字列の一覧を示します。
4+
5+
## クエリ文字列一覧
6+
### 文字
7+
8+
<dl>
9+
<dt>title</dt>
10+
<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
11+
<dt>text</dt>
12+
<dd>本文です。</dd>
13+
<dt>url</dt>
14+
<dd>URLです。末尾に挿入されます。</dd>
15+
</dl>
16+
17+
18+
### リプライ情報
19+
以下のいずれか
20+
21+
<dl>
22+
<dt>replyId</dt>
23+
<dd>リプライ先のノートid</dd>
24+
<dt>replyUri</dt>
25+
<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
26+
</dl>
27+
28+
### Renote情報
29+
以下のいずれか
30+
31+
<dl>
32+
<dt>renoteId</dt>
33+
<dd>Renote先のノートid</dd>
34+
<dt>renoteUri</dt>
35+
<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
36+
</dl>
37+
38+
### 公開範囲
39+
※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
40+
41+
<dl>
42+
<dt>visibility</dt>
43+
<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
44+
<dt>localOnly</dt>
45+
<dd>0(false) or 1(true)</dd>
46+
<dt>visibleUserIds</dt>
47+
<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
48+
<dt>visibleAccts</dt>
49+
<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
50+
</dl>
51+
52+
### ファイル
53+
<dl>
54+
<dt>fileIds</dt>
55+
<dd>添付したいファイルのid(カンマ区切りで)</dd>
56+
</dl>

src/misc/acct.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
export type Acct = {
2-
username: string;
3-
host: string | null;
4-
};
1+
import * as Misskey from 'misskey-js';
52

6-
export const getAcct = (user: Acct) => {
3+
export const getAcct = (user: Misskey.Acct) => {
74
return user.host == null ? user.username : `${user.username}@${user.host}`;
85
};
96

10-
export const parseAcct = (acct: string): Acct => {
7+
export const parseAcct = (acct: string): Misskey.Acct => {
118
if (acct.startsWith('@')) acct = acct.substr(1);
129
const split = acct.split('@', 2);
1310
return { username: split[0], host: split[1] || null };

0 commit comments

Comments
 (0)